likes
comments
collection
share

JS DOM 操作 "武功秘籍"

作者站长头像
站长
· 阅读数 16

前言

在原生 JS、JQuery 时代,页面中几乎所有的交互都需要使用 DOM 完成操作。随着以 数据驱动视图更新 为核心的 框架(React、Vue 等) 的出现,使页面中数据更新视图变得简单。

尽管框架语法让视图更新变得简单,但这并不意味着 DOM 相关操作彻底被弃用。很多场景下实现一些交互功能离不开原生 DOM 相关语法使用。

本篇,是记录笔者在工作中不同场景下使用 JS DOM 操作的一个总结,或许这本 "武功秘籍" 会对屏幕前的你有所帮助。

内容大纲:

    1. DOM 基础操作 - 增删改查
    1. DOM 节点关系
    1. DOM 属性操作
    1. Node 节点类型
    1. 更详细的 DOM 事件
    1. 元素的尺寸及位置信息
    1. 鼠标事件的坐标信息
    1. JS 控制容器滚动位置
    1. 聊一聊 iframe 元素

一、DOM 基础操作 - 增删改查

除了基础 JS 语法语句外,对页面中 DOM 元素进行 增删改查 也是作为 JS 初学者必备技能。下面我们列举场景的 DOM 操作满足日常工作使用。

1、查找 DOM

JS 提供查找 DOM 的方式很多,比如:

根据元素的 id 属性查找:

<div id="container"></div>
const ele = document.getElementById('container');
// 小技巧:由于 id 值要求唯一,通过 window[id] 也可以访问到 DOM 元素
window.container

根据元素的 class 属性查找,注意它返回的是一个数组,是匹配到的所有 DOM:

const ele = document.getElementsByClassName('container')[0];

通用查找元素,当存在多个匹配元素时,只返回第一个,它支持的查找规则有很多,比如:

// 根据 id 查找
const ele = document.querySelector('.container');
// 根据 class 查找
const ele = document.querySelector('.container');
// 根据 标签 查找
const ele = document.querySelector('div');
// 但 id、class 不能是一个纯数字,比如不能将一个数据 id 作为查找规则,否则会视为无效并报错,如:
const ele = document.querySelector('#567'); // error.

获取页面中 form 表单集合,没有时返回一个空数组:

const forms = document.forms;

获取 head、body 元素,可直接从 document 中获取:

const head = document.head;
const body = document.body;

获取文档中当前处于 聚焦 的元素(如:input 输入框)

const activeElement = document.activeElement;

注意,getElementById 和 forms 只能在 document 上使用,其他方式支持在任意 DOM 元素中使用。

2、创建 DOM

document 作为页面中最顶级的元素节点(文档),提供了创建节点的方式:

创建一个 DOM 元素:

const box = document.createElement('div');

创建一个 文本 节点:

const text = document.createTextNode('文本内容');

克隆一个 节点:

// 语法:
nodeEle.cloneNode(boolean); // boolean 值为 false 标识只克隆该元素,为 true 会克隆所有子元素。

const cloneBody = document.body.cloneNode(true);

创建一个文档片段(内存变量):

const fragment = document.createDocumentFragment();

由于文档片段只是创建在内存中,并不在 DOM 树中,通常会使用它来进行批量操作元素,避免重复引起页面回流,从而提升性能。如下示例:

<ul id="list"></ul>
<script>
  const fragment = document.createDocumentFragment();
  [1, 2, 3, 4, 5].forEach(num => {
    const li = document.createElement('li');
    li.textContent = num;
    fragment.appendChild(li);
  });
  list.appendChild(fragment);
</script>

3、添加 DOM

比较常用的是,在容器末尾添加一个元素:

// 语法:
parent.appendChild(child);

const container = document.querySelector('.container');
const child = document.createElement('span');
container.appendChild(child);

如果我们要在 容器内 某个元素之前插入新节点,JS 提供了方法:

// 语法
parent.insertBefore(newChild, targetChild);

<div class="container">
  <span class="child1"></span>
</div>

