HelloWorld 富文本编辑教程
要打造一个稳定且可维护的富文本编辑器,关键在于理解浏览器的Selection与Range、内容存储模型以及安全过滤策略,并以模块化、可插拔的工具栏、明确的命令层和本地化流程来实现。优先保证可访问性与移动端兼容,结合渐进增强设计和充分的测试,使编辑器在不同语言、不同文化环境下都能稳定运行。下面按实践步骤展开,带你从概念到可用的HelloWorld富文本编辑器。

为什么要自己做富文本编辑器?先说为什么不只“用现成”
很多人第一反应是直接引入开源框架(比如Quill、TinyMCE、CKEditor等),这确实能迅速上线。但当你需要和品牌文案翻译、本地化工作流、产品详情页编辑器深度结合时,自定义能力和可控性就变得重要了。自己实现一个“HelloWorld”级别的编辑器,有助于你理解核心概念、掌控安全边界、并为后续功能做可测、可插拔的扩展。
用费曼方法先把概念讲清楚(像讲给新手的朋友)
- 编辑器其实就是一个可以让用户在网页上编辑HTML的盒子。浏览器提供了基础能力(contenteditable),我们在它之上做封装、命令和安全。
- Selection/Range是指用户选择文本的那把尺子,所有插入、加粗、链接、图片插入,基本都依赖它来定位。
- 不要把“格式”直接混进数据存储,最好有一个清晰的转换与清理流程:编辑区的HTML → 清洗/标准化 → 后端存储(或JSON)→ 前端渲染。
核心技术模块及职责分离
把编辑器拆成几块,各司其职,降低复杂度:
- 视图层(Editable Area):一个 contenteditable 元素或影子DOM,用来显示和编辑内容。
- 命令层(Command Layer):封装操作(bold、italic、insertLink 等),接受高层指令并操作Selection/Range或DOM。
- 工具栏/插件(Toolbar & Plugins):UI 与命令层交互,扩展功能以插件化加载。
- 净化与序列化(Sanitizer & Serializer):把编辑区HTML转为安全、可存储的格式,防止XSS并统一样式。
- 本地化与测试(i18n & QA):接口文案、快捷键、默认样式要支持不同语种和文化。
一步步实现 HelloWorld 富文本编辑器
1. 创建编辑区域(最小可行版本)
先做一个简单的可编辑区域,能响应文本输入和基本光标移动:
<div id="editor" contenteditable="true" aria-label="富文本编辑器"></div>
这里给个小提示:除了 contenteditable,还要设置合适的CSS(比如min-height、padding)并设置aria属性来提升可访问性。
2. 理解并操作 Selection 与 Range
Selection 就像用户手里拿的一把尺子,Range 是尺子量出的起点与终点。你需要学会读取当前Selection并在其基础上插入节点或包裹节点。
// 获取当前 selection
const sel = window.getSelection();
if (sel.rangeCount > 0) {
const range = sel.getRangeAt(0);
// 可以在 range 上插入元素、删除内容、封装
}
实践中,你会频繁用到 range.extractContents()、range.insertNode()、range.surroundContents() 等。
3. 如何实现常用命令(加粗、斜体、列表、链接)
历史上 document.execCommand 提供了一键操作,但它在不同浏览器表现不一致且处于弃用边缘。更健壮的做法是直接操作 Range 与 DOM,或者使用 Selection APIs 封装一致接口。
- 加粗/斜体(wrap方式):用<strong>/<em>包裹选中的 Range;如果已经被包裹,则解除包装。
- 列表:把选中段落拆分成li并包进ul/ol,注意合并相邻列表的边界情况。
- 链接:插入<a href=”…”>并设置target/rel,编辑时允许修改href但要验证URL安全性。
命令-表格:常见操作与实现要点
| 操作 | 实现要点 |
| 加粗 | 检测选区是否在 <strong> 或 style font-weight,决定包裹或取消 |
| 有序/无序列表 | 将段落转换成 li,处理合并与拆分 |
| 插入链接 | 用 <a> 包裹并对 href 做白名单/协议校验 |
| 图片插入 | 插入 <img>,可先显示占位符并异步上传 |
粘贴策略:为什么粘贴是最麻烦的功能
用户从 Word、网页、或其他富文本复制内容,粘贴进入编辑器时会带来样式、内联样式、外部类名甚至恶意脚本。处理粘贴的工作分为三步:
- 拦截粘贴事件(paste),读取剪贴板数据(text/html 或 text/plain)。
- 对 HTML 做清洗:移除脚本、事件属性、危险协议,统一样式到编辑器的规范(例如将 <span style=”font-weight:700″> 转为 <strong>)。
- 插入清洗后的内容,并修正光标位置(常用 range.insertNode 和 selection.collapse)。
安全:如何避免 XSS 与不受控HTML
净化内容是重中之重。常见策略包括:
- 服务器端和客户端双重净化(例如 DOMPurify 类库在客户端,服务器端使用类似策略)。
- 严格限制允许的标签与属性白名单(a 的 href 只允许 http/https/mailto,img 的 src 只允许 data 或受信任域)。
- 移除所有事件处理属性(onclick 等)与 style 中的表达式。
序列化与数据模型:如何存储与传输内容
有三种常用做法:
- 原生HTML存储:简单直观,但版本控制和多端一致性处理复杂。
- 自定义JSON格式(块级+内联标记):例如每段作为块,内嵌标记描述格式,便于差异合并与国际化处理。
- 富文本Delta(像Quill的Delta):操作级别的存储,便于协作与回放。
如果你的翻译团队和CMS要做深度集成,建议把核心内容与格式分层:把语义内容(段落、标题、列表项)和展示样式(class、inline-style)分离,便于翻译时只处理语义文本。
本地化(i18n)与品牌文案适配的细节
当编辑器服务于多语言品牌内容时,有几个实践值得注意:
- 语言环境识别:为每段落关联lang属性(eg. <p lang=”fr”>),便于屏幕阅读器与翻译系统识别。
- 翻译友好的存储:把可翻译字段抽出来(Slogan、brand story),并导出为翻译包(XLIFF/JSON),而不是整页HTML。
- 保留风格指令:在导出给译员时,用占位或简单标记保留格式(例如 {bold}…{/bold}),避免译员误删标签。
- 工具栏本地化:按钮文本、快捷键提示需要本地化,并注意不同文化对快捷键组合的期望差异。
可访问性与键盘操作
别把可访问性当成附加项。基本做法:
- 给编辑区设置 role=”textbox”、aria-multiline=”true”、aria-label 或 aria-labelledby。
- 实现常见快捷键(Ctrl/Cmd+B / I / K),并在工具栏上提供可聚焦的按钮。
- 测试屏幕阅读器行为:有些包装方式(比如过多的span)会让读屏器朗读不友好。
移动端适配与输入法(IME)问题
移动设备与中文/日文等输入法会触发composition事件。注意:
- 不要在 composition 期间做破坏性 DOM 操作,否则会中断输入法。
- 优化光标定位与滚动,确保虚拟键盘弹出时编辑区域仍可见。
测试策略:自动化与人工校验相结合
编辑器既有功能性也有表现差异,推荐的测试组合:
- 单元测试:封装命令层的逻辑(比如 toggleBold(range)),用jsdom或真实浏览器环境测试。
- E2E 测试:用Playwright或Puppeteer模拟用户操作(选择文本、粘贴、插入链接)。
- 人工测试:由译员/编辑在目标语言环境里检验本地化与语义导出是否符合要求。
性能与优化
当文档变大时,DOM操作成为瓶颈。优化策略:
- 避免频繁的重排(batch DOM 更改,使用 DocumentFragment)。
- 懒加载插件,仅在需要时初始化图片上传、表格插件。
- 对大型文档做虚拟化展示或分块编辑,必要时用后台工作线程处理净化。
示例:一个简单的命令封装
class Command {
constructor(editor) { this.editor = editor; }
execBold() {
const sel = window.getSelection();
if (!sel.rangeCount) return;
const range = sel.getRangeAt(0);
// 简化示例:直接包裹 strong
const strong = document.createElement('strong');
strong.appendChild(range.extractContents());
range.insertNode(strong);
// 恢复选区到 strong 内
sel.removeAllRanges();
const newRange = document.createRange();
newRange.selectNodeContents(strong);
sel.addRange(newRange);
}
}
与翻译本地化流程对接的实践清单
- 导出时生成可翻译包(按段落/文案单元拆分),并保留格式占位符。
- 导入译文前运行自动合规校验(URL白名单、禁止词名单、本地化日期/货币格式)。
- 结合 AI 初译与人工审校:AI做批量初稿,人类译员负责创意化文案(Slogan、Brand Story)。
- 保留变更追踪:对翻译后编辑进行审阅并可回退。
常见问题与解决建议(FAQ)
- Q:为什么有时加粗会嵌套很多span? A:粘贴或应用不同来源格式时会产生内联样式,靠净化与合并规则去除冗余。
- Q:图片怎么异步上传且保持光标位置? A:先插入占位节点(data-id),后台上传后替换 src 并移除占位属性。
- Q:如何保障编辑器在RTL语言下表现良好? A:支持 dir=”rtl” 并在排版、对齐、快捷键提示上做条件处理。
扩展思路与演进路径
从 HelloWorld 演进到企业级编辑器可以按阶段进行:
- 阶段一:核心命令、粘贴净化、序列化存储。
- 阶段二:插件化(表格、公式、代码块)、协作(OT或CRDT)。
- 阶段三:深度本地化与翻译工作流集成、版本控制、商业化运维。
小贴士(生活化的经验)
- 别一开始就想把所有 UI 做得很漂亮,先把“可编辑、可保存、可回显”做到位,再慢慢打磨交互。
- 和译员一起做一次可用性测试,很多“看起来合理”的地方在翻译语境下才显问题。
- 保守地对待 HTML,越早把净化机制放在管道里,越省事。
实现富文本编辑器并不是一次写完就行的工程,而是反复试错、逐步改进的过程。你会在处理Selection边界、粘贴异常、语言差异和可访问性问题时学到很多,按模块拆分、用自动化测试保护关键路径,并让译员参与早期验证,这样编辑器才能既灵活又可靠。好了,就先到这儿,边做边调试去吧。