前言
在原生 JS、JQuery 时代,页面中几乎所有的交互都需要使用 DOM 完成操作。随着以 数据驱动视图更新 为核心的 框架(React、Vue 等) 的出现,使页面中数据更新视图变得简单。
尽管框架语法让视图更新变得简单,但这并不意味着 DOM 相关操作彻底被弃用。很多场景下实现一些交互功能离不开原生 DOM 相关语法使用。
本篇,是记录笔者在工作中不同场景下使用 JS DOM 操作的一个总结,或许这本 "武功秘籍" 会对屏幕前的你有所帮助。
每个模块能够用在哪些场景 说一下。
一、DOM 基础操作 - 增删改查
除了基础 JS 语法语句外,对页面中 DOM 元素进行 增删改查 也是作为 JS 初学者必备技能。下面我们列举场景的 DOM 操作满足日常工作使用。
1、查找 DOM
JS 提供查找 DOM 的方式很多,比如:
根据元素的 id 属性查找:
js
<div id="container"></div>
const ele = document.getElementById('container');
// 小技巧:由于 id 值要求唯一,通过 window[id] 也可以访问到 DOM 元素
window.container
根据元素的 class 属性查找,注意它返回的是一个数组,是匹配到的所有 DOM:
js
const ele = document.getElementsByClassName('container')[0];
通用查找元素,当存在多个匹配元素时,只返回第一个,它支持的查找规则有很多,比如:
js
// 根据 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 表单集合,没有时返回一个空数组:
js
const forms = document.forms;
获取 head、body 元素,可直接从 document 中获取:
js
const head = document.head;
const body = document.body;
获取文档中当前处于 聚焦 的元素(如:input 输入框)
js
const activeElement = document.activeElement;
注意,getElementById 和 forms 只能在 document 上使用,其他方式支持在任意 DOM 元素中使用。
2、创建 DOM
document
作为页面中最顶级的元素节点(文档),提供了创建节点的方式:
创建一个 DOM 元素:
js
const box = document.createElement('div');
创建一个 文本 节点:
js
const text = document.createTextNode('文本内容');
克隆一个 节点:
js
// 语法:
nodeEle.cloneNode(boolean); // boolean 值为 false 标识只克隆该元素,为 true 会克隆所有子元素。
const cloneBody = document.body.cloneNode(true);
创建一个文档片段(内存变量):
js
const fragment = document.createDocumentFragment();
由于文档片段只是创建在内存中,并不在 DOM 树中,通常会使用它来进行批量操作元素,避免重复引起页面回流,从而提升性能。如下示例:
js
<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
比较常用的是,在容器末尾添加一个元素:
js
// 语法:
parent.appendChild(child);
const container = document.querySelector('.container');
const child = document.createElement('span');
container.appendChild(child);
如果我们要在 容器内 某个元素之前插入新节点,JS 提供了方法:
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
(面试题):
js
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'));
将容器内 指定子节点 替换为新节点:
js
// 语法
parent.replaceChild(newChild, targetChild);
4、删除 DOM
将 DOM 元素从容器内移除:
js
// 语法
parent.removeChild(targetChild);
const container = document.querySelector('.container');
container.removeChild(container.querySelector('.child'));
5、修改 DOM
修改 DOM 元素内容为一个子元素结构:
js
const container = document.querySelector('.container');
container.innerHTML = `<span>child</span>`;
修改 DOM 元素内容为一个文本内容:
js
const container = document.querySelector('.container');
container.textContent = "容器";
二、DOM 节点关系
除了通过 id、class 查找元素外,借助 DOM 树元素之间的关系也是一种获取元素的方式。
获取 父 元素/节点:
js
ele.parentNode:父节点,节点包括 Element 和 Document;
ele.parentElement:父元素,与 parentNode 区别是,其父节点必须是一个 Element 元素。
获取 子 元素/节点 集合:
js
ele.children: 返回子元素集合,只返回元素节点;
ele.childNodes:返回 Node 节点列表,可能包含文本节点(换行也会转为文本节点)、注释节点。
获取 关系 节点:
js
ele.firstChild:返回第一个子节点(元素、文本、注释),不存在返回 null;
ele.lastChild:返回最后一个子节点(元素、文本、注释),不存在返回 null;
ele.firstElementChild:返回第一个元素节点;
ele.lastElementChild:返回最后一个元素节点;
ele.previousSibling:返回节点的前一个节点(元素、文本、注释);
ele.nextSibling:返回节点的后一个节点(元素、文本、注释);
ele.previousElementSibling:返回节点的前一个元素节点;
ele.nextElementSiblng:返回节点的后一个元素节点。
三、DOM 属性操作
一个 DOM 元素除了可以设置 id、class 属性外,还可以 自定义属性(通常以 data- 规范开头) 来实现绑定数据。
为元素设置属性:
js
// 语法:
element.setAttribute(name, value);
const container = document.querySelector('.container');
container.setAttribute("id", "container");
// 自定义属性
container.setAttribute("data-index", "1");
获取元素指定属性:
js
// 语法:
element.getAttribute(name);
const container = document.querySelector('.container');
container.getAttribute("class");
判断元素是否存在指定属性,存在返回 true,不存在时返回 false:
js
// 语法:
element.hasAttribute(name);
const container = document.querySelector('.container');
container.hasAttribute('class');
获取元素的所有属性名称,如:id、class data-index 等
js
// 语法:
element.attributes
获取元素所有以 data- 开头的自定义属性及属性值,返回值是一个对象,
jd
// 语法:
element.dataset
四、Node 节点类型
我们知道,DOM 元素只是 DOM Node 节点中的一类,在 DOM 中除了 元素 外,还包含一些其他类型节点 Nodes,如:文档节点,文本节点、注释节点 等。
那么如何判断一个 Node 节点属于什么类型呢,可通过节点的 nodeType
来获取节点类型:
js
<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);
});
输出结果如下:
js
3
1
3
8
3
nodeType
是一个从 1 开始的 number 值,每个数值代表了不同类型的节点。上例中 换行 和 文本 属于文本节点(类型 为 3),DOM 元素属于元素节点(类型为 1),注释属于注释节点(类型为 8)。
常见的 Nodes 节点类型如下:
js
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_NODE 指 document
10 DOCUMENT_TYPE_NODE <!DOCTYPE>
11 DOCUMENT_FRAGMENT_NODE 文档碎片节点
12 NOTATION_NODE DTD中声明的符号节点
五、更详细的 DOM 事件
在网页中做任何交互,都离不开 事件 交互,常见的事件行为:鼠标事件、键盘事件、输入事件、滚动事件 等。
大多数事件交互都可以作用在 window、document 以及 DOM 元素 上。
1、绑定事件
在 DOM 编程中,可以使用两种方式来绑定事件:onEvent
和 addEventListener
。
onEvent
使用很简单,可直接在 HTML 元素上使用 onEvent
属性,或者给 DOM 引用设置 onEvent
来指定事件处理程序。
js
<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
方法来绑定事件处理程序。
js
// 语法:
el.addEventListener(type, listener[, useCapture]);
el
: 事件绑定的目标对象,比如 window、document 以及 DOM 标签元素;type
: 事件类型,如:click、mousedown,注意这里的事件类型不用加前缀on
;listener
: 事件处理函数,函数接收event
事件对象作为参数;useCapture
: 是否为捕获阶段,默认 false 冒泡,值为 true 时是捕获;
此外,useCapture
第三参数可以为一个对象:
js
el.addEventListener(type, listener, {
capture: false, // 设置 冒泡 或者 捕获
once: false, // 是否设置单次监听, 如果为 true 会在调用后自动销毁listener
passive: false // 是否让 阻止事件默认行为(preventDefault()) 失效,如果为 true, 意味着 listener 将无法通过 preventDefault 阻止事件默认行为
})
扩展知识 :listener
的另一种形式 - 对象。
通常 listener 是一个函数,但也可以传递一个对象(或者实例对象,如:Sortable),当传入一个对象时,要求这个对象必须提供 handleEvent
方法,所有事件的触发都会进入此方法。
这样使用的好处之一是:通过 handleEvent
方法来拿到所在对象,能够使用对象上的信息:
js
const obj = {
name: 'foo',
handleEvent: function () {
alert('click name=' + this.name);
}
};
document.body.addEventListener('click', obj, false);
其次,将不同事件放在一起,让程序更加内聚:
js
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 可以使用捕获方式进行传播。
从下面这个示例理解一下两者:
html
<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
html
<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
html
<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、拖拽上传图片原理
通常涉及文件上传的需求,除了支持点击选择本地文件外,通常还会支持能够将图片拖动到区域内进行上传。
这就需要借助 HTML5 拖拽特性 drag/drop
相关事件,更详细的使用感兴趣可以查看 React 中使用拖拽。
假设我们现在有一个拖拽区域:
html
<div id="container"></div>
现在我们希望容器能够支持被拖放图片并拿到 Files 信息,需要使用 ondragover
和 ondrop
来实现(要阻止默认行为)
js
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)。
js
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 等属性。
js
// 语法:
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.offsetParent
和 ele.offsetLeft
可以轻松实现元素与父元素左侧的距离,顶部距离同理。
js
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、五种获取元素宽高的方式
js
<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
举例:
js
const container = document.querySelector('#container');
container.addEventListener('mousemove', event => {
event.stopPropagation();
console.log("event: ", event);
});
1、clientX
:鼠标相对于浏览器有效区域左上角 x 轴的坐标,不随滚动条滚动而改变;
2、clientY
:鼠标相对于浏览有效区域左上角 y 轴的坐标,不随滚动条滚动而改变;
3、pageX
:鼠标相对于浏览器有效区域左上角 x 轴的坐标,随滚动条滚动而改变;
4、pageY
:鼠标相对于浏览有效区域左上角 y 轴的坐标,随滚动条滚动而改变;
5、event.offsetX
:相对于事件源 (event.target 目标元素) 左上角 水平 偏移;
6、event.offsetY
:相对于事件源 (event.target 目标元素) 左上角 垂直 偏移;
7、screenX
:鼠标相对于显示器屏幕左上角 x 轴坐标;
8、screenY
:鼠标相对于显示器屏幕左上角 y 轴坐标;
9、event.layerX
:相对于 offsetParent 左上角的 水平 偏移;
10、event.layerY
:相对于 offsetParent 左上角的 水平 偏移。
八、滚动到页面顶部、底部
相信大家在实现一个数据列表时都会遇到这样一个交互:当滚动处于列表底部时,改变顶部固定的筛选项重新获取数据,我们期望列表滚动位置能够回到顶部。
下面我们来聊一聊实现 页面滚动 的几种方式。
1、锚点方式:(CSS 方式)
html
<div id="topAnchor"></div>
<a href="#topAnchor">回到顶部</a>
2、scrollTop:是元素上一个可读写的属性,通过设置为 0 回到滚动容器顶部
js
document.body.scrollTop = document.documentElement.scrollTop = 0;
3、scrollTo(x, y):滚动到当前window中的指定位置,设置scrollTo(0, 0) 可以实现回到顶部的效果:
js
window.scrollTo(0, 0);
4、scrollIntoView 方法用于将滚动条滚动到指定元素的位置,可用于代替 a 标签的 href 属性来实现锚点跳转:(适用于元素平滑滚动)
js
document.body.scrollIntoView(true);
document.getElementById('app').scrollIntoView(true); // 滚动到某个锚点元素
document.getElementById('root').scrollIntoView({ behavior: "smooth" }); // 过度效果
5、实现一个 平滑滚动:(具有滚动效果)
js
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、平滑滚动 的其他方式:
html
// 方式一:
window.scrollTo({ top: element.offsetTop - 50, behavior: 'smooth' });
// 方式二:
body: { scroll-behavior: smooth; }
九、聊一聊 iframe 元素
iframe
是一个比较强大的标签元素,在日常业务开发可能很少用到,基于它能够设置 src 来渲染 HTML 页面的能力,通常用来接入第三方网站到本网站。
下面我们来聊聊在使用 iframe 时的一些使用事项。
1、判断一个页面是否运行在 iframe 内
有时我们一个网站能够 独立 运行在浏览器 Tab 页,也能够通过 iframe 被嵌入在其他网站内。而判断是否运行在 iframe 内的方式可以是:
- 通过
window.self
和window.top
判断:
window.self 指向当前 window,window.top 返回最顶层窗口的引用,如果是在 iframe 下,window.top 将指向外部引用 iframe 的窗口(父页面)。
js
const isRunInIframe = window.self !== window.top;
- 通过
window.parent
属性:
window.parent 属性返回当前窗口的父窗口。如果页面在 iframe 中运行,window.parent 将返回父窗口的引用,否则返回当前窗口的引用。
js
const isRunInIframe = window.parent !== window;
- 通过
window.frameElement
属性:
如果网站是在 iframe 内,这个值将返回这个 iframe 元素,否则返回 null。
js
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 加载完成后操作:
js
// 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 本地服务)。
- 父页面 向 子页面 发送消息:(在父窗口中操作子窗口发消息,然后让子窗口接收自己刚才发的消息。)
html
// 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);
});
- 子页面 向 父页面 发送消息:(在子窗口中操作父窗口发消息,然后让父窗口接收自己刚才发的消息)
js
// iframePage.html
window.parent.postMessage("iframe 页面发送第一条消息.", "父页面的 origin 或者使用 *");
// index.html
window.addEventListener("message", event => {
// event.origin 可用于判断要接收哪个网站发送过来的消息。
console.log("index message event: ", event.data);
});
所谓的跨窗口发送消息,就是通过在别的窗口操作本窗口发送消息,然后本窗口再自己接收的方式实现。