{T}

富文本编程

富文本编辑(Rich Text Editing)又称 WYSIWYG(What You See Is What You Get),指用户在浏览器中直接操作排版、样式、图片、列表等内容,页面即时呈现最终效果。无论是后台 CMS、博客编辑器还是评论区,都离不开对富文本的支持。

为什么要关注富文本

  • 用户体验:富文本允许所见即所得,降低排版门槛。
  • 业务需求:自定义样式、表格、图片、附件等是常见诉求。
  • 实现成本:需要在易用性与可维护性之间平衡,避免重复造轮子。
  • 安全合规:富文本意味着可执行 HTML,必须关注脚本注入、XSS 等风险。

开启可编辑模式

浏览器提供两种原生机制让 DOM 进入可编辑状态:document.designModecontenteditable

designMode

designMode 针对整个文档(通常是一个 iframe)启用编辑能力。

html
<!-- HTML 结构省略,仅展示关键 JS 逻辑 -->
html
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>designMode 示例</title>
    <style>
      iframe {
        width: 100%;
        min-height: 360px;
        border: 1px solid #ccc;
      }
    </style>
  </head>
  <body>
    <iframe id="editor" src="embedded.html" title="富文本编辑框"></iframe>
    <script>
      window.addEventListener("load", () => {
        const frame = document.getElementById("editor")
        frame.contentDocument.designMode = "on"
      })
    </script>
  </body>
</html>

contenteditable

contenteditable 可作用于任意元素,使其可直接编辑,是现代浏览器首选方案。

html
<div id="editor" class="rich-zone" contenteditable="true">
  <h2>随便写点什么</h2>
  <p>可插入链接、图片、列表……</p>
</div>

<script>
  const editor = document.getElementById("editor")
  editor.addEventListener("focus", () => editor.classList.add("is-active"))
  editor.addEventListener("blur", () => editor.classList.remove("is-active"))
</script>

contenteditable 属性接受三个字符串值:

  • true:开启编辑。
  • false:禁用编辑。
  • inherit:继承父元素的设置。

方案对比

方案适用范围优势局限
designMode整个 iframe 文档隔离主文档、快捷启用菜单需要额外页面;与主文档样式隔离;移动端兼容性一般

// ... 中间省略 ...

| insertOrderedList | 切换为有序列表 | 同类的还有 insertUnorderedList | | createLink | 创建超链接 | 需要传入 URL | | removeFormat | 清除行内格式 | | | formatBlock | 应用块级元素 | 传入 <h1><blockquote> 等 | | insertHTML/insertImage | 插入 HTML / 图片 | 兼容性不一 |

code
function applyFormat(command, value = null) {
  document.execCommand(command, false, value)
}

document.getElementById("bold").addEventListener("click", () => applyFormat("bold"))
document.getElementById("color").addEventListener("click", () => applyFormat("foreColor", "#ff4d4f"))

Selection 与 Range API(现代能力)

Selection 描述用户当前选区,可通过 window.getSelection() 获取;选区由一个或多个 Range 组成,Range 则标记起始节点、偏移量。

关键属性/方法:

  • selection.anchorNode / selection.focusNode
  • selection.getRangeAt(index):获取指定范围。
  • range.cloneContents():克隆范围中的节点。
  • range.deleteContents() / range.insertNode(node):删除或插入。
  • range.surroundContents(wrapper):用节点包裹选区。
javascript
function highlightSelection(color = "#ffe58f") {
  const selection = window.getSelection()
  if (!selection || selection.isCollapsed) return

  const range = selection.getRangeAt(0)
  const wrapper = document.createElement("span")
  wrapper.style.backgroundColor = color
  range.surroundContents(wrapper)
  selection.removeAllRanges()
  selection.addRange(range)
}

document.getElementById("highlight").addEventListener("click", () => highlightSelection())

配合 Selection API 可以实现更细粒度的操作(如应用自定义行内样式、插入组件占位符等),也是现代编辑器(ProseMirror、Slate 等)的底层基础。

自定义工具栏示例

下面示例演示一个最小化工具栏,结合 contenteditableexecCommand/Selection 操作:

html
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>简易富文本编辑器</title>
    <style>
      body {
        font-family: system-ui, sans-serif;
        margin: 24px;
      }
      .toolbar {
        display: flex;

  // ... 中间省略 ...

        document.execCommand("removeFormat", false, null)
        editor.focus()
      })
    </script>
  </body>
</html>

即便使用 execCommand,也建议在工具栏上通过 document.queryCommandState(command)document.queryCommandEnabled(command) 判断可用性,并及时更新按钮状态。

表单集成

富文本区域往往不属于原生表单控件,需要手动在提交时同步内容。

javascript
const form = document.querySelector("#postForm")
const editor = document.querySelector("#editor")
const hiddenField = form.elements["content"]

form.addEventListener("submit", (event) => {
  hiddenField.value = editor.innerHTML
})

如果使用 iframe + designMode,可以改为:

javascript
form.addEventListener("submit", () => {
  const frame = document.getElementById("editorFrame")
  form.elements.content.value = frame.contentDocument.body.innerHTML
})

在发送到后端前,可搭配 DOMPurify 等库做 HTML 清洗,避免产生非预期标签。

安全与兼容性注意事项

  • 输入过滤:必须在服务端做白名单过滤,移除脚本、事件处理器、危险的 URL(如 javascript:)。
  • XSS 防护:对外展示富文本时,确认内容已经过转义或净化。
  • 快捷键拦截contenteditable 默认支持常见快捷键,但在特定业务中可能需要自定义(比如阻止 Enter 插入段落)。
  • 粘贴处理:剪贴板里常带有内联样式、Word 专用标签,建议监听 paste 事件并做格式化处理。
  • 撤销堆栈:浏览器原生撤销/重做有限,复杂场景需自行维护操作栈。
  • 国际化:IME 输入(中文、日文)可能与 Selection 操作冲突,要关注 compositionstart / compositionend 事件。

现代富文本框架概览

名称技术栈特点
ProseMirror / TipTap原生 / Vue / React插件化强、数据结构严谨、支持协同编辑
SlateReact使用 JSON AST 表达文档,完全受控组件,利于自定义
Quill原生 / ReactAPI 简洁、社区庞大、主题丰富
CKEditor 5原生 / 框架集成成熟商业方案,提供协同、版本控制、协作文档等扩展
Draft.jsReactFacebook 开源,支持受控编辑(目前维护节奏放缓)

选择框架时需要考量:是否支持协同、Markdown/HTML 转换、插件可扩展性、授权方式、移动端表现等。

最佳实践清单

  • 明确需求范围:是否需要表格、公式、代码块、协同编辑、版本历史等高级功能。
  • 区分视图与数据:使用结构化数据(如 JSON AST)比直接处理 HTML 更易维护、转换。
  • 慎用 innerHTML:读取或渲染内容前进行净化,避免恶意脚本。
  • 保持语义化:尽量输出语义化标签/样式,避免内联大量 style 属性。
  • 提供快捷键与可访问性:工具栏应支持键盘导航、ARIA 标签、屏幕阅读器提示。
  • 关注性能:大文档、高频撤销、复杂 DOM 操作需要虚拟化或分块渲染策略。
  • 足量测试:覆盖不同浏览器、输入法、剪贴板来源、移动端手势等边界场景。

通过上述内容,你可以系统掌握从选择控件到整体表单提交、再到富文本编辑的完整链路。根据业务需求挑选合适的策略,并在性能、可访问性与安全性之间做出平衡,即可构建稳定、高质量的表单体验。