Vue 3 持久化文本高亮组件发布:用字符区间替代 HTML 改写
译注:本文编译自 dev.to 社区文章《Persistent Text Highlighting for Vue 3》,作者 Marat Shagidullin,原文发布于 2026 年 9 月 28 日。原文链接见文末。
文本高亮看似简单:用户选中一段文字,包一层 <span>、加个背景色、保存结果即可。但在真实内容场景中,这种基于 replace() 的实现很快会变得难以维护,尤其是当文本包含 HTML、存在重复短语,或需要持久化并在之后恢复时。
一个常见的初版实现是这样的:
html = html.replace(
selectedText,
`<span class="highlight">${selectedText}</span>`,
);它在演示中可用,但面对真实内容就会失效。例如同一短语多次出现:
<p>Vue is great. Vue is flexible. Vue is everywhere.</p>基于 replace() 的方案无法可靠判断用户选中的究竟是哪一个 “Vue”,可能高亮第一个匹配项而非选中项,若使用全局替换则会高亮全部出现位置。
另一个常见问题出现在选区跨越 HTML 元素时:
<p>Read the <strong>important documentation</strong> carefully.</p>用户可能选中 “important documentation carefully”,但这段文字在原始 HTML 中并不作为一个连续子串存在。替换它需要解析并重写 DOM 结构,处理部分节点、嵌套标签、已有高亮以及重叠选区。每新增一处高亮,生成的 HTML 都会愈发复杂。
保存被修改的 HTML 还会让内容与标注产生不必要的耦合:存储中既有原始文档,又有生成的高亮标记。删除高亮意味着再次编辑 HTML,而文档结构一旦变化,已保存的标注就更难正确恢复。
vue3-highlight-text-color 采用了不同的思路:不保存生成的 HTML,而是把高亮存为源文本中的一个简单区间:
{
textId: 42,
color: "#bae6fd",
range: {
start: 18,
end: 41
}
}组件基于源 HTML 的 textContent 计算选中文本的字符偏移量;渲染时用这些偏移量在正确位置应用高亮,同时让原始源 HTML 与标注数据保持分离。这带来两点重要优势。
一、标注数据体积小、可序列化、与渲染无关
只需存储数字、一个颜色和一个可选的标记 ID。前端可将其放在 localStorage、Pinia 或其他状态管理器中,后端可持久化到普通数据库表或 JSON 字段。无需为每条标注保存整篇文章或其生成的高亮 HTML。对于包含大量高亮的文档,存储结构可以简单到如下形式:
[
{
"id": "marker-1",
"textId": 42,
"color": "#99f6e4",
"range": { "start": 18, "end": 41 }
},
{
"id": "marker-2",
"textId": 42,
"color": "#ddd6fe",
"range": { "start": 95, "end": 112 }
}
]这种数据格式易于传输、校验、索引、同步,也便于跨应用共享。
二、原始内容保持干净
源 HTML 始终是唯一事实来源,组件不要求在保存前修改它。高亮 span 只是渲染层面的事情,不属于存储文档的一部分。这使以下操作更容易:
- 在需要时渲染不带标注的同一文本;
- 之后加载并恢复标注;
- 使用自己的后端与持久化策略;
- 无需编辑原始 HTML 即可移除高亮;
- 将内容管理与用户生成的标注分离;
- 与未来可能出现的 Vue 之外的适配器共享同一标记格式。
该库内置默认取色器,但可通过 color-picker 插槽替换为自定义 UI,也可用 CSS 变量调整高亮样式,并按使用场景自定义颜色。
安装方式:
pnpm add vue3-highlight-text-color最小集成示例:
<script setup lang="ts">
import { ref } from "vue";
import {
subtractMarkerRange,
TextHighlighter,
type Marker,
type MarkerRange,
type NewMarker,
} from "vue3-highlight-text-color";
import "vue3-highlight-text-color/style.css";
const article = `
<p>
Select any part of this <strong>HTML text</strong> to create a highlight.
</p>
`;
const highlights = ref<Marker[]>([]);
function addHighlight(highlight: NewMarker) {
highlights.value.push({
...highlight,
id: crypto.randomUUID(),
});
}
function removeHighlight(range: MarkerRange) {
highlights.value = subtractMarkerRange(highlights.value, range);
}
</script>
<template>
<TextHighlighter
:text="article"
:text-id="42"
:markers="highlights"
:colors="['#99f6e4', '#bae6fd', '#ddd6fe']"
@handle-new-highlight="addHighlight"
@handle-remove-highlight="removeHighlight"
/>
</template>关于恢复:源文本需与保存的字符区间保持兼容。如果文档文本发生较大变化,应用可能需要内容版本化或区间迁移策略——这是任何持久化标注系统都要面对的问题。
该项目采用 MIT 许可证,可在在线 playground 试用,也可查看 GitHub 仓库或从 npm 安装。作者欢迎反馈、缺陷报告、功能请求与贡献。
原文链接
https://dev.to/marat_shagidullin_93df625/persistent-text-highlighting-for-vue-3-k7f