const container = document.querySelector('.container');
const child = document.createElement('span');
container.insertBefore(child, container.querySelector('.child1'));

如果我们想要在 容器内 某个元素之后插入新节点,JS 并为提供原生方法,我们自己实现一个 insertAfter(面试题):

function insertAfter(newChild, targetChild) {
  const parent = targetChild.parentNode;

  if (parent.lastChild === targetChild) {
    parent.appendChild(newChild);
  } else {
    parent.insertBefore(newChild, targetChild.nextSibling);
  }
}

const container = document.querySelector('.container');
const child = document.createElement('span');
insertAfter(child, container.querySelector('.child1'));

将容器内 指定子节点 替换为新节点:

// 语法
parent.replaceChild(newChild, targetChild);

4、删除 DOM

将 DOM 元素从容器内移除:

// 语法
parent.removeChild(targetChild);

const container = document.querySelector('.container');
container.removeChild(container.querySelector('.child'));

5、修改 DOM

修改 DOM 元素内容为一个子元素结构:

const container = document.querySelector('.container');
container.innerHTML = `<span>child</span>`;

修改 DOM 元素内容为一个文本内容:

const container = document.querySelector('.container');
container.textContent = "容器";

二、DOM 节点关系

除了通过 id、class 查找元素外,借助 DOM 树元素之间的关系也是一种获取元素的方式。

获取 父 元素/节点:

ele.parentNode:父节点,节点包括 ElementDocument
ele.parentElement:父元素,与 parentNode 区别是,其父节点必须是一个 Element 元素。

获取 子 元素/节点 集合:

ele.children: 返回子元素集合,只返回元素节点;
ele.childNodes:返回 Node 节点列表,可能包含文本节点(换行也会转为文本节点)、注释节点。

获取 关系 节点:

ele.firstChild:返回第一个子节点(元素、文本、注释),不存在返回 null
ele.lastChild:返回最后一个子节点(元素、文本、注释),不存在返回 null
ele.firstElementChild:返回第一个元素节点;
ele.lastElementChild:返回最后一个元素节点;

ele.previousSibling:返回节点的前一个节点(元素、文本、注释);
ele.nextSibling:返回节点的后一个节点(元素、文本、注释);
ele.previousElementSibling:返回节点的前一个元素节点;
ele.nextElementSiblng:返回节点的后一个元素节点。

三、DOM 属性操作

一个 DOM 元素除了可以设置 id、class 属性外,还可以 自定义属性(通常以 data- 规范开头) 来实现绑定数据。

为元素设置属性:

// 语法:
element.setAttribute(name, value);

const container = document.querySelector('.container');
container.setAttribute("id", "container");
// 自定义属性
container.setAttribute("data-index", "1");

获取元素指定属性:

// 语法:
element.getAttribute(name);

const container = document.querySelector('.container');
container.getAttribute("class");

判断元素是否存在指定属性,存在返回 true,不存在时返回 false:

// 语法:
element.hasAttribute(name);

const container = document.querySelector('.container');
container.hasAttribute('class');

移除元素指定属性:

container.removeAttribute("class");

获取元素的所有属性名称,如:id、class data-index 等

// 语法:
element.attributes

获取元素所有以 data- 开头的自定义属性及属性值,返回值是一个对象,

// 语法:
element.dataset

四、Node 节点类型

我们知道,DOM 元素只是 DOM Node 节点中的一类,在 DOM 中除了 元素 外,还包含一些其他类型节点 Nodes,如:文档节点,文本节点、注释节点 等。

那么如何判断一个 Node 节点属于什么类型呢,可通过节点的 nodeType 来获取节点类型:

<div class="container" id="1">
  <span class="child"></span>
  <!-- 这是注释节点 -->
  这是文本节点
</div>

const container = document.querySelector('.container');
Array.from(container.childNodes).forEach(node => {
  console.log(node.nodeType);
});

输出结果如下:

3
1
3
8
3

