1. 从一个真实困境说起:为什么协作工具需要原生 HTML 编辑能力
做过团队协作类产品的人大概都经历过这样的场景:产品经理在需求文档里贴了一段带样式的表格,开发在协作面板里看到的却是一堆错位的纯文本;运营同学想在一个任务卡片里嵌入一个简单的活动说明页,结果只能上传附件,点开还要下载。这类问题的根源不在于协作工具本身的功能缺失,而在于大多数协作平台的内容渲染层采用的是受限的富文本编辑器,它对 HTML 的支持是"白名单式"的——只允许加粗、斜体、列表这些基础标签,稍微复杂一点的布局、内联样式、自定义结构统统被过滤掉。
WorkBuddy 这个项目要解决的就是这个问题。它的核心命题是:在一个多人实时协作的环境里,如何安全地支持原生 HTML 内容的编辑与渲染。注意这里有两个关键词,一个是"原生 HTML",意味着不是那种被阉割的富文本子集,而是允许用户直接书写和编辑 HTML 结构;另一个是"协作",意味着多个用户可能同时编辑同一份内容,还要保证不冲突、不串数据。
这个能力适合谁参考?如果你正在做在线文档、项目管理面板、低代码搭建平台、邮件模板编辑器、营销落地页协作工具,或者任何需要让非技术人员也能编辑结构化内容的系统,这篇内容里的思路和踩坑记录都能直接复用。即便你只是想在内部工具里加一个"能贴 HTML 的备注框",也能从下面的方案取舍里找到适合自己的那一档。
我先把结论性的判断放在前面:协作场景下的 HTML 编辑,难点从来不是"怎么渲染 HTML",那是浏览器天生就会的事;真正的难点是如何在开放编辑能力和系统安全之间找到平衡点,同时还要处理协作带来的并发与状态同步问题。后面几个章节会围绕这个核心矛盾层层展开。
2. 拆解 WorkBuddy 的核心需求:原生 HTML 编辑到底要满足什么
2.1 需求分层:从"能编辑"到"能协作编辑"的四个台阶
很多人一上来就想做一个"完整的 HTML 编辑器",结果做到一半发现需求根本没理清。我在实际推进这个项目时,把需求拆成了四个递进的台阶,每一层解决不同的问题,团队可以按需选择做到哪一层。
第一层是基础编辑能力:用户能在一个编辑区域里输入 HTML 源码,并且能实时看到渲染效果。这一层本质上是"源码视图 + 预览视图"的双栏结构,技术上没有太大难度,一个 textarea 加一个 iframe 就能跑起来。
第二层是结构化编辑能力:用户不满足于看源码,还希望能像操作富文本一样,选中一段内容加个样式、插入一个表格、调整一下层级。这一层就需要引入所见即所得(WYSIWYG)的编辑逻辑,但又要保留 HTML 的完整结构,不能像传统富文本那样把内容转成私有格式。
第三层是协作同步能力:多个用户同时打开同一份内容,一个人的修改要能实时反映到其他人屏幕上,而且不能出现"我改的字被你的旧版本覆盖"这种经典冲突。这一层是 WorkBuddy 区别于普通 HTML 编辑器的关键。
第四层是安全与权限能力:不同角色的用户对 HTML 的编辑权限不同,有人只能改文字,有人能改结构,有人能插入脚本(当然要严格管控),同时所有渲染出来的 HTML 都要经过安全处理,防止恶意代码在协作环境里传播。
把这四层想清楚之后,你会发现很多技术选型的纠结就迎刃而解了。比如"要不要支持 script 标签"这个问题,答案取决于你的第四层设计——如果协作环境里有不可信用户,那答案就是坚决不支持,无论编辑体验多好。
2.2 为什么不能直接用现成的富文本编辑器
这是我在项目初期被问得最多的问题:"市面上那么多成熟的富文本编辑器,为什么不直接用?"我的回答是:现成富文本编辑器的数据模型和 HTML 不是一回事。
主流富文本编辑器大多采用自定义的文档模型(比如基于 JSON 的节点树),HTML 只是它的输入输出格式之一。当你把一段 HTML 喂给它,它会先解析成自己的模型,渲染时再转回 HTML。这个转换过程是有损的——你精心写的内联样式、自定义 data 属性、特殊的嵌套结构,很可能在转换中丢失或者被规范化成另一种写法。
对于 WorkBuddy 这种要求"原生 HTML"的场景,这种有损转换是不可接受的。用户写的是什么,存下来就应该是什么,渲染出来也应该是什么。所以我最终选择的路线是:以 HTML 字符串本身作为数据源,编辑器只做结构化的操作辅助,不做格式转换。这个决策后面会反复影响到协作同步和安全处理的实现方式。
2.3 协作场景给 HTML 编辑带来的额外约束
单机 HTML 编辑器和协作 HTML 编辑器,差别就像单人写文档和多人同时改一份文档。后者会引入几个前者完全不需要考虑的问题。
第一个是操作粒度问题。协作同步通常以"操作"为单位传输,比如"在位置 5 插入字符 a"。但 HTML 的结构化操作比纯文本复杂得多——插入一个 div 标签,实际上涉及开始标签和结束标签两个位置,中间还可能嵌套其他内容。如果操作粒度设计不好,同步时就会出现标签不匹配的破碎 HTML。
第二个是渲染一致性问题。不同用户的浏览器、不同版本的渲染引擎,对同一段 HTML 的解析可能有细微差异。在协作场景里,如果 A 看到的和 B 看到的不一样,讨论就会变成鸡同鸭讲。所以 WorkBuddy 在渲染层做了统一处理,尽量消除环境差异。
第三个是历史版本与回滚问题。协作过程中难免有人改错,需要回滚到某个历史版本。HTML 的版本管理比纯文本麻烦,因为它的结构是树形的,简单的 diff 算法效果不好。这部分我后面会单独讲。
3. 编辑器内核选型:在可控性和开发成本之间做取舍
3.1 三条技术路线的对比与实测结论
在确定"以 HTML 字符串为数据源"这个大方向后,编辑器内核的选择就成了关键决策。我实际评估并试用了三条路线,下面这张表是当时的对比结论。
| 路线 | 代表方案 | 开发成本 | HTML 保真度 | 协作适配难度 | 适用场景 |
|---|---|---|---|---|---|
| 纯源码编辑 | textarea + 语法高亮 | 低 | 完全保真 | 低 | 开发者工具、模板编辑 |
| 浏览器原生编辑 | contenteditable | 中 | 较高 | 高 | 轻量富文本、备注框 |
| 自建结构化编辑器 | 自定义模型 + 渲染层 | 高 | 可控 | 中 | 复杂协作、低代码平台 |
纯源码编辑这条路线最简单,用户直接写 HTML,系统只负责高亮和预览。它的优点是 HTML 完全保真,用户写什么就是什么,协作同步也简单,因为本质上就是文本同步。缺点是门槛高,非技术用户根本不会写 HTML,产品经理看到一堆尖括号就头大。
浏览器原生编辑路线利用 contenteditable 属性,让浏览器自己处理编辑逻辑。这条路线的诱惑力很大,因为浏览器帮你做了大量工作——光标管理、选区、输入法兼容等等。但实测下来问题不少:不同浏览器对 contenteditable 的行为差异很大,尤其是在处理嵌套结构和粘贴内容时;而且浏览器的编辑操作产生的 HTML 往往带有大量冗余标签和内联样式,保真度反而下降。
自建结构化编辑器是成本最高的路线,需要自己实现文档模型、渲染层、光标管理、选区处理等一整套东西。但它的可控性最强,协作适配也最灵活,因为你可以精确控制每一个操作的数据结构。
3.2 我最终选择的混合方案及其理由
经过反复权衡,WorkBuddy 最终采用的是一种混合方案:以源码编辑为基础,叠加结构化的操作辅助层。
具体来说,编辑区的主体是一个增强版的源码编辑器,支持语法高亮、标签自动补全、缩进格式化。用户可以直接写 HTML,也可以用工具栏插入常用结构(表格、列表、卡片等),插入操作会在光标位置生成规范的 HTML 片段。预览区实时渲染,让用户随时看到效果。
这个方案的核心考量是:把"完全自由的源码编辑"和"结构化的快捷操作"分开,让不同水平的用户各取所需。技术用户可以全程手写,享受完全的控制权;非技术用户可以用工具栏和模板,避开手写标签的麻烦。两者产出的都是标准 HTML,不存在格式转换的损耗。
提示:混合方案的关键在于"插入片段"的规范化。如果工具栏插入的 HTML 格式混乱,用户后续手写时就会很痛苦。我在项目里专门写了一套片段模板,保证插入的结构缩进统一、属性顺序一致,这样整个文档的可读性会好很多。
3.3 源码编辑器的几个实现细节
源码编辑器看起来简单,但要做得顺手,有几个细节必须处理好。
第一个是语法高亮的性能。HTML 的语法高亮需要对标签、属性、文本内容分别着色,如果每次输入都全量重新解析,长文档下会明显卡顿。我的做法是只对可视区域内的内容做高亮,滚动时动态更新,这样即使文档有几万行,编辑体验依然流畅。
第二个是标签自动闭合。用户输入<div>后,编辑器应该自动补上</div>,并且把光标放在中间。这个功能看似简单,但要处理好嵌套和自闭合标签的情况。比如输入<img>就不应该补闭合标签,输入<br>同理。我维护了一个自闭合标签的白名单,遇到白名单里的标签就跳过自动闭合。
第三个是缩进与格式化。HTML 的层级结构靠缩进体现,但用户手写时经常缩进混乱。我提供了一个"格式化"按钮,一键把整段 HTML 重新缩进。实现上用的是基于标签栈的算法:遇到开始标签入栈并增加缩进,遇到结束标签出栈并减少缩进,文本节点按当前缩进输出。
4. 协作同步的难点:HTML 结构如何在多人编辑中保持一致
4.1 为什么纯文本的 OT 算法不能直接套用
做过协作编辑的人对 OT(Operational Transformation,操作转换)和 CRDT(Conflict-free Replicated Data Type,无冲突复制数据类型)这两个词应该不陌生。它们是目前主流的两种协作同步方案。但直接把纯文本场景的 OT 算法搬到 HTML 编辑上,会遇到一个根本性的问题:HTML 是有结构的,而纯文本 OT 假设内容是一维字符序列。
举个例子。用户 A 在位置 10 插入了一个<div>开始标签,用户 B 同时在位置 15 插入了一个</div>结束标签。在纯文本 OT 看来,这是两个独立的插入操作,转换后可能变成 A 的<div>在位置 10,B 的</div>在位置 16。但这两个标签是否配对、配对后结构是否合法,纯文本 OT 完全不关心,结果就是一段破碎的 HTML。
所以 WorkBuddy 在同步层做了一件关键的事:把 HTML 解析成结构化的操作单元,在结构层面做转换,而不是在字符层面。具体来说,编辑器内部维护的是一棵 DOM 树,用户的每次编辑先转换成对树的操作(插入节点、删除节点、修改属性等),同步的是这些结构化操作,最后再序列化成 HTML 字符串存储。
4.2 结构化操作的粒度设计
操作粒度是协作同步的核心设计决策,粒度太粗会导致冲突频繁,粒度太细会导致同步数据量大、转换复杂。我最终确定的粒度是这样的:
- 文本编辑:以字符为单位,和纯文本 OT 一致,因为文本节点内部确实是一维的。
- 节点插入:以完整节点为单位,插入一个元素时,它的开始标签、属性、子节点、结束标签作为一个原子操作。
- 节点删除:同样以完整节点为单位,删除一个元素时连同它的所有子节点一起删。
- 属性修改:以单个属性为单位,修改 class 和修改 id 是两个独立操作。
- 节点移动:以"从位置 X 剪切,粘贴到位置 Y"的形式表达,内部拆成删除加插入。
这个粒度设计的逻辑是:保证每个操作执行后,文档树始终是合法的。插入和删除都以完整节点为单位,就不会出现标签不配对的情况。文本编辑虽然细,但文本节点内部没有结构,不会破坏树形。
4.3 冲突处理的实战策略
即便有了结构化操作,冲突依然会发生。最常见的冲突是两个人同时修改同一个节点的同一个属性,或者一个人删除了节点而另一个人正在这个节点里编辑。
对于属性冲突,我采用的策略是"后写覆盖",但会记录冲突日志,让用户知道自己的修改被覆盖了。这个策略看起来简单粗暴,但实测下来比复杂的合并算法更符合用户预期——大多数情况下,用户宁愿看到明确的覆盖提示,也不愿意面对一个自动合并出来的、谁都不认识的结果。
对于"删除与编辑"的冲突,策略是"删除优先"。如果 A 删除了一个节点,B 在这个节点里的编辑就自动失效,B 会收到提示说内容已被删除。这个策略的理由是:删除通常是有意为之的操作,而编辑可能是惯性输入,让删除生效更符合操作意图。
注意:冲突策略没有绝对的对错,关键是要和你的用户场景匹配。如果是文档协作,可能"编辑优先"更合适;如果是配置管理,可能"删除优先"更安全。WorkBuddy 面向的是任务协作场景,所以我选了上面这套策略。
4.4 同步数据的压缩与传输优化
结构化操作虽然表达清晰,但数据量比纯文本操作大。一个插入节点的操作,如果节点包含子节点,序列化后可能几百字节。在高频编辑场景下,这个开销不容忽视。
我做了两层优化。第一层是操作合并:短时间内对同一节点的连续操作合并成一个,比如连续插入三个字符,合并成一次插入三个字符的操作。第二层是增量传输:只传输变化的部分,而不是整个文档。这两层优化下来,实测同步数据量比优化前减少了大约七成,在弱网环境下的体验提升很明显。
5. 安全防线:让原生 HTML 编辑不成为系统漏洞
5.1 渲染隔离:iframe 沙箱的正确用法
支持原生 HTML 编辑,最大的安全顾虑就是恶意代码。用户完全可能写一段带脚本的 HTML,如果直接在协作页面里渲染,轻则弹窗骚扰,重则窃取其他用户的数据。
WorkBuddy 的渲染层采用了 iframe 沙箱方案。所有用户编辑的 HTML 内容,都在一个独立的 iframe 里渲染,而不是直接插入主文档。这个 iframe 配置了 sandbox 属性,禁用了脚本执行、表单提交、顶层导航等危险能力。
这里有个细节值得展开:sandbox 属性的取值直接决定了隔离强度。我最初用的是sandbox="allow-same-origin",想着让 iframe 里的内容能访问同源资源,方便加载样式。但后来发现这个配置有风险——如果 iframe 里的内容能访问同源,就可能通过某些方式影响主文档。最终我改成了sandbox="",也就是最严格的隔离,iframe 里的内容被当作来自独立源,无法访问主文档的任何东西。样式通过 postMessage 从主文档传入,虽然麻烦一点,但安全性有保障。
5.2 内容清洗:白名单机制的具体实现
iframe 沙箱解决了"渲染时执行脚本"的问题,但还有一类风险是"内容本身携带恶意结构",比如通过 CSS 表达式、超大尺寸的图片、无限循环的动画来拖垮页面。所以渲染前还需要对 HTML 做清洗。
清洗采用的是白名单机制:只允许特定的标签、属性和属性值通过,其余一律过滤。白名单的维护是个细致活,我列一下核心规则。
| 类别 | 允许项 | 处理方式 |
|---|---|---|
| 标签 | div, span, p, h1-h6, ul, ol, li, table, tr, td, a, img 等 | 白名单外标签直接移除,保留其文本内容 |
| 属性 | class, id, style, href, src, alt, title 等 | 白名单外属性移除 |
| style 属性 | 颜色、字体、边距、边框等展示类属性 | 过滤掉 position, z-index 等可能影响布局的属性 |
| href/src | 仅允许 http/https 协议 | 过滤 javascript: 等危险协议 |
| 事件属性 | 全部禁止 | onclick 等一律移除 |
清洗的实现我用了成熟的 HTML 解析库,而不是正则表达式。这一点很重要——用正则处理 HTML 是经典的踩坑行为,因为 HTML 的嵌套结构用正则根本描述不了,很容易被绕过。解析库会把 HTML 解析成树,然后逐节点检查,安全得多。
5.3 权限分级:不同角色能编辑什么
安全不只是技术问题,也是权限问题。WorkBuddy 把用户对 HTML 的编辑权限分成了三档。
第一档是只读,用户只能看渲染结果,不能编辑。适合普通成员查看任务说明。
第二档是受限编辑,用户可以编辑文本内容和基础样式,但不能插入新标签、不能修改结构。这一档的实现方式是:编辑器只允许在文本节点内操作,结构操作被禁用。
第三档是完全编辑,用户可以自由编辑 HTML 结构。这一档通常只开放给任务创建者或管理员。
权限分级的价值在于,它把安全风险控制在了可控范围内。即使完全编辑的用户写了有问题的 HTML,清洗层和沙箱层还有两道防线。而受限编辑的用户,从源头上就做不了危险操作。
6. 实测中踩过的坑与排查过程
6.1 光标跳动:协作同步中最恼人的问题
项目上线初期,用户反馈最多的问题就是"光标乱跳"。具体表现是:正在输入的时候,光标突然跳到别的位置,或者输入的内容出现在错误的地方。
这个问题的排查花了我不少时间。最初的怀疑是同步延迟导致的,但加了延迟日志后发现,即使同步很快,光标依然会跳。后来定位到根因:每次收到远程操作并应用到本地文档后,编辑器会重新渲染整个内容区,导致光标位置丢失。
解决思路是:在应用远程操作前,先记录当前光标的位置(用节点路径加偏移量表示),操作应用后,再根据操作对文档结构的影响,计算出光标应该移动到哪里,然后恢复。这里的关键是"根据操作影响计算新位置"——如果远程操作是在光标之前插入内容,光标要相应后移;如果是在光标之后,光标不动;如果删除了光标所在的节点,光标要移到最近的合法位置。
这个逻辑写起来不复杂,但边界情况很多,我前后改了三四版才稳定下来。经验就是:协作编辑器的光标管理,一定要用结构化的位置表示,绝对不能用简单的字符偏移量,否则一旦文档结构变化,偏移量就失效了。
6.2 粘贴富文本时的标签爆炸
另一个高频问题是粘贴。用户从网页或其他文档里复制一段内容,粘贴到编辑器里,结果带进来一大堆冗余标签和样式,把整个文档结构搞得一团糟。
这个问题的本质是:不同来源的 HTML 质量参差不齐,有的带大量嵌套的 span,有的带内联样式,有的甚至带条件注释。直接粘贴进来,文档就没法看了。
我的处理方案是在粘贴事件里做拦截和清洗。用户粘贴时,先拿到剪贴板里的 HTML,用和渲染层同一套清洗规则过一遍,去掉冗余标签和样式,然后再插入到文档里。同时提供一个"粘贴为纯文本"的选项,让用户可以选择只保留文字。
这里有个细节:清洗规则不能太激进,否则用户精心复制的表格结构会被破坏。我的做法是保留结构类标签(table、ul、ol 等),只清理样式类标签(font、span 的冗余嵌套)。这个平衡点是根据用户反馈反复调整出来的。
6.3 大文档下的性能断崖
当文档内容超过一定规模后,编辑器的性能会明显下降,输入有延迟,滚动卡顿。这个问题在协作场景下更严重,因为每次远程操作都会触发重新渲染。
性能优化我做了三件事。第一件是虚拟滚动,只渲染可视区域内的内容,文档再长也不影响渲染性能。第二件是操作批处理,把短时间内的多个远程操作合并成一次应用,减少重渲染次数。第三件是渲染节流,预览区的更新不是实时的,而是节流到每 200 毫秒一次,避免频繁重绘。
这三件事做完,实测在万行级别的文档下,输入延迟从原来的几百毫秒降到了几十毫秒,基本感觉不到卡顿。
7. 一些可以复用的经验与后续扩展方向
把 WorkBuddy 的 HTML 编辑能力做下来,我最大的体会是:协作场景下的富内容编辑,技术选型的核心不是"哪个方案最强",而是"哪个方案的可控性最高"。因为协作会放大一切不确定性,一个在单机场景下无关紧要的小问题,在多人同时编辑时可能变成灾难。所以我在每个决策点上,都优先选择那些行为可预测、边界清晰的方案,哪怕开发成本高一点。
如果要把这套能力继续扩展,我觉得有几个方向值得尝试。一是模板系统,把常用的 HTML 结构做成模板,用户一键插入,既降低了门槛,又保证了结构规范。二是版本对比,让用户能直观看到两个版本之间的差异,这对协作场景很有价值。三是组件化,把复杂的 HTML 结构封装成可复用的组件,用户编辑的是组件实例,底层还是 HTML,但操作更简单。
最后分享一个实操中的小技巧:在调试协作同步问题时,我习惯开两个浏览器窗口,一个正常操作,一个开着控制台看同步日志。这样能直观地看到操作是怎么传输、怎么应用的,比单窗口调试效率高很多。尤其是排查光标跳动这类问题时,双窗口对照能快速定位是发送端的问题还是接收端的问题。