审核业务的同事需要一款chrome高亮插件来加快审核,由于涉及到敏感的数据,因此不放心第三方的插件,并且调研发现很多chrome商店的高亮插件并不能兼容使用mvvm框架编写的网页,都存在一定的副作,用当被高亮的内容被修改时会造成信息显示错误,于是决定自己开发一个。
Chrome插件
总的来说,Chrome插件的开发完全是前端的技术,跟写一个网页、js类库并没有区别,一个前端技能娴熟的程序员可以在简单阅读文档后迅速进行Chrome插件的开发。
content_script
Chrome插件提供在网页中注入js代码(xss既视感)的能力,这些js代码就是content_script。注入的js代码和网页本身的js代码可以理解为不在同一个作用域,不能直接交互,但是可以访问DOM。高亮就是由content_script操作DOM实现。
popup
pop_up就是点击插件图标时出现的页面,在高亮插件里,这个页面用来输入高亮关键字、控制插件开关。
storage与通信
chrome提供了用于存储和通信的api。
storage分为local, sync, managed 3种类型。local类似于localStorage,sync在登录了google账号的情况下会把数据存放在云端,若未登录则使用local。需要注意2点:1. chrome stroage的操作是异步的。2. 存放的数据自动进行了parse,不像localSrorage存储的是String。
通信的api用于content_script、popup、background之间的通信。
以上就是开发一款chrome高亮插件需要了解的知识点。仅仅使用了chrome插件可以使用的一小部分能力。
高亮算法
在页面展示文本一般有两种途径:
- TextNode
- 利用input的value或者placeholder(百度首页的「百度一下」按钮就是使用input的value做的)
通常来说input标签的属性我们是不能乱动的,所以只需要关注TextNode。
给定一个关键字列表
1 | keyWrods = ['我', '爱', '她'] |
给定一个文本节点及其父节点
1 | <p>其实,我真的爱着她</p> |
比较自然的思路是使用字符串的replace方法进行正则匹配就可以完成高亮了。
所以这里有两个问题:
- 生成正则
- 完成高亮
生成正则
只有一点需要注意,特殊字符需要进行转义,比如”/“、”.”等等
1 | const getRegFromStrList = (function () { |
高亮单个文本节点
把匹配到的字符外面包裹一个wrap元素添加高亮的样式,然后整体替换到这个文本节点就可以了,由于做的是一个chrome插件,所以使用了一个只有chrome支持的实验性api: node.replaceWith,可以非常方便完成节点的替换操作。
1 | let reg = getRegFromStrList(keyWords) |
遍历DOM
遍历DOM,对每一个文本节点使用刚才的高亮算法,就完成了高亮。
有两种思路对DOM进行遍历。
- 遍历document.all
- 遍历DOM树
我采用的是层序遍历DOM的方法。有一点需要注意的是需要忽略掉一些特殊的标签,比如script, style,meta等等。
观察DOM变动
Mutation Observer
Mutation Observer 就是一个观察DOM变动的API,可以观察所有的DOM变动,包括属性,子孙节点,文本等等。
需要注意在每一个event-loop中,所有的DOM变动会进行合并,并不会多次触发,而且这个回调是异步的,并且是在micro-task中。
这一点非常重要,因为每高亮一个节点,就会产生一次DOM变动,如果每一次文本变动都会触发Mutation Observer,就会造成性能问题,而且,由于回调在micro-task中,使用throttle也不能很好的解决,因为通常的throttle是使用setTimeout来实现的。
标记已被高亮的node
在遍历DOM的过程,需要跳过已经被高亮过的节点,因为在遍历DOM进行高亮之后会造成DOM变动,所以在这一个tick执行完之后,会进入micro-task再执行高亮函数,由于匹配到关键字的文本一直存在,又会继续进行高亮,一直无限循环下去。
所以对需要更新高亮算法,把已被高亮的节点做一个标记,遍历DOM的时候遇到标记就跳过。
如何取消高亮
高亮之后,肯定需要能够取消,比如要提供一个开关,控制高亮插件的关闭,比如更新关键字的时候需要把DOM回退。
在进行高亮的时候,使用一个数组把这个textNode存起来,并且在textNode上添加一个responsedNode属性,指向的就是替换掉这个textNode的元素。当需要取消高亮的时候,遍历这个数组,替换回去就可以了。
在MVVM下工作
现在很多的网站是使用MVVM框架写的,比如Vue, React等等,框架会把很多DOM对象和一个javascript对象(virtual dom、数据)对应起来,当数据变化时更新(渲染)页面,比如使用textNode.textNode = newText。那么问题来了。
建立对应关系是在模板解析阶段完成的,这个时候并没有渲染DOM,高亮插件也没有对DOM进行过改变。当网页渲染好,高亮函数进行高亮的时候,我们「替換」掉了textNode,textNode是什么,就是一个对象,而框架保留的是对被我们替换掉的textNode的引用,所以当数据更新后,页面并不会发生变化,这就很尴尬了。不过既然发现了造成问题的原因,那么就好解决了。
观察被替换掉的节点
思路很简单,当被替换掉的文本节点有变动的时候,我们把它换回去就OK了。
我首先尝试的是使用Object.defineProperty定义setter来解决,不过显然是我天真,textNode的textContent属性是non-configurable的,无法重新定义。
so,还是拿起了之前用过的Mutation Observer武器,问题解决。当对一个文本节点进行高亮的时候,使用Mutation Observer对它进行观察,当发生变动时把原文本节点替换回去,这一次的替换又会造成一次DOM变动触发Mutation Observer,对新的文本进行高亮。
观察被替换掉的节点父节点
现在还有一个问题,一个节点本身被移除这个操作是没法通过观察它自己来监测到的,只能通过观察它的父节点来完成,所以还需要观察被替换掉的节点父节点,当观察到这个节点被移除是需要同步移除高亮节点。
总结
通过这个项目还是学到了不少东西,知识水平得到了提高。
- Chrome插件的开发
- Mutation Observer
- mvvm的渲染流程
- 正则处理
- 熟悉了更多的DOM API