nodeType 是一个从 1 开始的 number 值,每个数值代表了不同类型的节点。上例中 换行 和 文本 属于文本节点(类型 为 3),DOM 元素属于元素节点(类型为 1),注释属于注释节点(类型为 8)。

常见的 Nodes 节点类型如下:

1 ELEMENT_NODE  元素节点
2 ATTRIBUTE_NODE  属性节点
3 TEXT_NODE  文本节点
4 CDATA_SECTION_NODE  CDATA区段
5 ENTITY_REFERENCE_NODE 实体引用元素
6 ENTITY_NODE  实体
7 PROCESSING_INSTRUCTION_NODE  表示处理指令
8 COMMENT_NODE  注释节点
9 DOCUMENT_NODEdocument 
10 DOCUMENT_TYPE_NODE  <!DOCTYPE>
11 DOCUMENT_FRAGMENT_NODE  文档碎片节点
12 NOTATION_NODE  DTD中声明的符号节点

如果要进一步区分元素属于那一类标签,比如是否属于 li 标签元素,可通过 el.tagName 判断,这在事件委托中非常有用。

const clickList = (event: MouseEvent<HTMLElement>) => {
  if ((event.target as HTMLElement).tagName === "LI") {
    // ...
  }
};

五、更详细的 DOM 事件

在网页中做任何交互,都离不开 事件 交互,常见的事件行为:鼠标事件、键盘事件、输入事件、滚动事件 等

大多数事件交互都可以作用在 window、document 以及 DOM 元素 上。

1、绑定事件

在 DOM 编程中,可以使用两种方式来绑定事件:onEventaddEventListener

onEvent 使用很简单,可直接在 HTML 元素上使用 onEvent 属性,或者给 DOM 引用设置 onEvent 来指定事件处理程序。

<div class="container" onclick="handleClick(event)"></div> // event 为固定关键字
const handleClick = (event) => {
  console.log(event);
}
// or
const container = document.querySelector('.container');
container.onclick = (event) => {
  console.log(event);
}

它的缺点是只能指定一个事件处理程序,添加多个处理程序则会覆盖之前的处理程序。且不支持设定 事件阶段(属于 冒泡阶段)。

addEventListener 使用上灵活很多,通过调用 target.addEventListener 方法来绑定事件处理程序。

// 语法:
el.addEventListener(type, listener[, useCapture]);
  • el: 事件绑定的目标对象,比如 window、document 以及 DOM 标签元素;
  • type: 事件类型,如:click、mousedown,注意这里的事件类型不用加前缀 on
  • listener: 事件处理函数,函数接收 event 事件对象作为参数;
  • useCapture: 是否为捕获阶段,默认 false 冒泡,值为 true 时是捕获;

此外,useCapture 第三参数可以为一个对象:

el.addEventListener(type, listener, {
  capture: false, // 设置 冒泡 或者 捕获
  once: false,    // 是否设置单次监听, 如果为 true 会在调用后自动销毁listener
  passive: false  // 是否让 阻止事件默认行为(preventDefault()) 失效,如果为 true, 意味着 listener 将无法通过 preventDefault 阻止事件默认行为
})

扩展知识listener 的另一种形式 - 对象。

通常 listener 是一个函数,但也可以传递一个对象(或者实例对象,如:Sortable),当传入一个对象时,要求这个对象必须提供 handleEvent 方法,所有事件的触发都会进入此方法。

这样使用的好处之一是:通过 handleEvent 方法来拿到所在对象,能够使用对象上的信息:

const obj = {
  name: 'foo',
  handleEvent: function () {
    alert('click name=' + this.name);
  }
};
document.body.addEventListener('click', obj, false);

其次,将不同事件放在一起,让程序更加内聚:

const obj = {
  name: 'foo',
  handleEvent: function (e) {
    switch (e.type) {
      case "click":
        console.log("click event");
        break;
      case "mousedown":
        console.log("mousedown event");
        break;
    }
  }
};

document.body.addEventListener('click', obj, false);
document.body.addEventListener('mousedown', obj, false);

注意:这是 DOM2 的标准,IE6、7、8 版本浏览器不支持。

