news 2026/4/23 14:57:58

不修改DOM的高亮黑科技,你可能还不知道

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
不修改DOM的高亮黑科技,你可能还不知道

大家好,我是CC,在这里欢迎大家的到来~

背景

在传统实现文本高亮时通常使用span标签包裹文本,再给 span 标签添加相应高亮背景色。这种方式会修改原本的DOM结构,逻辑复杂,也会频繁导致页面重绘,消耗浏览器性能。而基于HighLightCSS自定义高亮API的这种方式可以实现在渲染层处理文本高亮,既不影响 DOM 树,而且完全独立于文档结构

实现步骤

创建 Range 对象

标识想要高亮的文本范围。

<div id="foo">纯 CSS 实现文本高亮</div> const parentNode = document.getElementById("foo"); const range1 = new Range(); range1.setStart(parentNode, 1); range1.setEnd(parentNode, 2); const range2 = new Range(); range2.setStart(parentNode, 4); range2.setEnd(parentNode, 6);

为 Range 对象添加 Highlight 对象

多个 Range 对象可以与同一个 Highlight 对象关联,这样会以相同方式高亮显示多个文本片段。

const highlight = new Highlight(range1, range2);

当然也可以在某些场景下创建多个 Highlight 对象,比如在使用协作文本编辑器中每个用户使用不同的文本颜色。

const user1Highlight = new Highlight(user1Range1, user1Range2); const user2Highlight = new Highlight(user2Range1, user2Range2, user2Range3);

HighlightRegistry 注册

注册表是一个 Map 对象,通过名称注册高亮。

CSS.highlights.set("user-1-highlight", user1Highlight); CSS.highlights.set("user-2-highlight", user2Highlight);

当然除了注册之外,也支持删除和清除。

CSS.highlights.delete("user-1-highlight"); CSS.highlights.clear();

::highlight()伪元素定义高亮样式

为文本片段添加自定义样式进行高亮。

::highlight(user-1-highlight) { background-color: yellow; color: black; } ::highlight(user-2-highlight) { background-color: black; color: yellow; }

应用场景

这里举例两个CSS自定义高亮API的应用场景。

搜索高亮文本

在多段文本中搜索检索到文本后直接高亮展示,方便用户查找。

