前言
我的前言都是良心话,还是姑且看一下吧:
别人一看这个标题,心想,“怎么又是一个老到掉牙的需求,网上一搜一大堆解决方案啦!”。没错!这个需求实在老土得不能再老土了,我真不想写这样一种需求的文章,无奈!无奈!
现实的情况是我想这么旧的需求网上资料一大把一大把,虽然我知道网上资料可能有坑,但是我总不信找不到一篇好的全面的资料的。然而现实又是一次啪啪的打脸,我是没找到,而且很多资料都是一个拷贝一个,质量参差不齐,想必很多找资料的人也深有体会
为了让别人不再走我的老路,特此写了此篇文章和大家分享
我不能说我写的文章质量杠杠滴。但是我会在这里,客观地指出我方案的缺点,不忽悠别人。
写该文章的目的只有两个:
- 让缺乏这方面经验的人能够信手拈来一个较为全面的方案,对自己对公司相对负责,别qa提很多bug啦(我也是这么过来,纯粹想帮助小白)
- 让更有能力的人,补充完善我的方案,或者借鉴我的经验,造出更强更全面的方案,当然,我也希望能让我学习一下就最好了。
目录
需求
还是说一下这到底是个什么需求吧。想必大家都试过在一个网页上,按下“ctrl + F”,然后输入关键词来找到页面上匹配的。
没错,就是这么一种类似的简单的需求。但是这么一个简单的需求,却暗藏杀机。这种需求(非就是这种形式)用文字明确描述一下:
页面上有一个按钮,或者一个输入框,进行操作时,针对某些关键词(任意字符串都可以,除换行符),在页面上进行高亮显示,注意此页面内容是有任何可能的网页
描述很抽象?那我就干脆定一个明确的需求:
实现一个插件,在任何别人的网页上高亮想要的关键词。
这里不说实现插件的本身,只描述高亮的方案。
接下来我将循序渐进地从一个个简单的需求到复杂的需求,告诉你这里边到底需要考虑什么。
一个最简单的方案
第一反应,想必大家都觉得用字符串来处理了吧,在字符串里找到匹配的文字,然后用一个html元素包围着,加上类名,css高亮!对吧,一切都感觉如此自然顺利~
我先不说这方案的鸡肋之处,光说落实到实际处理的时候,需要做些什么。
超简单处理
// js
var keyword = '关键词1'; // 假设这里的关键词为“关键词1”
var bodyContent = document.body.innerHTMl; // 获取页面内容
var contentArray = bodyContent.split(keyword);
document.body.innerHTMl = contentArray.join('<span>' + keyword + '</span>');
// css
.highlight {
background: yellow;
color: red;
}
简单处理二
这里相对上面还没那么简单,至于为啥我说这个方案的原因是,在后面讲的复杂方案里,需要用到这些知识。
关键词的处理
上面说需求的时候讲过,是针对任意关键词(除换行符)进行的高亮,如果更简单点,说只针对英文或中文,那么可以直接匹配了,如str.match('keyword');
。但是我们是要做一个通用的功能的话,还是要特别针对一些转义字符做处理的,不然如关键词为?keyword'
,用'?keyword'.match('?keyword');
,会报错。
我找了各种特殊字符进行了测试,最终形成了以下方法针对各种特殊字符进行了处理。
// string为原本要进行匹配的关键词
// 结果transformString为进行处理后的要用来进行匹配的关键词
var transformString = string.replace(/[.[*?+^$|()/]|\]|\\/g, '\\$&');
看不懂?想深究,可以看一下这边文章: 这是一篇男女老少入门精通咸宜的正则笔记
反正这里的意思就是把各种转义字符变成普通字符,以便可以匹配出来。
匹配高亮
// js部分
var bodyContent = document.body.innerHTMl; // 获取页面内容
var pattern = new RegExp(transformString, 'g'); // 生成正则表达式
// 匹配关键词并替换
document.body.innerHTMl = bodyContent.replace(pattern, '<span class="highlight">$&</span>');
// css
.highlight {
background: yellow;
color: red;
}
缺点
把页面的内容当成一个字符串来处理,存在很多预想不到的情况。
- script标签内有匹配文本,添加高亮html元素后,导致脚本报错。
- 标签属性(特别是自定义属性,如dats-*)存在匹配文本,添加高亮后,破坏原有功能
- 刚好匹配文本跟某内联样式文本匹配上,如
<div style="width: 300px;"></div>
,关键词刚好为width
,这时候就尴尬了,替换结果为<div style="<span class="highlight">width</span>: 300px;"><div
。这样就破坏了原本的样式了。 - 还有一种情况,如
<div>右</div>
,关键词为>右
,这时候替换结果为<div<span class="highlight">>右</span></div>
,同样破坏了结构。 - 以及还有很多很多情况,以上仅是我罗列的一些,未知的情况实在太多了
利用DOM节点高亮(基础版)
既然字符串的方法太多弊端了,那只能舍弃掉了,另寻他法。
这节内容就考大家的基础知识扎不扎实了
页面的内容有一个DOM树构成,其中有一种节点叫文本节点,就是我们页面上所能看到的文字(大部分,图片等除外),那么我们只要在这些文本节点里找到是否有我们匹配的关键词,匹配上的就对该文本节点做改造就好了。
封装一个函数做上述处理(注释中一个个解释), ①内容为上述讲过:
// ①
// string为原本要进行匹配的关键词
// 结果transformString为进行处理后的要用来进行匹配的关键词
var transformString = string.replace(/[.[*?+^$|()/]|\]|\\/g, '\\$&');
var pattern = new RegExp(transformString, 'i'); // 这里不区分大小写
/**
* ② 高亮关键字
* @param node - 节点
* @param pattern - 用于匹配的正则表达式,就是把上面的pattern传进来
*/
function highlightKeyword(node, pattern) {
// nodeType等于3表示是文本节点
if (node.nodeType === 3) {
// node.data为文本节点的文本内容
var matchResult = node.data.match(pattern);
// 有匹配上的话
if (matchResult) {
// 创建一个span节点,用来包裹住匹配到的关键词内容
var highlightEl = document.createElement('span');
// 不用类名来控制高亮,用自定义属性data-*来标识,
// 比用类名更减少概率与原本内容重名,避免样式覆盖
highlightEl.dataset.highlight = 'yes';
// splitText相关知识下面再说,可以先去理解了再回来这里看
// 从匹配到的初始位置开始截断到原本节点末尾,产生新的文本节点
var matchNode = node.splitText(matchResult.index);
// 从新的文本节点中再次截断,按照匹配到的关键词的长度开始截断,
// 此时0-length之间的文本作为matchNode的文本内容
matchNode.splitText(matchResult[0].length);
// 对matchNode这个文本节点的内容(即匹配到的关键词内容)创建出一个新的文本节点出来
var highlightTextNode = document.createTextNode(matchNode.data);
// 插入到创建的span节点中
highlightEl.appendChild(highlightTextNode);
// 把原本matchNode这个节点替换成用于标记高亮的span节点
matchNode.parentNode.replaceChild(highlightEl, matchNode);
}
}
// 如果是元素节点 且 不是script、style元素 且 不是已经标记过高亮的元素
// 至于要区分什么元素里的内容不是你想要高亮的,可自己补充,这里的script和style是最基础的了
// 不是已经标记过高亮的元素作为条件之一的理由是,避免进入死循环,一直往里套span标签
else if ((node.nodeType === 1) && !(/script|style/.test(node.tagName.toLowerCase())) && (node.dataset.highlight !== 'yes')) {
// 遍历该节点的所有子孙节点,找出文本节点进行高亮标记
var childNodes = node.childNodes;
for (var i = 0; i < childNodes.length; i++) {
highlightKeyword(childNodes[i], pattern);
}
}
}
注意这里的pattern参数,就是上述关键词处理后的正则表达式
/** css高亮样式设置 **/
[data-highlight=yes] {
display: inline-block;
background: #32a1ff;
}
这里用的是属性选择器
splitText
这个方法针对文本节点使用,IE8+都能使用。它的作用是能把文本节点按照指定位置分离出另一个文本节点,作为其兄弟节点,即它们是同父同母哦~ 看图理解更清楚:
虽然这个div原本是只有一个文本节点,后来变成了两个,但是对实际页面效果,看起来还是一样的。
语法
/**
* @param offset 指定的偏移量,值为从0开始到字符串长度的整数
* @returns replacementNode - 截出的新文本节点,不含offset处文本
*/
replacementNode = textnode.splitText(offset)
例子
<body>
<p