2、事件阶段 - 冒泡与捕获

事件的 冒泡(event bubbling)捕获(event capturing)是指在 DOM 中处理事件时的两种不同的传播方式。

  • 冒泡: 当一个元素触发了某个事件,该事件会从该元素开始向上冒泡传播到父元素,直到传播到最顶层的元素(window)。例如,当点击一个按钮时,点击事件会先触发按钮的点击事件,然后依次触发按钮的父元素、父元素的父元素,直到最顶层的元素。
  • 捕获: 与冒泡相反,捕获是从最顶层的元素开始,逐级向下传播到触发事件的元素。

在 DOM 事件处理中,默认情况下,事件是按照冒泡方式进行传播的。但是可以通过 addEventListener() 方法的第三个参数 useCapture 来设置事件的传播方式,将其设置为 true 可以使用捕获方式进行传播。

从下面这个示例理解一下两者:

<div id="parent">
  <div id="child">
    <button id="button">Click me</button>
  </div>
</div>

<script>
  const parent = document.getElementById('parent');
  const child = document.getElementById('child');
  const button = document.getElementById('button');

  // 冒泡
  parent.addEventListener('click', function() {
    console.log('bubbling Parent');
  });
  child.addEventListener('click', function() {
    console.log('bubbling Child');
  });
  button.addEventListener('click', function() {
    console.log('bubbling Button');
  });

  // 捕获
  parent.addEventListener('click', function() {
    console.log('capturing Parent');
  }, true);
  child.addEventListener('click', function() {
    console.log('capturing Child');
  }, true);
  button.addEventListener('click', function() {
    console.log('capturing Button');
  }, true);
</script>

// 输出如下:
capturing Parent
capturing Child
capturing Button
bubbling Button
bubbling Child
bubbling Parent

可见,当 冒泡 与 捕获 共存时,先执行 捕获,后执行 冒泡。(Chrome 主流浏览器)

借助事件 冒泡和捕获 的特性可以实现 事件委托(event delegation),即将事件处理程序绑定到父元素上,通过冒泡或捕获传播到子元素上触发相应的事件处理程序,可以减少事件处理程序的数量,提高性能。

3、鼠标 移入移出 事件如何选择

在使用 JS 实现元素 移入移除 功能时,有两组可选的交互事件:onmouseover/onmouseout 与 onmouseenter/onmouseleave

  • onmouseover: 移入事件,移入到目标元素或其子元素时触发;
  • onmouseout: 移出事件,移除目标元素或其子元素时触发;
  • onmouseenter: 移入事件,移入目标元素时触发;
  • onmouseleave: 移出事件,移出目标元素时触发;

两组 移入移出 事件的区别在于:

  • onmouseover/onmouseout 会在目标元素及其子元素中触发,比如 移入目标元素后再移入到子元素,会依次触发:目标元素 onmouseover(移入) -> 目标元素 onmouseout(移出) -> 子元素 onmouseover(移入);(示例 1)
  • onmouseenter/onmouseleave 移入到目标元素或其子元素时,过程中仅触发一次事件,但在 event.target 属性会返回触发事件的元素或其子元素;(示例 2)。

示例一:onmouseover/onmouseout

<div class="target">
  <p class="child"></p>
</div>

<script>
  const target = document.querySelector('.target'),
    child = document.querySelector('.child');

  target.addEventListener('mouseover', event => {
    console.log('移入 ', event.target);
  });
  target.addEventListener('mouseout', event => {
    console.log('移出 ', event.target);
  });

  // 输出:
  // 移入 <div class="target">...</div>
  // 移出 <div class="target">...</div>
  // 移入 <p class="child"></p>
</script>

示例二:onmouseenter/onmouseleave

