富文本编辑(Rich Text Editing)又称 WYSIWYG(What You See Is What You Get),指用户在浏览器中直接操作排版、样式、图片、列表等内容,页面即时呈现最终效果。无论是后台 CMS、博客编辑器还是评论区,都离不开对富文本的支持。
浏览器提供两种原生机制让 DOM 进入可编辑状态:document.designMode 与 contenteditable。
designMode 针对整个文档(通常是一个 iframe)启用编辑能力。
<!-- 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 可作用于任意元素,使其可直接编辑,是现代浏览器首选方案。
<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 文档 | 隔离主文档、快捷启用菜单 | 需要额外页面;与主文档样式隔离;移动端兼容性一般 |
contenteditable | 任意元素 | 无需 iframe、随主文档样式、现代首选 | 工具栏、焦点与样式管理需自行实现 |
早期浏览器通过 document.execCommand() 对可编辑区域执行格式化命令。
⚠️ 已废弃:
execCommand已被规范标记为废弃(obsolescent),不建议在新项目中使用;但因其兼容性极佳,简单场景仍能看到。现代编辑器普遍基于 Selection/Range 或自有数据模型实现。
常用命令:
| 命令 | 作用 | 备注 |
|---|---|---|
bold / italic | 加粗 / 斜体 | |
foreColor | 设置前景色 | 需传入颜色值 |
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 描述用户当前选区,可通过 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;
gap: 8px;
margin-bottom: 12px;
}
#editor {
border: 1px solid #ccc;
border-radius: 4px;
min-height: 160px;
padding: 12px;
}
</style>
</head>
<body>
<div class="toolbar">
<button data-cmd="bold">加粗</button>
<button data-cmd="italic">斜体</button>
<button data-cmd="insertUnorderedList">列表</button>
<button id="clearFormat">清除格式</button>
</div>
<div id="editor" contenteditable="true">
<p>选中文本后点击上方按钮</p>
</div>
<script>
const editor = document.getElementById("editor")
document.querySelectorAll("[data-cmd]").forEach((btn) => {
btn.addEventListener("click", () => {
document.execCommand(btn.dataset.cmd, false, null)
editor.focus()
})
})
document.getElementById("clearFormat").addEventListener("click", () => {
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 清洗,避免产生非预期标签。
javascript:)。contenteditable 默认支持常见快捷键,但在特定业务中可能需要自定义(比如阻止 Enter 插入段落)。paste 事件并做格式化处理。compositionstart / compositionend 事件。| 名称 | 技术栈 | 特点 |
|---|---|---|
| ProseMirror / TipTap | 原生 / Vue / React | 插件化强、数据结构严谨、支持协同编辑 |
| Slate | React | 使用 JSON AST 表达文档,完全受控组件,利于自定义 |
| Quill | 原生 / React | API 简洁、社区庞大、主题丰富 |
| CKEditor 5 | 原生 / 框架集成 | 成熟商业方案,提供协同、版本控制、协作文档等扩展 |
| Draft.js | React | Facebook 开源,支持受控编辑(目前维护节奏放缓) |
选择框架时需要考量:是否支持协同、Markdown/HTML 转换、插件可扩展性、授权方式、移动端表现等。
innerHTML:读取或渲染内容前进行净化,避免恶意脚本。style 属性。通过上述内容,你可以系统掌握从选择控件到整体表单提交、再到富文本编辑的完整链路。根据业务需求挑选合适的策略,并在性能、可访问性与安全性之间做出平衡,即可构建稳定、高质量的表单体验。