import { message } from "antd"; import { useEffect, useRef } from "react"; import "./index.less"; const HighlightText = ({ text, highlightedWords, type = "text" }: { text: string; highlightedWords: string[]; type?: "text" | "html"; }) => { // 将 Node 改为更具体的 HTMLDivElement 以修复 ref 类型错误 const textRef = useRef<HTMLDivElement | null>(null); useEffect(() => { if (!CSS.highlights) { message.warning("CSS Custom Highlight API not supported."); return; } CSS.highlights.clear(); // 支持多个词语:去重、trim,并过滤空字符串 const words = Array.from(new Set(highlightedWords.map((w) => w.trim()).filter(Boolean))); if (!words.length) { return; } if (!textRef.current) return; const treeWalker = document.createTreeWalker(textRef.current, NodeFilter.SHOW_TEXT); const allTextNodes: Text[] = []; let currentNode = treeWalker.nextNode() as Text | null; while (currentNode) { allTextNodes.push(currentNode); currentNode = treeWalker.nextNode() as Text | null; } // 为所有词语在所有文本节点中生成 range const ranges: Range[] = []; for (const el of allTextNodes) { const content = el.textContent || ""; for (const word of words) { let startPos = 0; while (startPos < content.length) { const index = content.indexOf(word, startPos); if (index === -1) break; const range = new Range(); range.setStart(el, index); range.setEnd(el, index + word.length); ranges.push(range); startPos = index + word.length; } } } if (ranges.length) { // 统一用一个高亮名称,样式在 ::highlight(search-results) 中定义 const searchResultsHighlight = new Highlight(...ranges); CSS.highlights.set("search-results", searchResultsHighlight); } }, [highlightedWords, text]); return ( <> {type === "html" ? ( <div ref={textRef} dangerouslySetInnerHTML={ { __html: text } }> </div> ) : ( <div ref={textRef}>{text}</div> )} </> ); }; export default HighlightText;

文本差异对比

对两段文本进行对比时,左侧高亮“不存在于右侧”的文本为绿色(删除),右侧高亮“多于左侧”的文本为红色(新增),可以直观看到差异。

import { message } from "antd"; import { useEffect, useRef } from "react"; import "./index.less"; type DiffProps = { left: string; right: string; type?: "text" | "html"; }; const TextDiff = ({ left, right, type = "text" }: DiffProps) => { const leftRef = useRef<HTMLDivElement | null>(null); const rightRef = useRef<HTMLDivElement | null>(null); useEffect(() => { if (!("highlights" in CSS)) { message.warning("CSS Custom Highlight API not supported."); return; } const collectNodes = (root: HTMLElement | null) => { if (!root) return { nodes: [] as Text[], starts: [] as number[], text: "" }; const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); const nodes: Text[] = []; const starts: number[] = []; let text = ""; let cur = walker.nextNode() as Text | null; while (cur) { nodes.push(cur); starts.push(text.length); text += cur.textContent || ""; cur = walker.nextNode() as Text | null; } return { nodes, starts, text }; }; const { nodes: leftNodes, starts: leftStarts, text: leftText } = collectNodes(leftRef.current); const { nodes: rightNodes, starts: rightStarts, text: rightText } = collectNodes(rightRef.current); const n = leftText.length; const m = rightText.length; const dp: number[][] = Array(n + 1) .fill(0) .map(() => Array(m + 1).fill(0)); for (let i = 1; i <= n; i++) { for (let j = 1; j <= m; j++) { if (leftText[i - 1] === rightText[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1; else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); } } type Op = { t: "equal" | "del" | "add"; ai?: number; bi?: number }; const ops: Op[] = []; let i = n, j = m; while (i > 0 || j > 0) { if (i > 0 && j > 0 && leftText[i - 1] === rightText[j - 1]) { ops.push({ t: "equal", ai: i - 1, bi: j - 1 }); i--; j--; } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) { ops.push({ t: "add", bi: j - 1 }); j--; } else if (i > 0) { ops.push({ t: "del", ai: i - 1 }); i--; } } ops.reverse(); type Span = { start: number; length: number }; const leftDelSpans: Span[] = []; const rightAddSpans: Span[] = []; let posA = 0; let posB = 0; let delRunStart: number | null = null; let delRunLen = 0; let addRunStart: number | null = null; let addRunLen = 0; const flushDel = () => { if (delRunStart !== null && delRunLen > 0) leftDelSpans.push({ start: delRunStart, length: delRunLen }); delRunStart = null; delRunLen = 0; }; const flushAdd = () => { if (addRunStart !== null && addRunLen > 0) rightAddSpans.push({ start: addRunStart, length: addRunLen }); addRunStart = null; addRunLen = 0; }; for (const op of ops) { if (op.t === "equal") { flushDel(); flushAdd(); posA++; posB++; } else if (op.t === "del") { if (delRunStart === null) delRunStart = posA; delRunLen++; flushAdd(); posA++; } else if (op.t === "add") { if (addRunStart === null) addRunStart = posB; addRunLen++; flushDel(); posB++; } } flushDel(); flushAdd(); const locate = (starts: number[], nodes: Text[], pos: number) => { let idx = 0; while (idx < nodes.length) { const nodeLen = (nodes[idx].textContent || "").length; const s = starts[idx]; if (pos < s + nodeLen) return { nodeIndex: idx, offset: pos - s }; idx++; } const lastIdx = nodes.length - 1; return { nodeIndex: Math.max(0, lastIdx), offset: (nodes[lastIdx]?.textContent || "").length }; }; const spansToRanges = (spans: Span[], starts: number[], nodes: Text[]) => { const ranges: Range[] = []; for (const { start, length } of spans) { const end = start + length; const sLoc = locate(starts, nodes, start); const eLoc = locate(starts, nodes, end); const r = new Range(); r.setStart(nodes[sLoc.nodeIndex], sLoc.offset); r.setEnd(nodes[eLoc.nodeIndex], eLoc.offset); ranges.push(r); } return ranges; }; const leftRanges = spansToRanges(leftDelSpans, leftStarts, leftNodes); const rightRanges = spansToRanges(rightAddSpans, rightStarts, rightNodes); if (leftRanges.length) CSS.highlights.set("diff-del-left", new Highlight(...leftRanges)); else CSS.highlights.delete("diff-del-left"); if (rightRanges.length) CSS.highlights.set("diff-add-right", new Highlight(...rightRanges)); else CSS.highlights.delete("diff-add-right"); }, [left, right, type]); return ( <div className="text-diff-container"> {type === "html" ? ( <div className="text-diff-pane" ref={leftRef} dangerouslySetInnerHTML={{ __html: left }} /> ) : ( <div className="text-diff-pane" ref={leftRef}> {left} </div> )} {type === "html" ? ( <div className="text-diff-pane" ref={rightRef} dangerouslySetInnerHTML={{ __html: right }} /> ) : ( <div className="text-diff-pane" ref={rightRef}> {right} </div> )} </div> ); }; export default TextDiff;

总结

目前来看,CSS Custom Highlight API 就是网页文本高亮的“神器”。特别适合那些需要疯狂标记、又不能动原文档结构的应用,比如在线文档、代码编辑器。只要浏览器支持,用它就对了,绝对是未来的趋势。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/23 14:15:53

一次半夜回滚,让我彻底扔掉了本地开发环境

对于一个初创团队而言&#xff0c;最兴奋的时刻&#xff0c;莫过于核心产品上线的那一刻。我至今还记得那个周五晚上&#xff0c;我们准备了一个月的新版本终于要发布了。团队所有人都挤在会议室&#xff0c;盯着部署脚本&#xff0c;等待见证奇迹。然而&#xff0c;奇迹没有发…

作者头像 李华
网站建设 2026/4/23 10:50:05

PHP 8.6 新特性预览,更简洁的语法与更严谨的类型控制

PHP 8.5上线没多久&#xff0c;PHP 8.6的RFC&#xff08;征求意见稿&#xff09;就已逐步落地。我发现PHP正在变得越来越严谨&#xff0c;同时也在努力减少那些机械重复的样板代码。 按照PHP开发组的发布节奏&#xff0c;PHP 8.6 预计将在 2026年11月下旬正式发布。虽然距离正…

作者头像 李华
网站建设 2026/4/23 6:20:37

职场人必备效率工具:2026年四款主流AI生成PPT工具实测报告

AI生成PPT不是一个新鲜事情了&#xff0c;记得这股风潮刚刚吹起来的时候&#xff0c;还有许多免费可以体验的产品或者是加个很便宜的&#xff0c;但是到今年几乎没有了&#xff0c;且都在疯狂的涨价。本期文章就为大家盘点4大免费好用&#xff08;或者有试用机会&#xff09;的…

作者头像 李华