<script>
  const target = document.querySelector('.target'),
    child = document.querySelector('.child');

  target.addEventListener('mouseenter', event => {
    console.log('移入 ', event.target);
    // event.target 属性会返回触发事件的元素或其子元素
    // 如果你希望在事件处理程序中获取绑定事件的元素,而不是子元素,你可以使用 event.currentTarget 属性。
    // event.currentTarget 属性始终指向绑定事件的元素,而不是触发事件的元素。
    
    // 另外,要避免在 await 语句的下方去使用 event.currentTarget,否则你可能拿到的是 null。
    // 这是因为:event.currentTarget 不能在异步代码中获取该信息,只能以同步方式去访问。
  });
  target.addEventListener('mouseleave', event => {
    console.log('移出 ', event.target);
  });

  // 输出:
  // 移入 <div class="target">...</div>
  // 移出 <div class="target">...</div>
</script>

基于两组事件的特性,可根据业务场景选择使用。比如你想通过 事件委托 来优化事件绑定,可以使用 onmouseover/onmouseout,如果 只想为目标元素绑定事件,使用 onmouseenter/onmouseleave

4、拖拽上传图片原理

通常涉及文件上传的需求,除了支持点击选择本地文件外,通常还会支持能够将图片拖动到区域内进行上传。

假设我们现在有一个拖拽区域:

<div id="container"></div>

现在我们希望容器能够支持被拖放图片并拿到 Files 信息,需要使用 ondragoverondrop 来实现(要阻止默认行为)

const container = document.querySelector('#container');
container.addEventListener('dragover', event => event.preventDefault());
container.addEventListener('drop', event => {
  event.preventDefault();
  const files = event.dataTransfer.files;
  console.log(files);
});

5、计算鼠标按下后移动的距离

这个交互需求其实很常见,比如我们自定义一个视频播放器的进度条,按住进度条可拖动修改进度,根据拖动的距离来计算进度。

实现此交互需要结合三个鼠标事件:拖动元素的按下事件(onmousedown)、document 移动事件(onmousemove) 和 document 松开事件(onmouseup)

const container = document.querySelector('#container');
container.addEventListener('mousedown', event => {
  event.stopPropagation();
  const startX = event.clientX;

  const onMouseMove = (event) => {
    event.stopPropagation();
    const clientX = event.clientX;
    console.log(`移动了 ${clientX - startX} px`);
  };

  const onMouseUp = (event) => {
    event.stopPropagation();
    document.removeEventListener('mousemove', onMouseMove);
    document.removeEventListener('mouseup', onMouseUp);
  }
  
  document.addEventListener('mousemove', onMouseMove);
  document.addEventListener('mouseup', onMouseUp);
});

6、持续更新中...

六、元素的尺寸及位置信息

1、DOM 自身属性

1、offsetWidth:返回元素的宽度(包括元素宽度、内边距和边框,不包括外边距);

2、offsetHeight:返回元素的高度(包括元素高度、内边距和边框,不包括外边距);

3、clientWidth:返回元素的宽度(包括元素宽度、内边距,不包括边框和外边距);

4、clientHeight:返回元素怒的高度(包括元素高度、内边距,不包括边框和外边距);

如果元素 display: none 或者是 inline 行内元素,获取到的 clientWidth 为 0(行内元素 尽管有文本撑开了元素,但 width 和 height 依旧为 0)。

5、style.width: 返回元素宽度(包括元素宽度,不包括内边距、边框和外边距);

6、style.height: 返回元素高度(包括元素高度,不包括内边距、边框和外边距);

仅在为元素设置了内联样式 style.width、style.height 可以拿到(带 px 单位的字符串),否则拿到的是空。建议使用 getBoundingClientRect() 获取元素的 宽高。

7、scrollWidth:返回元素的宽度(包括元素宽度、内边距和溢出尺寸,不包括边框和外边距),无溢出情况,与clientWidth相同;

8、scrollHeight:返回元素的高度(包括元素高度、内边距和溢出尺寸,不包括边框和外边距),无溢出情况,与clientHeight相同;

9、offsetLeft:返回当前元素距离 offsetParent 左边 的偏移量,IE怪异模型以父元素为参照,DOM 模式以最近一个定位父元素进行偏移设置位置,都没有以window为参照物

10、offsetTop:返回当前元素距离 offsetParent 上边 的偏移量;

