坦白说,直接使用 innerHTML 来实现关键词高亮功能,十个案例里往往有九个会出问题——表单内的数据莫名其妙被清空、已绑定的事件监听器突然失效、甚至连 img 标签上的 onerror 事件也无法正常触发,更极端的情况下还可能直接导致整个 DOM 嵌套结构崩溃。根本原因其实只有一条:你绕过了文本节点,把整个 HTML 字符串当作普通文本来处理了。

因此,正确的实现方式只有一个:只操作那些真正承载“文字”的节点。
为什么不能对 innerHTML 做正则替换
浏览器并不会判断你输入的内容是否为正常文字。如果用户搜索的是 ,或者关键词中包含了 &、" 这类特殊字符,innerHTML 会直接执行或解析成标签,轻则页面样式错乱,重则引发 XSS 安全漏洞。更隐蔽的问题在于:一个带有 onclick 事件的按钮,经过高亮处理后可能直接变成纯文本,点击完全失效;input 输入框中刚键入的内容,也会被一并清空。
不少开发者的第一反应就是下面这种写法:
el.innerHTML = el.innerHTML.replace(/keyword/gi, '$&');
这种写法跳过了 DOM 树的遍历过程,直接将整个 HTML 字符串当作文本处理,完全没有考虑节点语义和当前状态。说白了,就是为了省事,但省出来的全是隐患。
正确做法:只遍历并替换文本节点
接下来是干货。核心逻辑其实非常清晰:递归地访问所有 DOM 节点,遇到 Node.TEXT_NODE 时才进行匹配和包裹操作,其余节点(如元素节点、注释节点)直接跳过。匹配时使用 document.createElement('mark') 创建新节点,然后通过 replaceChild 替换掉原来的文本节点。
- 使用
node.nodeType === Node.TEXT_NODE判断是否为文本节点,这是最基础的一步。 - 中文搜索时需要特别留意。比如你想搜索“窗透”,但“初晓”中的“透”字也可能被错误匹配。解决方法是在正则中加入 Unicode 边界限定,写成
/(\u4e00-\u9fa5|^)\s*keyword\s*(\u4e00-\u9fa5|$)/gi。 - 英文大小写不敏感匹配使用
new RegExp(keyword, 'gi'),但务必记得keyword必须先经过escapeRegExp()进行转义处理,否则用户输入一个.或*就会引发异常。 - 每次执行高亮之前,先克隆当前选区(通过
getSelection().getRangeAt(0).cloneRange()),替换完成后再恢复选区,否则getSelection()可能会返回空值。
highlightText 函数的关键参数与容错点
该函数通常接收三个参数:node(要搜索的根节点)、keyword(用户输入的关键词)、caseSensitive(是否区分大小写)。不过,除了核心逻辑之外,实际编写这个函数时还需要留意几个“坑”:
- 如果
keyword.trim() === '',必须提前 return,否则正则表达式会变成//g,匹配所有空字符串,后果就是页面直接崩溃。 - 目标区域如果包含大量动态内容(比如 React/Vue 渲染出来的区域),不要直接传
document.body,尽量限定为具体的容器,例如document.getElementById('content')。这样可以避免不必要的性能开销。 - 多词搜索(比如“AI 工具”)不要简单地用
split(' '),而应该使用keyword.split(/\s+/).filter(Boolean),然后逐个词调用高亮逻辑。 - 在性能敏感的场景下(比如超过 200 条卡片),必须加入防抖机制。使用
setTimeout配合clearTimeout控制触发频率,不要让用户每敲一个字就重新遍历整棵 DOM 树,等用户敲完回车后再执行搜索即可。
CSS 和语义细节不能省
高亮功能实现后,样式和语义同样不能忽视。 标签天然适合用于高亮展示,比 更具语义化,而且默认带有浅黄色背景。不过,很多父级 CSS 样式会覆盖掉它,因此需要强制声明:
mark { background: #ffeb3b; color: #212121; padding: 0 2px; border-radius: 2px; }还有一个小细节:mark 是内联元素,通常不会影响块级布局;但如果父容器设置了 user-select: none,那么高亮文字就无法被选中和复制——这一点经常被开发者忽略。
说到底,最麻烦的并不是如何标红,而是如何在标红之后,依然让页面保持可交互、可编辑、可选中。所有绕过文本节点的操作,本质上都是在透支 DOM 的稳定性。这一点,值得每一位前端开发者认真思考。
