自定义高亮 API(Custom Highlight API)的使用方法

原文链接:Using the Custom Highlight API,2025 年 8 月 7 日,by Chris Coyier。

最近,自定义高亮 API(Custom Highlight API)引起了我的注意------火狐浏览器(Firefox 140 版本,2025 年 6 月)刚刚开始支持该 API,至此所有主流浏览器均已兼容。

借助这个 API,我们可以通过 JavaScript 中的 Range() 类,对获取到的文本应用(部分)样式。虽然我更愿意说它是"对选中的文本"进行操作,因为对于习惯使用 CSS 的人来说,高亮 API 的操作方式比较特别,它并不涉及常规的选择器,

我觉得有必要先简单解释一下核心概念------要是我刚开始研究这个 API 时有人这么告诉我就好了:

  1. 首先需要一个 textNode(文本节点)。(例如 document.querySelector("p").firstChild
  2. 接着需要创建一个 Range()(范围对象),并调用其 setStartsetEnd 方法,通过两个整数参数定义文本范围的起始和结束位置。
  3. 然后调用 CSS.highlights.set() 方法,将这个 Range 对象与一个名称关联起来。
  4. 最后在 CSS 中使用 ::highlight() 伪元素,并传入刚才定义的名称,即可为目标文本应用样式。

假设页面上有一个包含文本的 <p> 标签,那么高亮的完整实现流程如下:

javascript 复制代码
const WORD_TO_HIGHLIGHT = "wisdom"; // 要高亮的单词
const NAME_OF_HIGHLIGHT = "our-highlight"; // 高亮样式的名称

// 1. 获取文本节点(<p> 标签的第一个子节点,即文本内容)
const textNode = document.querySelector("p").firstChild;
// 获取文本节点的内容
const textContent = textNode.textContent;

// 2. 计算要高亮文本的起始和结束索引
const startIndex = textContent.indexOf(WORD_TO_HIGHLIGHT);
const endIndex = startIndex + WORD_TO_HIGHLIGHT.length;

// 3. 创建 Range 对象并定义范围
const range = new Range();
range.setStart(textNode, startIndex); // 从起始索引开始
range.setEnd(textNode, endIndex); // 到结束索引结束

// 4. 创建 Highlight 对象并关联到 CSS 高亮范围
const highlight = new Highlight(range);
CSS.highlights.set(NAME_OF_HIGHLIGHT, highlight);
css 复制代码
::highlight(our-highlight) {
  color: red;
  text-shadow: 0 0 5px white, 0 0 10px red;
}

查看效果,可以看到"wisdom" 这个单词被应用了自定义 CSS 样式,但通过开发者工具会发现它周围并没有我们通常认为"必须存在"的包裹元素(比如 <span>)。

这种方式类似浏览器自身实现的查找逻辑------比如当你使用浏览器内置的"查找"功能时,浏览器就是这样为部分文本应用样式的。

查看 demo

为什么这个 API 有用?

  • 无需修改 DOM 就能定位文本并为其应用样式,这种能力本身就很有价值。DOM API 常被诟病性能不佳,因此避免操作 DOM 可能会带来优势,尤其是在需要频繁处理文本样式的场景中。
  • 要知道,添加或删除 <span> 标签不仅可能"变慢",还会改变 DOM 结构,进而影响其他依赖 DOM 的 CSS 样式和 JavaScript 逻辑。
  • DOM 重量(DOM weight)是网页性能的重要影响因素。DOM 元素过多会导致重排重绘的"成本"剧增,页面体验会随之下降,比如动画卡顿、滚动不流畅等。

举个例子:某 GitHub 的 PR(拉取请求)页面仅修改了 17 个文件,但页面中已经有超过 4500 个 <span> 标签------它们用于代码差异的颜色标注和语法高亮。这个数量已经相当可观了,而且实际场景中可能会更糟。

我相信这个 API 存在的原因还有很多,但以上这些是我立刻能想到的几个核心优势。

进阶用法(搜索功能示例)

new Highlight() 构造函数支持传入多个 Range 对象。这意味着,一个 CSS 中的 ::highlight() 伪元素可以作用于多个文本范围。这个特性非常适合实现页面自带的搜索功能------如果搜索是你正在开发的 Web 应用的核心功能,那么构建自定义搜索界面,无疑比依赖浏览器内置的查找功能更灵活。

下面我们实现一个"用户输入关键词,页面高亮匹配文本"的功能:

首先,创建一个搜索输入框:

html 复制代码
<label>
  搜索下方文本
  <input type="search" value="oven" id="searchTerm"> <!-- 默认值为 "oven" -->
</label>

然后,监听输入框的内容变化事件:

javascript 复制代码
// 监听输入框的文本变化(实时搜索)
window.searchTerm.addEventListener("input", (e) => {
  // 将输入的文本转为小写后传入搜索函数(搜索通常需要忽略大小写)
  doSearch(e.target.value.toLowerCase());
});

注意:我们将输入的文本转为小写后再传入搜索函数,因为忽略大小写的搜索对用户来说通常更实用。

接下来,doSearch 函数会接收搜索关键词,并通过正则表达式在所有文本中匹配:

javascript 复制代码
// 在 doSearch 函数中创建正则表达式(g:全局匹配,i:忽略大小写)
const regex = new RegExp(searchTerm, "gi");

我们需要获取所有匹配文本的起始索引,并将其存入数组。实现代码虽然有点长,但逻辑很清晰:

javascript 复制代码
// 获取所有匹配文本的起始索引(theTextContent 是页面中要搜索的完整文本)
const indexes = [...theTextContent.matchAll(new RegExp(searchTerm, 'gi'))].map(a => a.index);

有了索引数组后,我们就可以循环创建 Range 对象,再将所有 Range 传入 Highlight 构造函数:

javascript 复制代码
const arrayOfRanges = []; // 存储所有 Range 对象的数组

indexes.forEach(matchIndex => {
  // 为每个匹配的文本创建 Range 对象
  const searchRange = new Range();
  // par 是要搜索的文本节点(与前文的 textNode 类似)
  searchRange.setStart(par, matchIndex); 
  searchRange.setEnd(par, matchIndex + searchTerm.length); // 结束位置 = 起始索引 + 关键词长度
  
  arrayOfRanges.push(searchRange); // 将 Range 对象加入数组
})

// 创建 Highlight 对象(传入所有 Range),并关联到 CSS 高亮集合
const ourHighlight = new Highlight(...arrayOfRanges);
CSS.highlights.set("search-results", ourHighlight); // 高亮名称为 "search-results"

把这些代码整合起来,就能实现一个功能完整的实时搜索高亮效果了。

javascript 复制代码
const NAME_OF_HIGHLIGHT = "search-results";

// 监听输入框的文本变化(实时搜索)
window.searchTerm.addEventListener("input", (e) => {
  // 将输入的文本转为小写后传入搜索函数(搜索通常需要忽略大小写)
  doSearch(e.target.value.toLowerCase());
});

function doSearch(searchTerm) {
// 创建正则表达式(g:全局匹配,i:忽略大小写)
const regex = new RegExp(searchTerm, "gi");

 // 获取所有匹配文本的起始索引(theTextContent 是页面中要搜索的完整文本)
const indexes = [...theTextContent.matchAll(new RegExp(searchTerm, 'gi'))].map(a => a.index);

const arrayOfRanges = []; // 存储所有 Range 对象的数组
  
indexes.forEach(matchIndex => {
  // 为每个匹配的文本创建 Range 对象
  const searchRange = new Range();
  // par 是要搜索的文本节点(与前文的 textNode 类似)
  searchRange.setStart(par, matchIndex); 
  searchRange.setEnd(par, matchIndex + searchTerm.length); // 结束位置 = 起始索引 + 关键词长度
  
  arrayOfRanges.push(searchRange); // 将 Range 对象加入数组
})
    
// 创建 Highlight 对象(传入所有 Range),并关联到 CSS 高亮集合
const ourHighlight = new Highlight(...arrayOfRanges);
CSS.highlights.set("search-results", ourHighlight); // 高亮名称为 "search-results"
}

// 获取文本节点(<p> 标签的第一个子节点,即文本内容)
const par = document.querySelector("p").firstChild;
// 获取文本节点的内容
const theTextContent = textNode.textContent.toLowerCase();

doSearch("oven");
css 复制代码
::highlight(search-results) {
  color: orange;
}
html 复制代码
<main>
  <label>
    Search the text below
    <input type="search" value="oven" id="searchTerm">
  </label>
  <p>The pizza oven was very hot and it burned the pizza. But the pizza was delicious anyway, I don't blame the oven.</p>
</main>

<script src="./script.js"></script>

查看 demo

用于语法高亮

自定义高亮 API 非常适合实现代码的语法高亮功能。André Ruffert 已经实践了这个想法:他创建了一个 Web 组件,先通过 Lea Verou 开发的 Prism.js 对代码进行分词(tokenize),但没有像默认的 Prism 那样用 <span> 标签包裹不同语法部分,而是改用这个自定义高亮 API 来应用样式。

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My New Pen</title>
    <link rel="stylesheet" href="./style.css">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/syntax-highlight-element@1/dist/themes/prettylights.min.css">
  </head>
  <body>
    <main>
      <syntax-highlight language="js">let str = "";

for (let i = 0; i < 9; i++) {
  str += i;
}

console.log(str);
// Expected output: "012345678"
</syntax-highlight>
    </main>

    <script type="module" src="./script.js"></script>
  </body>
</html>
javascript 复制代码
import 'syntax-highlight-element';

demo

我觉得这个思路非常棒,但需要注意的是:该 API 只能在客户端(浏览器)使用。对于语法高亮来说,这意味着用户会看到"先显示原始代码,再加载语法高亮"的延迟。

说实话,我更倾向于在可能的情况下使用服务端渲染的语法高亮------如果能从服务端直接返回带有 <span> 标签(用于语法高亮)的代码,并且这种方式不会严重影响性能和可访问性,那么服务端方案可能更优。

另外,我还对"自带语法高亮的字体"很感兴趣------这种字体似乎是字体厂商尚未开发的领域,潜力巨大。

相关推荐
代码AI弗森21 小时前
使用 JavaScript 构建 RAG(检索增强生成)库:原理与实现
开发语言·javascript·ecmascript
Lhy@@21 小时前
Axios 整理常用形式及涉及的参数
javascript
@大迁世界1 天前
告别 React 中丑陋的导入路径,借助 Vite 的魔法
前端·javascript·react.js·前端框架·ecmascript
EndingCoder1 天前
Electron Fiddle:快速实验与原型开发
前端·javascript·electron·前端框架
EndingCoder1 天前
Electron 进程模型:主进程与渲染进程详解
前端·javascript·electron·前端框架
想起你的日子1 天前
Vue2+Element 初学
前端·javascript·vue.js
小高0071 天前
一文吃透前端请求:XHR vs Fetch vs Axios,原理 + 实战 + 选型
前端·javascript·vue.js
无羡仙1 天前
JavaScript 数组扁平化全解析
前端·javascript
inksci1 天前
飞帆fvi.cn拖放配置实现卡片布局
前端·javascript
前端大卫1 天前
解决中文输入法导致的频繁 Input 事件!
前端·javascript