offsetParent 是指最近一个设置了 position: relative 的父元素,没有则是 body。

11、scrollLeft: 设置或获取位于对象左边界和窗口中可见内容的最左端之间的距离;

12、scrollTop: 设置或获取位于对象最顶端和窗口中可见内容的最顶端之间的距离。

2、getBoundingClientRect

ele.getBoundingClientRect() 用于获取某个元素相对于视窗的位置集合。集合中有 top, right, bottom, left 等属性。

// 语法:
const container = document.querySelector('#container');
const rect = container.getBoundingClientRect();

1、rect.top:元素的上边到视窗上边的距离;

2、rect.right:元素的右边到视窗左边的距离(注意是到视窗左边);

3、rect.bottom:元素的下边到视窗上边的距离(注意是到视窗上边);

4、rect.left:元素的左边到视窗左边的距离;

5、rect.x:等价于 rectObject.left;

6、rect.y:等价于 rectObject.top;

7、rect.width:元素的宽度(包括边框、padding);

8、rect.height:元素的高度(包括边框、padding)。

3、计算 DOM 元素距离指定父元素左边的距离

借助 ele.offsetParentele.offsetLeft 可以轻松实现元素与父元素左侧的距离,顶部距离同理。

export function getDistanceFromParentLeft(element: HTMLElement, parent: HTMLElement) {
  let distance = 0;
  while (element && element !== parent) {
    distance += element.offsetLeft;
    element = element.offsetParent as HTMLElement; // 注意这里是 offsetParent
  }
  return distance;
}

let element = document.getElementById('myElement');
let parent = document.getElementById('myParent');
let distance = getDistanceFromParentLeft(element, parent);
console.log(distance);

4、五种获取元素宽高的方式

<div id="element" style="width: 200px; height: 100px; padding: 10px; margin: 10px; border: 1px solid pink;"></div>

const ele = document.getElementById("element"); 

// 方式一:通过元素 style 获取,不包括 包括 padding 和 border,得到的是字符串
// element.style 读取的只是元素的内联样式,即写在元素的 style 属性上的样式
console.log(ele.style.width); // '200px'
console.log(ele.style.height); // '100px'

// 方式二:通过 window 提供的计算元素样式方法获取,得到的是字符串,包括单位 px
// getComputedStyle 读取的样式是最终样式,包括了内联样式、嵌入样式和外部样式。
console.log(window.getComputedStyle(ele).width); // '200px'
console.log(window.getComputedStyle(ele).height); // '100px'

// 方式三:通过 element.offsetWidth 来获取,并且包括 padding 和 border 得到的尺寸不带单位 number
// 支持 内联样式、嵌入样式和外部样式。
console.log(ele.offsetWidth); // 222
console.log(ele.offsetHeight); // 122

// 方式四:通过 element.clientWidth 获取,包括 padding 得到的尺寸不带单位 number
console.log(ele.clientWidth); // 220
console.log(ele.clientHeight); // 120

// 方式五:获取元素的宽、高、位置等信息,得到的是 number 类型,不带单位,支持 内联样式、嵌入样式和外部样式。
// 并且 width、height 包括边框、padding,不包括 margin,一般用于鼠标移动场景
const rect = ele.getBoundingClientRect();
console.log(rect.width); // 222
console.log(rect.height); // 122

// 输出:
200px
100px
200px
100px
222
122
220
120
222
122

七、鼠标事件的坐标信息

实现 拖动、移动 交互需要根据鼠标事件中的位置信息实现。鼠标事件有很多,不过每个事件中关于距离的属性含义是一样的,这里以 mousemove 举例:

const container = document.querySelector('#container');
container.addEventListener('mousemove', event => {
  event.stopPropagation();
  console.log("event: ", event);
});

1、event.clientX:鼠标相对于浏览器有效区域左上角 x 轴的坐标,不随滚动条滚动而改变;

2、event.clientY:鼠标相对于浏览有效区域左上角 y 轴的坐标,不随滚动条滚动而改变;

