wangEditor v5实现Word批注与修订导入的完整方案
发布时间:2026/9/9 22:31:40 锦皓数字建站

如果你在wangEditor里导入过带批注的Word文档大概率会骂娘——正文倒是正常进来了但是同事给你的批注像人间蒸发一样修订痕迹也全变成了普通文本等于整个审阅过程白干了。这个事我前前后后折腾了两周最后在wangEditor v5里成功把Word导入、批注侧边展示、修订痕迹都做了出来甚至还顺手解决了几个表格双线变单线的兼容问题。今天就把这套实现思路和踩坑记录整理出来给同样被在线文档审阅折磨的朋友一点参考。1. 需求背景与整体方案选型在线文档一旦涉及多人协作批注和修订记录就是刚需。你可以把Word当成“审阅者的红笔”批注是贴便利贴修订是直接在稿子上画删除线、写替换文字。但wangEditor本身是一个纯前端富文本编辑器并没有原生支持Word批注和修订的导入Word文档上传后默认只能提取正文内容丢掉审阅信息。这会让很多团队在从Office搬到Web端时非常痛苦尤其是合同审批、论文评审、方案会签这类场景批注和修订就是核心资产丢不起。我当时接到的需求更直接用户把一份带审阅意见的Word文档丢进网页编辑器正文能编辑不说批注要能点开看修订要能区分出是谁改了哪一句、什么时候改的。如果这些做不到功能上线等于没做。1.1 为什么批注和修订必须被保留很多人觉得批注和修订只是格式问题实际上它们是“审阅流程”的载体。批注保存的是人和人的沟通上下文“这里写得不清楚”“这个数字需要再确认”这些信息一旦丢失接收到文档的人就不知道之前发生了什么。修订记录则更关键它保存了文档的演进历史谁在什么时候添加了什么、删除了什么、改了什么格式这是合规审计和团队追责的重要依据。更重要的是用户对这类功能有强烈的心理预期。他们在Word里已经习惯了“批注挂在右侧修订用颜色标出来”如果导入web编辑器后这些痕迹全部消失第一反应就是系统有bug而不是格式不支持。所以我们在技术选型上第一优先级就是尽可能保留原文档中的批注和修订元数据哪怕是只读展示也比完全丢失强。1.2 可行技术路线对比实现Word转HTML并保留批注修订业内其实有几种常见路线我这里直接做了个对比方案原理优点缺点后端转换POI/docx4j服务端解析docx转换成带有批注标记的HTML或JSON服务端可做缓存、可批量处理需要维护转换服务交互链路长调试麻烦前端解析 mammoth用JSZip解压docxmammoth将正文转HTML自己解析批注修订XML纯前端集成简单实时性好需要自己处理比较复杂的XML映射坑比较多商业组件如ONLYOFFICE、Collabora直接使用成熟的文档处理内核功能最完整基本不需要开发成本高部署重wangEditor生态里不好集成我最终选择了方案B核心技术栈是JSZip mammoth fast-xml-parser cheerio前端自己解析docx。理由很直接需求是“导入并保留批注修订”并不要求做一个完整的Office兼容内核。wangEditor本身是前端编辑器数据在浏览器里走一遍不需要后端参与部署成本最低而且docx格式是开放的zipXML结构完全有能力自己解析。这里也多说一句如果你所在团队有Java后端方案A其实也成熟POI对批注和修订的提取都有对应的API但前后端联调成本不低。如果你要做的是中文团队内部工具我更推荐前端方案迭代快、可定制性强遇到问题直接在浏览器里打断点比在后端绕一圈舒服得多。2. 核心细节解析docx中批注和修订到底存在哪很多人被“docx”这个后缀骗了以为它就是一个文件其实它本质上是一个zip压缩包。包里面有一堆XML文件其中最关键的是word/document.xml正文内容、word/comments.xml批注内容、word/people.xml人员信息。修订记录则直接嵌在document.xml里通过特定的标签标记插入、删除和格式修改。搞懂这几个文件的关系后面实现就顺了。2.1 批注的存储结构批注在docx里分两部分。第一部分是正文中的锚点在word/document.xml里表现为一对标签w:commentRangeStart w:id1/ w:rw:t需要审阅的文字/w:t/w:r w:commentRangeEnd w:id1/简单理解commentRangeStart和commentRangeEnd就像两个括号把被批注的文字包起来。id用来关联批注内容。第二部分是批注的具体内容在word/comments.xml里w:comment w:id1 w:author张三 w:date2025-01-15T10:30:00Z w:p w:rw:t这里的数据需要再核实一下/w:t/w:r /w:p /w:comment这里能看到批注者、批注时间和批注文本。所以解析思路很清晰先读取comments.xml拿到批注内容和作者信息再去document.xml里通过id找到对应的文字范围最后把这段文字包裹成一个带批注信息的HTML节点。如果你还见过word/people.xml它是用于存储参与者信息的尤其是Office 2016以后很多批注的author会指向people.xml里的displayName解析时如果发现comments.xml里只剩一个id就需要去people.xml里再查一次。2.2 修订记录的存储结构修订记录比批注稍微复杂一点因为它直接在正文流里标记。插入的内容放在w:ins标签里表示这段文字是后来加进去的w:ins w:id2 w:author李四 w:date2025-01-16T09:00:00Z w:rw:t新增的这段话/w:t/w:r /w:ins删除的内容放在w:del标签里被删除的文字通常用w:delText而不是w:t包裹目的是让解析器知道“这段文字已经不存在了”但仍然保留在文档里供审阅者查看w:del w:id3 w:author李四 w:date2025-01-16T09:00:00Z w:rw:delText这段被删掉了/w:delText/w:r /w:del还有格式修改比如有人把一段文字从红色改成黑色底层会用w:rPrChange记录修改前后的格式。不过大多数导入场景下我们可以先只关注插入和删除两类修订格式类的修订在HTML里不好还原一般用特殊底色提示即可。这里有一个容易踩的坑如果你的解析逻辑只读取w:t来取文本遇到删除修订就会漏掉内容因为被删除的文字写在w:delText里。实际开发中一定要两个标签都处理特别是需要展示修订痕迹时delText的内容通常要渲染成带删除线样式的文字。2.3 如何把XML锚点映射到HTML文本理论清楚了实际落地还有个麻烦mammoth.js 负责把document.xml转成干净的HTML但它默认不会保留批注和修订。经过测试mammoth对未知的commentRangeStart这类标签几乎是无视的直接跳过。所以我们需要自己做锚点映射。我在项目里用了一个不算优雅但很稳定的办法在把document.xml交给mammoth转HTML之前先预处理XML把批注锚点和修订标签替换成特殊的占位符。比如遇到commentRangeStart就插入一个文本标记【批注开始 id1】遇到w:ins就插入【修订插入开始】等mammoth转换完HTML再用cheerio把这些占位符替换成真正的自定义节点。为什么不用正规的span>npm install wangeditor/editor wangeditor/editor-for-vuenext jszip mammoth fast-xml-parser cheerio这里有个细节vue2项目里用wangEditor v5要装wangeditor/editor-for-vuenext老版本是给v4用的直接装最新npm包可能会拿到for-vue3版本导入的时候就会报“createEditor is not a function”之类的错误。如果你在vue2里使用务必确认版本。然后初始化编辑器import { createEditor, createToolbar, DomEditor } from wangeditor/editor import wangeditor/editor/dist/css/style.css3.2 解析docx的批注和修订上传文件后拿到ArrayBuffer先用JSZip解压读取需要的XMLconst zip await JSZip.loadAsync(arrayBuffer) const commentsXML await zip.file(word/comments.xml)?.async(string) const documentXML await zip.file(word/document.xml).async(string)然后用fast-xml-parser解析XML注意要保留属性把下划线后的_转成驼峰命名也打开方便后面取值const parser new XMLParser({ ignoreAttributes: false, attributeNamePrefix: _ }) const commentsDoc parser.parse(commentsXML)拿到批注列表后按id存成数组例如const commentMap {} commentsDoc[w:comments][w:comment].forEach(item { const id item[_w:id] const author item[_w:author] const date item[_w:date] // 从p→r→t链上取批注文本 const text extractText(item) commentMap[id] { author, date, text } })同一时间在document.xml里遍历所有w:commentRangeStart拿到位置信息插入占位符。由于一个文档里批注可能很多我会先给每个批注生成一个全局唯一ID然后以类似__COMMENT_START_1__的形式插入到文本流中。同理修订标签也做相同处理。3.3 生成带批注和修订标识的HTML预处理完document.xml后交给mammoth转换正文const result await mammoth.convertToHtml({ arrayBuffer: buf }, options) let html result.value此时HTML里会有一堆__COMMENT_START_1__这类占位符。接下来用cheerio把占位符替换成真正自定义元素const $ cheerio.load(html) $(body).html($(body).html().replace(/__COMMENT_START_(\d)__/g, (match, id) { return span>const commentModule { type: comment, parseElemHtml(elemDom) { return { type: comment, commentId: elemDom.getAttribute(data-comment-id), } }, renderElem(elem, children) { const vnode h(span, { class: wangeditor-comment, attrs: { data-comment-id: elem.commentId, contenteditable: false, title: 批注${commentMap[elem.commentId]?.text || }, }, }, children) return vnode }, toHtml(elem) { return span>const editor createEditor({ selector: #editor, html: , config: { EXTEND_CONF: { commentModule, insModule, delModule, }, }, })注册完成后把之前生成好的HTML交给编辑器editor.setHtml(html)这样编辑器就能识别>.wangEditor-tabble { border-collapse: collapse; } .wangEditor-tabble td { border: 1px solid #d0d7de; }如果需要精确还原双线可以在解析表格XML时把边框宽度信息提取出来转成内联CSS。但说实话大多数场景下用户能接受“单线灰色边框”优先保证布局一致不要过度纠结边框样式。4.5 加载大文档卡死如果Word文档有几百个批注或者正文有几十页前端一次性解析所有XML再转HTML确实会卡。我实测一个18MB的docx在普通笔记本上解析要好几秒。优化建议把JSZip解压和mammoth转换都放到Web Worker里跑主线程只负责接收结果。另外占位符替换阶段如果HTML很大不要用字符串全局replace用cheerio遍历文本节点这样能省掉很多无谓的重复匹配。5. 扩展方向与个人体会这个功能做完之后再回头看其实只是“解析docx 自定义节点渲染”的组合拳但前期对OOXML结构不熟的时候确实头大。如果你们团队也在做类似的东西有几个扩展方向可以提前考虑。5.1 导出为Word时保留批注和修订很多系统只要求“导入看”但真正完整的审阅闭环是需要再把文档导回Word并且批注修订都还在。这个可以用docx.js实现把你编辑器里的自定义节点反向转成OOXML里的commentRangeStart、commentRangeEnd、w:ins、w:del。工作量比导入还要大一点因为要处理嵌套和顺序但方向是明确的。5.2 配合只读模式和AI审稿如果你做的是审阅场景通常导入后要让普通用户“只能看不能改”这就用到了wangEditor的只读模式editor.disable()或者editor.config.readOnly true。批注在这种模式下反而更适合展示因为用户不需要删除或修改批注只需要阅读。现在也有团队把AI模型接进来让AI在正文里生成批注底层就是新增>
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。