Skip to content

Vue 3 持久化文本高亮组件发布:用字符区间替代 HTML 改写 ​

译注:本文编译自 dev.to 社区文章《Persistent Text Highlighting for Vue 3》,作者 Marat Shagidullin,原文发布于 2026 年 9 月 28 日。原文链接见文末。

文本高亮看似简单:用户选中一段文字,包一层 <span>、加个背景色、保存结果即可。但在真实内容场景中,这种基于 replace() 的实现很快会变得难以维护,尤其是当文本包含 HTML、存在重复短语,或需要持久化并在之后恢复时。

一个常见的初版实现是这样的:

typescript
html = html.replace(
  selectedText,
  `<span class="highlight">${selectedText}</span>`,
);

它在演示中可用,但面对真实内容就会失效。例如同一短语多次出现:

xml
<p>Vue is great. Vue is flexible. Vue is everywhere.</p>

基于 replace() 的方案无法可靠判断用户选中的究竟是哪一个 “Vue”,可能高亮第一个匹配项而非选中项,若使用全局替换则会高亮全部出现位置。

另一个常见问题出现在选区跨越 HTML 元素时:

xml
<p>Read the <strong>important documentation</strong> carefully.</p>

用户可能选中 “important documentation carefully”,但这段文字在原始 HTML 中并不作为一个连续子串存在。替换它需要解析并重写 DOM 结构,处理部分节点、嵌套标签、已有高亮以及重叠选区。每新增一处高亮,生成的 HTML 都会愈发复杂。

保存被修改的 HTML 还会让内容与标注产生不必要的耦合:存储中既有原始文档,又有生成的高亮标记。删除高亮意味着再次编辑 HTML,而文档结构一旦变化,已保存的标注就更难正确恢复。

vue3-highlight-text-color 采用了不同的思路:不保存生成的 HTML,而是把高亮存为源文本中的一个简单区间:

typescript
{
  textId: 42,
  color: "#bae6fd",
  range: {
    start: 18,
    end: 41
  }
}

组件基于源 HTML 的 textContent 计算选中文本的字符偏移量;渲染时用这些偏移量在正确位置应用高亮,同时让原始源 HTML 与标注数据保持分离。这带来两点重要优势。

一、标注数据体积小、可序列化、与渲染无关

只需存储数字、一个颜色和一个可选的标记 ID。前端可将其放在 localStorage、Pinia 或其他状态管理器中,后端可持久化到普通数据库表或 JSON 字段。无需为每条标注保存整篇文章或其生成的高亮 HTML。对于包含大量高亮的文档,存储结构可以简单到如下形式:

json
[
  {
    "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 变量调整高亮样式,并按使用场景自定义颜色。

安装方式:

shell
pnpm add vue3-highlight-text-color

最小集成示例:

vue
<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