3、event.pageX:鼠标相对于浏览器有效区域左上角 x 轴的坐标,随滚动条滚动而改变;

4、event.pageY:鼠标相对于浏览有效区域左上角 y 轴的坐标,随滚动条滚动而改变;

5、event.offsetX:相对于事件源 (event.target 目标元素) 左上角 水平 偏移;

6、event.offsetY:相对于事件源 (event.target 目标元素) 左上角 垂直 偏移;

7、event.screenX:鼠标相对于显示器屏幕左上角 x 轴坐标;

8、event.screenY:鼠标相对于显示器屏幕左上角 y 轴坐标;

9、event.layerX:相对于 offsetParent 左上角的 水平 偏移;

10、event.layerY:相对于 offsetParent 左上角的 水平 偏移。

八、JS 控制容器滚动位置

相信大家在实现一个数据列表时都会遇到这样一个交互:当滚动处于列表底部时,改变顶部固定的筛选项重新获取数据,我们期望列表滚动位置能够回到顶部。

下面我们来聊一聊实现 页面滚动 的几种方式。

1、锚点方式:(CSS 方式)

<div id="topAnchor"></div>
<a href="#topAnchor">回到顶部</a>

2、scrollTop:是元素上一个可读写的属性,通过设置为 0 回到滚动容器顶部

document.body.scrollTop = document.documentElement.scrollTop = 0;

3、scrollTo(x, y):滚动到当前window中的指定位置,设置scrollTo(0, 0) 可以实现回到顶部的效果:

window.scrollTo(0, 0);

4、scrollIntoView 方法用于将滚动条滚动到指定元素的位置,可用于代替 a 标签的 href 属性来实现锚点跳转:(适用于元素平滑滚动)

document.body.scrollIntoView(true);
document.getElementById('app').scrollIntoView(true); // 滚动到某个锚点元素
document.getElementById('root').scrollIntoView({ behavior: "smooth" }); // 过度效果

5、实现一个 平滑滚动:(具有滚动效果)

const handleScrollTop = () => {
  let sTop = document.documentElement.scrollTop || document.body.scrollTop;
  if (sTop > 0) {
    // 1000 / 60 --> 16.666... 大约每秒执行 60 次回调
    window.requestAnimationFrame(handleScrollTop);
    window.scrollTo(0, sTop - sTop / 10);
  }
}

6、平滑滚动 的其他方式:

// 方式一:
window.scrollTo({ top: element.offsetTop - 50, behavior: 'smooth' });
// 方式二:
body: { scroll-behavior: smooth; }

九、聊一聊 iframe 元素

iframe 是一个比较强大的标签元素,在日常业务开发可能很少用到,基于它能够设置 src 来渲染 HTML 页面的能力,通常用来接入第三方网站到本网站。

下面我们来聊聊在使用 iframe 时的一些使用事项。

1、判断一个页面是否运行在 iframe 内

有时我们一个网站能够 独立 运行在浏览器 Tab 页,也能够通过 iframe 被嵌入在其他网站内。而判断是否运行在 iframe 内的方式可以是:

  • 通过 window.selfwindow.top 判断:

window.self 指向当前 window,window.top 返回最顶层窗口的引用,如果是在 iframe 下,window.top 将指向外部引用 iframe 的窗口(父页面)。

const isRunInIframe = window.self !== window.top;
  • 通过 window.parent 属性:

window.parent 属性返回当前窗口的父窗口。如果页面在 iframe 中运行,window.parent 将返回父窗口的引用,否则返回当前窗口的引用。

const isRunInIframe = window.parent !== window;
  • 通过 window.frameElement 属性:

如果网站是在 iframe 内,这个值将返回这个 iframe 元素,否则返回 null。

const isRunInIframe = window.frameElement !== null;

注意,如果 iframe 加载的内容(页面)来自不同的域名或协议,window.frameElement 得到的始终是 null,只有在同源下才会有值。

2、操作 iframe 中的 DOM 元素

