前言
在原生 JS、JQuery 时代,页面中几乎所有的交互都需要使用 DOM 完成操作。随着以 数据驱动视图更新 为核心的 框架(React、Vue 等) 的出现,使页面中数据更新视图变得简单。
尽管框架语法让视图更新变得简单,但这并不意味着 DOM 相关操作彻底被弃用。很多场景下实现一些交互功能离不开原生 DOM 相关语法使用。
本篇,是记录笔者在工作中不同场景下使用 JS DOM 操作的一个总结,或许这本 "武功秘籍" 会对屏幕前的你有所帮助。
内容大纲:
-
- DOM 基础操作 - 增删改查
-
- DOM 节点关系
-
- DOM 属性操作
-
- Node 节点类型
-
- 更详细的 DOM 事件
-
- 元素的尺寸及位置信息
-
- 鼠标事件的坐标信息
-
- JS 控制容器滚动位置
-
<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:父节点,节点包括 Element 和 Document;
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 Attrs 操作
一个 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
四、DOM nodeType
我们知道,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_NODE 指 document
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 编程中,可以使用两种方式来绑定事件:onEvent 和 addEventListener。
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、拖拽上传图片原理
通常涉及文件上传的需求,除了支持点击选择本地文件外,通常还会支持能够将图片拖动到区域内进行上传。
这就需要借助 HTML5 拖拽特性 drag/drop 相关事件,更详细的使用感兴趣可以查看 React 中使用拖拽。
假设我们现在有一个拖拽区域:
<div id="container"></div>
现在我们希望容器能够支持被拖放图片并拿到 Files 信息,需要使用 ondragover 和 ondrop 来实现(要阻止默认行为)
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、鼠标点击事件与聚焦事件的执行顺序
以可聚焦元素 input 为例:
<input
type="text"
defaultValue="input 元素"
onMouseDown={() => {
console.log("mousedown");
}}
onMouseUp={() => {
console.log("mouseup");
}}
onClick={() => {
console.log("click");
}}
onFocus={() => {
console.log("focus");
}}
/>
点击后,执行顺序为:
mousedown
focus
mouseup
click
onmousedown:当鼠标指针移动到元素上方,并按下鼠标按键时触发。这是鼠标操作事件序列中的第一个事件,标志着鼠标与元素交互的开始。onfocus:当元素获得焦点时触发。对于可聚焦的元素(如表单元素、链接等),当用户通过鼠标点击、键盘 Tab 键导航或其他方式使其成为活动焦点时,会触发该事件。onmouseup:当在元素上按下鼠标按键后,释放鼠标按键时触发。它与 onmousedown 成对出现,表示鼠标按键按下的结束。onclick:当用户点击鼠标按钮(通常是左键),并且在元素上按下并释放鼠标按键时触发。它是用户完成一次完整点击操作的确认事件。
7、持续更新中...
六、DOM 的尺寸及位置等信息
1、DOM 自身属性
1、offsetWidth:返回元素的宽度(包括元素宽度、内边距、滚动条 和 边框,不包括外边距);
2、offsetHeight:返回元素的高度(包括元素高度、内边距 滚动条 和 边框,不包括外边距);
3、clientWidth:返回元素的宽度(包括元素宽度、内边距,不包括 滚动条、边框和外边距);
4、clientHeight:返回元素怒的高度(包括元素高度、内边距,不包括 滚动条、边框和外边距);
如果元素 display: none 或者是 inline 行内元素,获取到的 clientWidth 为 0。
(行内元素 尽管有文本撑开了元素,但 width 和 height 依旧为 0)。
5、scrollWidth:返回元素的宽度(包括元素宽度、内边距 和 溢出尺寸,不包括 滚动条、边框和外边距),无溢出情况,与 clientWidth 相同;
6、scrollHeight:返回元素的高度(包括元素高度、内边距 和 溢出尺寸,不包括 滚动条、边框和外边距),无溢出情况,与 clientHeight 相同;
offsetWidth与clientWidth区别:在元素出现滚动条的场景下,前者的数值 包含滚动条宽度,后者的数值 不包含 滚动条宽度;在元素有边框的的场景下,前者的数值 包含边框宽度,后者的数值 不包含边框宽度。
clientWidth与scrollWidth区别:后者的数值 包含内容过多出现溢出的尺寸。
这几个属性常用来判断文本是否出现溢出省略(结合 CSS 溢出省略属性),当出现省略时,鼠标移入文本使用 ToolTip 展示完整内容。
const { offsetWidth, scrollWidth, offsetHeight, scrollHeight } = ele;
// 单行文本省略采用 width 比较
if (offsetWidth !== scrollWidth) console.log("单行文本出现省略")
// 多行文本省略采用 height 比较
if (offsetHeight !== scrollHeight) console.log("多行文本出现省略")
7、style.width: 返回元素宽度(包括元素宽度,不包括内边距、边框、滚动条宽度 和 外边距);
8、style.height: 返回元素高度(包括元素高度,不包括内边距、边框、滚动条宽度 和 外边距);
!!! 注意:仅在为元素设置了内联样式 style.width、style.height 可以拿到(带 px 单位的字符串),否则拿到的是空。
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),对应 offsetWidth;
8、rect.height:元素的高度(包括边框、padding) 对应 offsetHeight。
3、计算 DOM 元素距离指定父元素左边的距离
借助 ele.offsetParent 和 ele.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
5、获取 DOM 的样式信息
有时,我们需要获取 DOM 的样式信息如 font-size 字体大小,可以使用 window.getComputedStyle(element) 来实现。
// 假设你有一个元素的ID是 'myElement'
const element = document.getElementById('myElement');
// 使用 getComputedStyle 获取元素的计算样式
const computedStyle = window.getComputedStyle(element);
// 获取字体大小
const fontSize = computedStyle.getPropertyValue('font-size');
// or
// const fontSize = computedStyle['font-size'];
console.log(fontSize); // 这将输出元素的字体大小,例如 "16px"
如果你想要获取的是字体大小的数值(不包含单位),你可以使用以下代码:
// 将字体大小字符串转换为数值,去除'px'等单位
var fontSizeValue = parseInt(fontSize, 10);
console.log(fontSizeValue); // 这将输出字体大小的数值,例如 16
七、鼠标事件的坐标信息
实现 拖动、移动 交互需要根据鼠标事件中的位置信息实现。鼠标事件有很多,不过每个事件中关于距离的属性含义是一样的,这里以 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 左上角的 水平 偏移。
八、触发容器滚动的方式
相信大家在实现一个数据列表时都会遇到这样一个交互:当滚动处于列表底部时,改变筛选项重新获取数据后,我们期望列表滚动位置能够回到顶部。
下面我们来聊一聊实现 页面滚动 的几种方式。
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" }); // 过度效果
scrollIntoView 传入配置对象使用介绍:
1)behavior:用于指定滚动的行为,默认值为 "auto"。可以设置为 "auto"、"smooth" 或者 "instant"。其中,"auto"表示浏览器自动选择滚动方式,"smooth"表示平滑滚动,"instant"表示瞬间滚动。
2)block:用于指定滚动的垂直方向,默认值为 "start"。可以设置为 "start"、"center"、"end"或者"nearest"。其中,"start" 表示将元素的顶部与可见区域的顶部对齐,"center" 表示将元素的中部与可见区域的中部对齐,"end" 表示将元素的底部与可见区域的底部对齐,"nearest" 表示将元素滚动到可见区域内,如果元素已经在可见区域内,则不进行滚动。
3)inline:用于指定滚动的水平方向,默认值为 "nearest"。可以设置为 "start"、"center"、"end"或者"nearest"。其中,"start" 表示将元素的左边与可见区域的左边对齐,"center" 表示将元素的中部与可见区域的中部对齐,"end" 表示将元素的右边与可见区域的右边对齐,"nearest" 表示将元素滚动到可见区域内,如果元素已经在可见区域内,则不进行滚动。
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/ele.scrollTo({
top: element.offsetTop - 50,
behavior: 'smooth'
});
// 方式二:
body: { scroll-behavior: smooth; }
九、<iframe> 使用总结
iframe 是一个很强大的标签元素,它能够在我们当前网页内开辟一个新的隔离环境(类似沙箱),去运行一个新的子网页。
下面我们来聊聊在使用 iframe 时的一些使用事项。
1、iframe 两种用法
用法一:设置 src 属性。
<iframe> src 属性同 <img> src 指向一个 url 地址,在这里 iframe src 指向的是一个网页地址(可以是相对路径,或是一个 http:// 线上网站地址)。
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iframe src 属性用法</title>
</head>
<body>
<iframe src="./iframe.html"></iframe>
</body>
</html>
// iframe.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iframe page</title>
</head>
<body>
<h1>iframe page.</h1>
</body>
</html>
用法二:设置 srcdoc 属性。
用法一的 src 属性必须要求提供一个可访问的 文件 或是 http 网页 url。但在一些场景下,我们并不想多提供这样一个 iframe.html 文件,只是想将我们本站的一些代码,放入 <iframe> 元素内去独立运行,srcdoc 属性便能派上用场。
简单理解,就是将原本的 iframe.html 文件内容,通过 JS 将其字符串内容作为 iframe srcdoc 属性传递过去。
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iframe srcdoc 属性用法</title>
</head>
<body>
<iframe id="iframe"></iframe>
<script>
const iframe = document.getElementById('iframe');
// 构建 iframe.html 内容放入到 srcdoc 内
iframe.srcdoc = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Hello World</h1>
\<script type="module"\>
console.log('Hello World');
import { initIframe } from './index.js';
initIframe();
\<\/script\>
</body>
</html>
`;
</script>
</body>
</html>
2、判断一个页面是否运行在 iframe 内
有时我们一个网站能够 独立 运行在浏览器 Tab 页,也能够通过 iframe 被嵌入在其他网站内。而判断是否运行在 iframe 内的方式可以是:
- 通过
window.self和window.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,只有在同源下才会有值。
3、操作 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,建议使用下面 通信 方式。
4、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);
});
所谓的跨窗口发送消息,就是通过在别的窗口操作本窗口来发送消息,然后本窗口再自己接收的方式实现。
此外,postMessage 推送消息第一个参数不一定非要是一个 string,同样可以传递对象,一般会这样使用:
window.parent.postMessage({
type: "xxx",
data: xxx
}, "*");
扩展 - 另一种通信方式 dispatchEvent:
与 postMessage() 用法相似,这里以子页面 向 父页面 发送消息为例:
// iframePage.html
window.parent.document.dispatchEvent(new CustomEvent('custom-ready', data));
// index.html
document.addEventListener("custom-ready", data => {
console.log("触发自定义事件 custom-ready", data);
});
5、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();
}
};
同理,当打开 iframe 时,应当让 document.activeElement 聚焦元素指向 iframe 页面,这样才能顺利触发 iframe 页面中的键盘事件。
可以在 message 事件回调中完成:
const handleMessagePublish = (event: any) => {
// event.origin 可用于判断要接收哪个网站发送过来的消息。
if (event.data === "openIframe") {
...
// 设置焦点。iframe 打开后,让页面焦点应用到 iframe 中,能够顺利触发 iframe 的键盘事件。
if (!document.body.hasAttribute("tabindex")) {
document.body.setAttribute("tabindex", "0");
document.body.focus();
document.body.removeAttribute("tabindex");
} else {
document.body.focus();
}
}
};
6、in a frame because it set 'X-Frame-Options' to 'sameorigin'.
如果在嵌入 iframe 网站上时遇到如下报错:Refused to display https://xxx in a frame because it set ‘X-Frame-Options’ to ‘SAMEORIGIN’。
这是因为服务器对网站 iframe 方式访问做了限制。
X-Frame-Options 是一个HTTP标头(header),用来告诉浏览器这个网页是否可以放在iFrame内。
X-Frame-Options: DENY,告诉浏览器不要(DENY)把这个网页放在iFrame内;X-Frame-Options: SAMEORIGIN,告诉浏览器只有同源协议的网站才可访问 iframe 页面;X-Frame-Options: ALLOW-FROM https://www.baidu.com/,告诉浏览器这个网页只能放在www.baidu.com/网页架设的iFrame…X-Frame-Options: AllowAll,第四个例子允许所有站点内嵌。
解决方法可以通过服务器将 X-Frame-Options 限制关闭。