富文本编程
富文本编辑(Rich Text Editing)又称 WYSIWYG(What You See Is What You Get),指用户在浏览器中直接操作排版、样式、图片、列表等内容,页面即时呈现最终效果。无论是后台 CMS、博客编辑器还是评论区,都离不开对富文本的支持。
为什么要关注富文本
- 用户体验:富文本允许所见即所得,降低排版门槛。
- 业务需求:自定义样式、表格、图片、附件等是常见诉求。
- 实现成本:需要在易用性与可维护性之间平衡,避免重复造轮子。
- 安全合规:富文本意味着可执行 HTML,必须关注脚本注入、XSS 等风险。
开启可编辑模式
浏览器提供两种原生机制让 DOM 进入可编辑状态:document.designMode 与 contenteditable。
designMode
designMode 针对整个文档(通常是一个 iframe)启用编辑能力。
<!-- HTML 结构省略,仅展示关键 JS 逻辑 --><!-- 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 可作用于任意元素,使其可直接编辑,是现代浏览器首选方案。
<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 / 图片 | 兼容性不一 |
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.focusNodeselection.getRangeAt(index):获取指定范围。range.cloneContents():克隆范围中的节点。range.deleteContents()/range.insertNode(node):删除或插入。range.surroundContents(wrapper):用节点包裹选区。
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 等)的底层基础。
自定义工具栏示例
下面示例演示一个最小化工具栏,结合 contenteditable 与 execCommand/Selection 操作:
<!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) 判断可用性,并及时更新按钮状态。
表单集成
富文本区域往往不属于原生表单控件,需要手动在提交时同步内容。
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,可以改为:
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 | 插件化强、数据结构严谨、支持协同编辑 |
| Slate | React | 使用 JSON AST 表达文档,完全受控组件,利于自定义 |
| Quill | 原生 / React | API 简洁、社区庞大、主题丰富 |
| CKEditor 5 | 原生 / 框架集成 | 成熟商业方案,提供协同、版本控制、协作文档等扩展 |
| Draft.js | React | Facebook 开源,支持受控编辑(目前维护节奏放缓) |
选择框架时需要考量:是否支持协同、Markdown/HTML 转换、插件可扩展性、授权方式、移动端表现等。
最佳实践清单
- 明确需求范围:是否需要表格、公式、代码块、协同编辑、版本历史等高级功能。
- 区分视图与数据:使用结构化数据(如 JSON AST)比直接处理 HTML 更易维护、转换。
- 慎用
innerHTML:读取或渲染内容前进行净化,避免恶意脚本。 - 保持语义化:尽量输出语义化标签/样式,避免内联大量
style属性。 - 提供快捷键与可访问性:工具栏应支持键盘导航、ARIA 标签、屏幕阅读器提示。
- 关注性能:大文档、高频撤销、复杂 DOM 操作需要虚拟化或分块渲染策略。
- 足量测试:覆盖不同浏览器、输入法、剪贴板来源、移动端手势等边界场景。
通过上述内容,你可以系统掌握从选择控件到整体表单提交、再到富文本编辑的完整链路。根据业务需求挑选合适的策略,并在性能、可访问性与安全性之间做出平衡,即可构建稳定、高质量的表单体验。