在使用 iframe 的上级页面,若想操作 iframe 里面的 DOM 元素,通过 iframe 元素的两个属性可以访问:

  • iframe.contentWindow: 指向 iframe 页面内的 window 全局对象;
  • iframe.contentDocument: 指向 iframe 页面内的 window.document 文档对象。

获取 iframe 内的元素,需要在 iframe 加载完成后操作:

// iframe 加载完成后触发的函数
iframe.onload = function() {
  console.log(iframe.contentWindow.document.querySelector("#root"));
  console.log(iframe.contentDocument.querySelector("#root"));
}

注意,如果 iframe 加载的内容(页面)来自不同的域名或协议,父页面访问 iframe.contentDocument 得到是 null,且访问 iframe.contentWindow.document 会提示跨域,这是浏览器的安全策略。一般不建议直接去操作 DOM,建议使用下面 通信 方式。

3、如何跳过跨域去访问 iframe 内容?

从上面我们知道,两个不同源的页面,相互访问时会被跨域拦截。

要实现在不同域名的 iframe 内部页面与外部页面进行通信,可以使用 postMessage() 方法在不同域名的窗口之间安全地传递消息。

假设我们有两个页面:index.html 和 iframePage.html(为了模拟跨域,这里起了一个 3000 本地服务)。

  • 父页面 向 子页面 发送消息:(在父窗口中操作子窗口发消息,然后让子窗口接收自己刚才发的消息。
// index.html
<iframe id="iframe" src="http://localhost:3000/"></iframe>

<script>
  const iframe = document.querySelector("#iframe");
  iframe.onload = function() {
    // 加载完成后由 父页面 向 iframe 页面发送一条消息
    // 参数一:要发送的数据,
    // 参数二:目标窗口的源(origin),用于指定将发送消息到具有特定源(origin)的窗口,即要发送到哪个 url,一般为 iframe 页面的 url,也可用 * 代替。
    iframe.contentWindow.postMessage("父页面发送第一条消息.", "http://localhost:3000");
  }
</script>

// iframePage.html
window.addEventListener("message", event => {
  // event.origin 可用于判断要接收哪个网站发送过来的消息。
  console.log("iframe message event: ", event.data);
});
  • 子页面 向 父页面 发送消息:(在子窗口中操作父窗口发消息,然后让父窗口接收自己刚才发的消息
// iframePage.html
window.parent.postMessage("iframe 页面发送第一条消息.", "父页面的 origin 或者使用 *");

// index.html
window.addEventListener("message", event => {
  // event.origin 可用于判断要接收哪个网站发送过来的消息。
  console.log("index message event: ", event.data);
});

所谓的跨窗口发送消息,就是通过在别的窗口操作本窗口发送消息,然后本窗口再自己接收的方式实现。

4、iframe 页面与 parent 页面 焦点 问题

我们知道,初次进入 parent 页面,document.activeElement 是 document.body。

document.activeElement: 文档中当前获得焦点的元素,是一个只读属性。

现在我们有需求:通过绑定 window onkeydown 快捷键能够打开 iframe 呈现其内容,并且在 iframe 内部提供 Close Icon 能够去关闭 iframe。

这时遇到一个问题:

当点击 Close Icon 关闭 iframe 后,再按键盘唤起 iframe 时,发现 parent 页面中绑定的 window onkeydown 事件不触发,且这时候输出 document.activeElement 得到的是 iframe 元素。只有先点击 parent 页面之后才能正常使用键盘事件监听。

解决办法也很简单:在关闭 iframe 时将 document.activeElement 聚焦元素指向 document.parent

但由于 document.activeElement 是一个只读属性,我们需要借助 document.body.focus() 来完成。

注意:body 元素必须 可接受焦点(即存在 tabindex 属性)。

// parent 监听到 iframe close 回调
const handleClose = () => {
  ...
  // 设置焦点
  if (!document.body.hasAttribute("tabindex")) {
    document.body.setAttribute("tabindex", 0);
    document.body.focus();
    document.body.removeAttribute("tabindex");
  } else {
    document.body.focus();
  }
};

持续更新中...

参考