高性能富文本编辑器必须放弃纯html模板驱动,转为基于虚拟dom或节点树的响应式控制,因contenteditable无法解决光标同步、格式隔离、增量更新等核心问题。

直接用 contenteditable + 原生 HTML 模板拼接,做不出高性能富文本编辑组件——模板只是静态结构,而富文本的核心是动态 DOM 状态管理、光标同步、格式隔离和增量更新。真要“高性能”,必须放弃纯模板驱动,转为基于虚拟 DOM 或节点树的响应式控制。
为什么 HTML 模板无法支撑富文本的实时交互
HTML 模板(比如 <div contenteditable="true"></div> 配合预设 toolbar 按钮)只解决“显示”问题,不解决以下关键行为:
-
contenteditable元素内光标位置无法被模板描述或预测,每次输入/粘贴后 DOM 结构可能突变,导致模板重渲染失焦 - 模板中写死的
<strong></strong>或<ul></ul>标签,无法响应用户选区变化自动高亮对应 toolbar 按钮(即document.queryCommandState('bold')在现代浏览器中已不可靠) - 粘贴 Word 内容时,模板生成的初始结构会被浏览器直接注入一堆嵌套
<div class="MsoNormal">,模板无法拦截或清洗 <li>表格、代码块、图片等复杂节点一旦用模板硬编码,就失去可编辑性——<code><td> 里没设 <code>contenteditable="true",点进去根本不能输字真正可用的“模板”应是声明式节点定义,不是 HTML 字符串
高性能组件(如 Tiptap、ProseMirror、mp-html 的编辑模式)从不依赖
innerHTML拼接模板,而是把“模板”理解为节点 schema:- 定义合法节点类型:比如
paragraph、heading、image、table,每个类型带自己的属性约束(如image必须有src和alt) - 用 JSON 描述初始内容,而非 HTML 字符串:
{ type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] }] } - 渲染层按 schema 映射到真实 DOM,但所有变更都走 transaction 提交,而非
innerHTML = newHtml - 这样做的好处:撤销/重做可序列化、协作编辑能 diff 节点差异、移动端光标不会因整段替换而跳失
如果非要从 HTML 字符串启动,必须做三件事
某些场景(如 CMS 导入旧文章)确实需要从 HTML 初始化编辑器。此时不能直接
editor.innerHTML = htmlStr,否则性能崩、状态丢、样式乱:- 先用
DOMParser解析 HTML 字符串,再递归遍历清洗:删空<span></span>、合并相邻文本节点、标准化<b></b>→<strong></strong>、补全缺失的<tbody> <li>禁止直接插入含 <code>style属性的节点;统一走编辑器内置的 mark 类型(如colormark),由 theme CSS 控制渲染 - 插入后调用
editor.commands.setContent()(Tiptap)或view.dispatch(tr)(ProseMirror),触发完整状态同步,而不是 DOM 替换 - 它没有 selection tracking,iOS 上长按选词后点击加粗,经常作用在错误位置
- 不支持撤销栈,
history.back()会丢失整个编辑状态 - 表格编辑仅限单元格内文字,无法增删行列、合并单元格——这些操作需要 schema-aware 的 command,不是 HTML 模板能承载的
- 它的优势在「渲染」:解析 HTML → 小程序组件树 → 高性能 diff 更新;编辑只是附带功能,别当主力用
mp-html 的编辑能力其实不推荐用于生产编辑场景
虽然
mp-html支持“编辑模式”,但它本质仍是渲染器扩展,不是编辑器内核:真正卡住性能的从来不是“怎么画按钮”,而是“光标在第 3 行第 5 个字符时,按 Ctrl+B,该不该把整个 paragraph 重新 render”。这一步没走对,模板再漂亮也白搭。
- 定义合法节点类型:比如











