亲爱的观众老婆们大家好,欢迎大家欢迎来到一可的昆特牌馆小白的地盘(看在我为你宣传的份上,跪求别告我侵权台词啊T.T)。作为某浏览器的小前端,最近接到一个需求:某境外视频网站在我们浏览器打开后是白屏,希望帮忙定位原因并提出解决方案。
经过Fiddler各种抓包替换文件后,定位出问题是自家浏览器严格模式上的bug。然而浏览器发版太久,依靠商务推动对方修改代码也不是一时半刻就能解决问题,于是皮球又踢回给可怜的小前端。既然浏览器是自家的,那可不可以通过在对方页面中使用注入功能解决这问题呢?
示例代码大致如下:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script src="1.js"></script>
<script src="2.js"></script>
<script src="error.js"></script>
</body>
</html>
根据这样的情况,可以想到的解决方案是在页面替换error.js
为我们上传到CDN的ok.js
,这样页面就没有问题啦。然而注入不是银弹,客户端提供的注入功能是有限制的,只能在<head>
中注入JS文件,无法直接修改DOM内容,这就意味着我们无法直接修改src
指向,只通过在<head>
文件中注入JS,真的能完成这个需求吗?至此似乎陷入了僵局。然而,我们一起回顾一下浏览器的渲染过程,在不添加defer
与async
属性的前提下,浏览器碰到<script>
标签是会停下来等待JS文件下载下来,执行完毕后再继续解析渲染其他内容。如果我们有能力监测到DOM节点的添加并能对其进行干预,之前的需求就有解决方案了。幸运的是,我们拥有这样的能力,它就是MutationObserver
。
MutationObserver 简介
MutationObserver给开发者们提供了一种能在某个范围内的DOM树发生变化时作出适当反应的能力.该API设计用来替换掉在DOM3事件规范中引入的Mutation事件.
其实MDN对MutationObserver
的介绍是十分清晰的,但其中有些细节可能不太好理解。初看之下感觉这个API其实挺复杂的,但别急,先记住两个东东就好。首先是通过一个构造函数MutationObserver()
创建一个观察实例,构造函数接受一个函数作为唯一参数,即const observer = MutationObserver(function(){})
。其次要一个配置对象MutationObserverInit
,即const option = {}
,别担心是什么高大上的东西,其实就是一个配置。之后调用实例,传入参数观察对象:observer.observe(targetElement, option)
就好了。看吧看吧,是不是挺简单的?
不过,对于前端来说,不谈兼容性就使用API,那是耍流氓,因此我们需要瞧瞧MutationObserver
在浏览器的的兼容性如何:
看到的基本都是原谅绿的,尤其移动端,可以放心使用。要注意的是版本比较旧浏览器是需要前缀的,即WebKitMutationObserver
或MozMutationObserver
。由于自家浏览器是支持这个API的,我就不添加前缀了。不然可以参考MDN的的例子:
var observer = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
这样可以确保兼容性。
MutationObserverInit 对象简析
原本应该是先介绍构造函数及其传入的函数的,但想了想,发现先介绍这个配置对象会比较好理解。如之前所说,MutationObserver
是可以监测DOM变化的,然而大家想一下,DOM变化其实有挺多类型的,比如改下文本,改下属性等等。如果这个API是监测全部变化的话,是不是有点浪费浏览器资源?监测范围可控,才是最佳实践,因而有MutationObserverInit
这个配置对象。
MutationRecord
可配置的挺多,初看还真会懵逼,但好好归纳之后,发现其实是十分清晰的。我们从主到次过一次。首先是监测的类型,分三类,分别是:
childList
,代表观察目标节点子节点的变化;attributes
,代表观察目标节点属性的变化;characterData
,代表观察目标节点是文本节点、注释节点时的变化。
值得一提的是characterData
,感觉用得不多,必须将观察的目标节点指定为文本、注释等节点时才会生效(补充一下:业务可能用到不多,但可能会有极为精彩的hack!如Vue源码中的用法。)。
举个栗子:
const p = document.querySelector('p');
const childListBtn = document.querySelector('.childList');
const characterDataBtn = document.querySelector('.characterData');
childListBtn.addEventListener('click', function() {
p.innerHTML = 'childList';
})
characterDataBtn.addEventListener('click', function() {
p.firstChild.data = 'characterDataBtn';
})
//...观察相关代码
如果你观察的目标节点是p
标签,它的文本发生变化时,触发的是childList
类型的变化,即触发的是子节点变化。只有将观察的目标节点设置为p.firstChild
,且改变了p.firstChild
里面的属性时,才会触发characterData
类型的变化。希望大家注意这个细节。
分完类型之后,剩下的几个选项就是增强上面三个类型了:
subtree
,代表不但观察子节点,还会观察目标节点所有的子孙节点,三种类型均可增强;attributeOldValue
,代表是否需要将发生变化的属性节点之前的属性值记录下来,这是用于增强attributes
的;attributeFilter
,唯一一个值不是布尔值而是数组的选项,如果设置了该值,则只有该数组中包含的属性名发生变化时才会被观察到,也是增强attributes
的;characterDataOldValue
,代表是否需要将发生变化的characterData节点之前的文本内容记录下来,这是用于增强attributes
的。
值得注意的是,无论是三个大类还是增强的选项,默认值都是undefined
,而增强选项必须在对应的选项设置为true
的情况下才可使用,不然浏览器会报错。
MutationObserverInit
这个配置对象就介绍完了,之后我们来看看MutationObserver
的构造函数及其关键的回调函数。
构造函数 MutationObserver
其实我说MutationObserver
的构造函数重要,是骗你的。重要的是构造函数是理解它接受的那个参数,即回调函数。回调函数执行时会被传入两个参数。先说最好理解的第二个参数,传入的是new
出来的那个实例。第一个参数则是一个数组,由数组中的每一项都是一个对象:MutationRecord
,接下来我们将好好看一看这个对象。
MutationRecord
对象包含很多属性,主要是用于展示DOM节点发生了什么变化:
这里我偷个懒,直接上了MDN文档上的说明。文档写的很详细,但这里说一些需要注意的细节。addedNodes
和removedNodes
的类型是NodeList,也就意味着它不是一个真正的数组,使用数组方法时需要谨慎。另外,更重要的是理解为什么回调函数的第一个参数是数组。按照逻辑,某一个节点产生变化,一个MutationRecord
对象应该是足够描述它的变化的。我认为,这句话在MutationObserver
中既对也不对,接下来的理解只是我个人的见解,还请dalao们指教。
MutationObserver
所做到的,并不是在一个函数执行完后,比较DOM前后的变化,监测到变化后给回调函数传入相关的变化,而是记录DOM的变化“记录”。这看起来很绕,但请看如下例子:
<p>1111</p>
<button>click</button>
<script>
function obFn(changeArr) {
console.log(changeArr.length)
}
const observer = new MutationObserver(obFn);
const option = {
childList: true,
subtree: true,
};
const p = document.querySelector('p');
observer.observe(p, option);
const btn = document.querySelector('button');
btn.addEventListener('click', function() {
p.innerHTML = '<span>1</span><span>2</span><span>3</span>';
})
</script>
当点击按钮后,控制台会打印出1
。但当btn
的事件绑定改为:
btn.addEventListener('click', function() {
const a = document.createElement('a');
a.innerHTML = '<span>123</span>';
const a1 = a.cloneNode();
const a2 = a.cloneNode();
p.appendChild(a);
p.appendChild(a1);
p.appendChild(a2);
})
点击按钮,控制台打印出3
!第一例中,对<p>
的操作记录只有一次:p.innerHTML = '<span>1</span><span>2</span><span>3</span>';
,因而变化数组的长度是1,变化对象MutationRecord
中addedNodes
的长度是3 。第二例中,对<p>
的操作记录有3次,因而变化数组的长度是3,数组每项的变化对象MutationRecord
中addedNodes
的长度是1 。
通过这样的对比,能理解我为何说MutationObserver
是记录DOM的变化“记录”,因而回调函数的第一个参数是一个数组,而不是一个只记录了前后变化的对象!
最后,MutationObserver
的构造函数生成的实例有三个方法:observe
,接受两个参数,第一个是观察DOM节点,第二个是MutationObserverInit
配置对象;disconnect
,停止观察DOM节点;takeRecords
,清空观察者对象的记录队列,并返回里面的内容。
小结
写了半天,感觉好像偏题了,最后附上解决问题写的代码:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script>
function observerFn(changeArr, observer) {
changeArr.forEach((obj) => {
const addNodes = [...obj.addedNodes];
addNodes.forEach((node) => {
if (node.nodeName === 'SCRIPT' && /error/.test(node.src)) {
node.src = 'ok.js';
observer.disconnect();
}
})
})
}
const observer = new MutationObserver(observerFn);
const option = {
childList: true,
subtree: true,
}
observer.observe(document.documentElement, option);
</script>
</head>
<body>
<script src="1.js"></script>
<script src="2.js"></script>
<script src="error.js"></script>
</body>
</html>
上述代码稍微修改一下,其实能阻止某些劫持的。只要不是白名单内的JS文件,全部改变它的src
,并上传到自己的服务器,这样可以记录下来到底是什么劫持了我们的网页,也算是个拓展了思路。除此之外,MutationObserver
可以记录DOM变化,这个能力实在是太强大了,还有数之不尽的应用场景等待着我们研究与开发。
以上就是我对MutationObserver
的一些归纳,希望能帮到大家。不当之处还请不吝赐教!