前阵子被一段“从Word复制进百度UE编辑器后整篇乱掉”的稿子折腾了半宿。用户用的是百度UE,也就是大家熟知的UEditor,从Word 2019复制了一篇带三级列表、两张表格、若干截图的技术文档,粘贴进编辑器点击保存,再从前端页面打开,三级标题全变成普通段落,表格边框全线丢失,图片倒是还在,但所有列表都变成了带着诡异缩进的纯文本。更麻烦的是,编辑部同事用的Office版本五花八门,Word 2010、2016、WPS、Office 365都有,每台机器复制出来的“脏HTML”还都不一样。
排查到最后,问题基本锁定在“Word文档生成的那套HTML结构”和“UEditor自带的过滤规则”之间缺少一层翻译。这篇文章不打算铺垫太多背景,直接把我在实际项目里做的一层Word版本兼容扩展展开讲。如果你正在给UEditor写二次开发,或者需要处理“从不同版本Word/WPS复制粘贴进富文本编辑器后格式错乱”的问题,下面这套组件化扩展的思路、步骤和踩坑记录应该能帮你少走不少弯路。
1. 从编辑器粘贴事故说起:Word兼容问题的真正形态
1.1 一个让我决定改扩展的线上案例
先说那个让我决定动手的案例。当时用户反馈的是“复制进来以后内容本身没丢,但格式完全不是Word里的样子”,比如一个三级列表,在Word里看起来是“1.1.1 标题”这种自动编号,粘贴后却变成“1. 1.1 1.1.1”,或者序号全没了。再比如Word里带底纹的表格,粘到编辑器后底纹丢失,只剩一层不完整的border。
我第一反应是UEditor的默认过滤把样式吃了,于是调高了白名单,加了一堆allowClass、allowStyle配置,结果问题反而更严重了。后来把编辑器里的HTML复制出来,才看到里面残留了大量类似下面的内容:
html复制<!--[if gte mso 9]><xml><w:WordDocument><w:View>Normal</w:View><w:Zoom>0</w:Zoom>
<w:TrackMoves>false</w:TrackMoves><w:TrackFormatting></w:TrackFormatting>
<w:PunctuationKerning/><w:DrawingGridVerticalSpacing>7.8 磅</w:DrawingGridVerticalSpacing>
<w:DisplayHorizontalDrawingGridEvery>0</w:DisplayHorizontalDrawingGridEvery>
</w:WordDocument></xml><![endif]-->
这些是Word复制到剪贴板时自带的条件注释、命名空间声明和MSO样式。不同Office版本不会老老实实地给你一套标准HTML,而是带着各种自己的私有协议。UEditor就算内置了过滤,也挡不住这些结构在到达编辑器之前已经把样式层级弄乱了。
1.2 不同版本Word“穿”到剪贴板里的衣服
Word文档本身是二进制或者Office Open XML格式,但做“复制粘贴”时,Office会额外生成一份HTML放在系统剪贴板的text/html字段里。这份HTML不是简单地把内容转成<p>、<span>,而是夹杂着大量不同版本特有的“包装层”。
我拿实际复制出来的一段内容统计过,不同来源的HTML特征差异非常明显,见下表:
| 文档来源 | HTML常见特征 | 典型问题 |
|---|---|---|
| Word 2003 / 早期 .doc | 大量<xml>块、<o:p>空段落、VML图形描述 |
条件注释进入正文,段落间出现大量空行 |
| Word 2007/2010 (.docx) | xmlns:w="urn:schemas-microsoft-com:office:word",<style>里定义了大量.MsoNormal、.MsoListParagraph |
UEditor会丢弃<style>,导致样式依赖全部失效 |
| Word 2013/2016+ | 更强的mso-list段落模拟列表,class名称带MsoListParagraphCxSpFirst/Middle/Last |
三段为一个列表项,清洗后列表错乱严重 |
| Office 365 网页版 | 开始转向标准<ul>/<ol>,但style里仍带mso-私有属性,部分内联样式冲突 |
直接插进去之后,列表外观和编辑器主题样式互相打架 |
| WPS / 国产Office | 兼容Windows版Word产物,但同时会生成data-自定义属性和自己的一套字体回退 |
如果按“微软家族”去白名单,会漏掉很多属性 |
真正的问题不是文字编码或者中文字符,而是Word在“版本A”里用<p mso-list>模拟列表,在“版本B”里改用真正的<ul><li>,在“版本C”里又是<span>加手动编号。如果不做版本识别和结构归一化,只靠UEditor默认的filterWord,做一百遍都是一个结果——不同版本粘贴后表现不一样。
这也是为什么标题里的“版本兼容”四个字,本质上不是去兼容Word二进制格式,而是要兼容“Word通过剪贴板生成的那几种HTML方言”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 剖析UEditor的粘贴链路,找到最合适的扩展点
2.1 UEditor本身就是一套“过滤器链”
UEditor不像市面上那些轻量编辑器那样直接execCommand('insertHTML')就完事,它在把外部HTML插入编辑区域前,会经过一系列处理。这个过程大体是:
- 接收剪贴板里的
text/html; - 调用
UE.filterWord等工具对Word特征做初步过滤; - 经过XSS白名单过滤规则,把不在白名单里的标签和属性移除;
- 调用浏览器的
insertHTML能力把清理后的HTML插进可编辑区域; - 最后触发
contentchange,把内容同步到隐藏的textarea。
这里面最容易出问题的,是第3步。XSS过滤规则为了安全,默认会删除<style>、<xml>、<o:p>,还会把形如mso-list开头的内联样式当作罕见样式直接剥掉。结果就是,Word里那套依赖class="MsoListParagraph"和<xml>声明的列表逻辑,到了编辑器里变成了一堆没有层级关系的<p>标签。
所以,如果只依靠UEditor自带的处理,不同Word版本的产物基本都会被“一刀切”,但切完之后能不能恢复到Word里的视觉层级,就是纯运气了。正确做法是:在UEditor的默认过滤之前,先做一轮“版本归一化”,把各种Word方言翻译成UEditor认识的标准HTML,然后再让UEditor的过滤链去处理。
2.2 扩展点怎么选:监听、命令、还是代理按钮
UEditor做二次开发有好几个入口,常见的有:
UE.registerUI:注册工具栏按钮;UE.commands:注册内部命令;editor.addListener:监听编辑器事件;- 直接在实例上覆盖某个方法。
我实验下来,处理Word粘贴最合适的入口是“监听粘贴事件 + 注册自定义命令”。为什么不是工具栏按钮?因为编辑的操作是直接把Word内容复制进编辑器,他不可能先去点一下“导入Word专用按钮”再粘贴。为什么不是只监听paste事件直接插入?因为UEditor的插入结果最好还是通过它自己的命令体系进入undo/redo栈,不然用户按Ctrl+Z撤销时会发现内容撤不掉。
下面是事件绑定的骨架。UEditor版本不同API细节略有差别,但思路是一致的:
javascript复制editor.addListener('ready', function () {
var body = editor.body;
body.addEventListener('paste', function (e) {
var html = '';
if (e.clipboardData && e.clipboardData.getData) {
html = e.clipboardData.getData('text/html') || '';
} else if (window.clipboardData && window.clipboardData.getData) {
// 老版本IE的回退
html = window.clipboardData.getData('Text') || '';
}
if (!html) {
return;
}
if (isWordHtml(html)) {
e.preventDefault();
e.stopPropagation();
var normalized = normalizeWordHtml(html);
editor.execCommand('insertNormalizedHtml', normalized);
}
});
});
这段代码的核心思路是:先判断剪贴板里的HTML是否属于Word产物,是的话就拦截默认粘贴,把内容交给自定义的normalizeWordHtml处理,再通过insertNormalizedHtml命令插回去。这样既挡住了原始脏HTML,又不会破坏UEditor自己的内容管理机制。
2.3 为什么“组件扩展”比“改源码”划算
很多人遇到这种问题第一反应是直接改UEditor源码,比如去改filterWord函数,或者在核心文件里加一段处理逻辑。改源码确实最快,但副作用也最明显:
- 以后UEditor升级就没法直接覆盖文件了;
- 多个项目如果共用一个公共包,A项目的特殊处理会影响到B项目;
- 一旦改了核心过滤,出问题后很难判断是UEditor本身的bug还是自己改出来的bug。
组件扩展的好处在于,所有兼容逻辑都被收拢在一个独立模块里,不侵入核心代码。需要的时候加载一次,不需要的时候整个目录删掉即可。后面你会看到,我把所有规则都放进了third-party/wordCompat目录里,单个模块职责单一,调试时只需要在控制台里查对应文件,不用翻UEditor那一大坨源码。
3. 把Word版本兼容收敛到一个可插拔组件中
3.1 单组件多模块:wordCompat怎么组织代码
把扩展做成组件,并不是说写一个大函数就行。Word粘贴处理会涉及版本判断、HTML清理、列表重建、表格补丁、图片处理等多件事,混在一起后期没法维护。我实际项目的目录结构是这样的:
text复制ueditor/
third-party/
wordCompat/
index.js // 对外入口,负责注册命令和监听事件
version-detector.js // 识别HTML来源,判断Word版本
html-cleaner.js // 去掉命名空间、条件注释、无意义空标签
list-normalizer.js // 把mso-list还原成嵌套ol/ul
table-normalizer.js // 表格基础样式补全
index.js // 把上面几个模块串联起来
每个文件只做一件事:
version-detector.js检查HTML字符串,返回一个“可能的来源版本”;html-cleaner.js负责删除XML声明、Office条件注释、无关的<xml>标签;list-normalizer.js专门处理列表段落;table-normalizer.js处理表格的宽度、边框、内容对齐等;index.js暴露initWordCompat(editor),在UEditor实例初始化时调用。
这样拆分的另一个好处是:列表、表格、HTML清洗这些函数,不只用在剪贴板场景,后面如果要支持直接上传docx文件解析,也可以复用同一套逻辑。
3.2 粘贴拦截:先判断来的是不是Word内容
拦截之前必须先判断“是不是Word内容”。判断逻辑不能太激进,因为用户在网页复制带格式的内容时也可能误触发。我用了几个特征组合判断,命中任意两个以上才进入Word解析流程:
javascript复制function isWordHtml(html) {
if (!html) return false;
var score = 0;
// 特征1:微软Word命名空间
if (/urn:schemas-microsoft-com:office:word/i.test(html)) {
score++;
}
// 特征2:出现w:WordDocument或mso-开头的样式
if (/<w:WordDocument|mso-[a-z-]+\s*:/i.test(html)) {
score++;
}
// 特征3:Office条件注释
if (/<!--\[if gte mso/i.test(html)) {
score++;
}
// 特征4:Mso开头的class,比如MsoNormal、MsoListParagraph
if (/class="[^"]*\bMso[A-Z][^"]*"/.test(html)) {
score++;
}
return score >= 2;
}
为什么用“分数”而不是“命中一个就判定”?因为有些HTML编辑器(比如某国产在线Word)也会包含mso-样式,但内容本身可能已经足够干净,没必要走清洗流程。分数阈值能降低误判率,让真正来自普通网页的复制粘贴不被拦截。
拦截后还要注意一个细节:UEditor的paste事件触发是异步场景,e.preventDefault()要在同步代码里执行,不能在某个异步请求回调之后才调用,否则浏览器会无视这个阻止操作。所以后面所有清洗工作最好都同步执行,或者先阻止默认行为,再做异步处理。
3.3 统一成标准结构之后再塞回编辑器
normalizeWordHtml是整个扩展的核心管道。我把它设计成按顺序执行的几个阶段,每一阶段只处理一类问题:
javascript复制function normalizeWordHtml(rawHtml) {
var html = rawHtml;
// 第一阶段:去掉XML声明、条件注释和Office包装层
html = htmlCleaner.stripOfficeBoilerplate(html);
// 第二阶段:从残留的style/class里提取有效格式
html = htmlCleaner.extractInlineStyles(html);
// 第三阶段:处理Word模拟出来的列表
html = listNormalizer.restoreNestedList(html);
// 第四阶段:处理表格属性
html = tableNormalizer.normalize(html);
// 第五阶段:移除空段落、空span和多余命名空间
html = htmlCleaner.finalClean(html);
return html;
}
先去掉Office包装层,再做列表重建,最后做表格补丁。顺序不能反:如果先处理表格,表格内部那些带着mso-list的单元格内容会被列表正则误伤;如果最后才去剥Office声明,前面正则匹配结构时又会因为注释干扰而失败。
插回编辑器时,我的做法不是直接调用editor.execCommand('insertHtml'),而是先临时注册了一个自定义命令insertNormalizedHtml。原因在2.2里提过:让这个过程进入UEditor的undo栈。命令体内部还是调用官方插入逻辑,但封装一层后可以在插入前后记录光标位置,避免执行完命令后光标跑到文档开头。
javascript复制editor.registerCommand('insertNormalizedHtml', {
execCommand: function (cmdName, content) {
// 用一个临时div装载清洗结果,避免直接传字符串引起的潜在解析差异
var holder = document.createElement('div');
holder.innerHTML = content;
// 这里会经过UEditor后续的过滤,但我们交出去的内容已经是标准HTML了
editor.execCommand('insertHtml', holder.innerHTML);
return true;
}
});
4. Word版本差异化解:不同Office产物如何收口
4.1 识别版本元信息,但别迷信版本号
很多人拿到Word HTML后会第一时间去找<w:WordDocument>里的版本号。确实能看到类似Word.Document.8、Word.Document.12、Word.Document.15这样的ProgId,但它们并不完全可靠。
Word.Document.8对应Word 2003及更早;Word.Document.12对应Word 2007;Word.Document.15对应Word 2013;Word.Document.16是Word 2016/2019/365。
问题在于,WPS也会复制出带Word.Document字段的内容,版本号可能是8,但实际生成HTML特征更接近新版Office。浏览器在网页端使用Office Online时,版本特征又可能完全不同。所以我的建议是:版本号只用来做日志和统计数据,判断逻辑不能依赖这个字段,而是依赖“出现在HTML里的标签结构特征”。
比如,如果一个HTML里同时出现了w:ListInfo和mso-list,说明列表是用段落模拟的,需要走完整列表重建;如果HTML里是标准的<ul><li>,那就让UEditor的默认规则处理即可。按特征收口,比按版本号适配要稳得多。
4.2 列表与编号:最容易错乱,必须主动重建
Word的列表模拟,早期版本和现代版本差别很大。很多docx文档复制出来后,Word并不会给你一个干净的<ol>,而是每个列表项都是一个<p>,带着类似这样的样式:
html复制<p class="MsoListParagraph" style="mso-list:l1 level1 lfo1; text-indent:-18.0pt;">
<span>•</span>
<span>第一层内容</span>
</p>
这种结构在Word客户端里能靠mso-list和lfo1这样的内部标识渲染出自动编号,但到了网页上,UEditor拿到的是一个普通的<p>。CSS里根本没有.MsoListParagraph的定义,内容自然就失去列表外观。
list-normalizer干的事情就是识别出这些“假列表段落”,把它们转成真正的嵌套<ul>/<ol>。实现思路是先拿到所有段落,解析每段的mso-list:l{listId} level{level} lfo{listNumber},以{listId}:{level}作为分组键,再按顺序构建DOM树。
javascript复制function extractListInfo(p) {
var style = p.getAttribute('style') || '';
var match = style.match(/mso-list:\s*l(\d+)\s+level(\d+)\s+lfo(\d+)/i);
if (!match) return null;
return {
listId: match[1],
level: parseInt(match[2], 10),
lfo: match[3]
};
}
拿到列表信息后,再判断这个列表是有序还是无序。判断来源很关键:如果段落里出现的bullet симв是•或者•,那通常对应无序列表;如果是数字编号,就需要识别list-style-type;但有时候Word是手动输入“1.”等字符,而不是真正的自动编号,这时候没法强转,只能看上下文。不过在实际业务里,90%的用户都希望保留Word里看到的那个层级结构,所以在不确定时,我倾向于转成有序列表,这样看起来更像原文档。
重建列表时最容易踩的坑是“分段插入”。如果一个列表在Word里分了好几页,剪贴板HTML中间会出现<br>或者</p><p>的断点,直接把所有mso-list段落塞进一个<ul>后,后续正文也会被吞进列表里。所以list-normalizer的重建逻辑必须同时记录“当前列表是否结束”:
- 遇到带
mso-list的段落,开始构建列表; - 遇到不带
mso-list的段落,关闭当前列表; - 如果两个相邻列表段的
lfo相同、level不同,保持嵌套关系。
4.3 表格与图片:别被Word的花活带偏
Word粘贴的表格,普遍不带border属性,边框是通过<table class="MsoTableGrid" style="border-collapse:collapse; mso-yfti-tbllook:1184; mso-border-alt:solid windowtext .5pt; ...">实现的。这行样式一旦被UEditor默认过滤剥掉,表格就变成看不见边框的“隐形表格”。
我在table-normalizer里做的工作很简单:遍历所有<table>,检测style里是否含有solid和颜色值,如果有,就补一个可配置的默认边框类。这样并不会把所有表格都画成黑框,但能保证编辑上传表格后,至少能看出表格结构。
图片的问题则更头疼。Word 2010之后,复制小图进剪贴板时通常会把图片以data:image/png;base64的形式嵌到HTML里,这本来很省事,但大图片会把HTML撑到几MB。有一次用户粘贴了一张截图,最后生成的HTML里光图片base64就占了4MB,前端页面加载直接卡顿。处理方案是:在组件里对<img>做一遍扫描,如果发现base64长度超过阈值,就把图片数据抽出来转成Blob,上传到服务器的临时图片接口,再把src替换成上传后的URL。这个动作通常是异步的,所以需要在拦截paste时先阻止默认插入,把图片异步上传完以后,再一次性地把HTML插回去。这里要注意,异步上传期间不能让用户继续输入,否则光标位置和内容会被第二次插入干扰。
5. 兼容性踩坑实录:我掉进去过的五个位置
5.1 Chrome与Safari的剪贴板行为差异
不同浏览器对于剪贴板事件的实现,对Word粘贴处理有巨大影响。Chrome里复制Word内容后,clipboardData.types会包含text/html,而且内容很完整,图片、表格样式基本都在。但Safari的paste事件在有些版本中只给text/plain,不直接暴露text/html。这时候如果你拿不到text/html,就只能放弃自定义清理,让浏览器默认粘贴,最终内容通常只剩下纯文本,格式全丢。
针对Safari,我试过几种方案。比较有效的一种是:当clipboardData拿不到text/html时,不去拦截paste事件,让UEditor自行处理,同时添加一个“从Word粘贴”的工具栏按钮作为备用入口。按钮会唤起一个隐藏的textarea或contenteditable区域,用户自行粘贴后,组件再从那个临时区域里读取HTML。这样虽然多了一步用户操作,但至少格式不会丢。
另外,Chrome在2023年后对非安全上下文(http://而非https://)的剪贴板读取权限收紧了很多。如果你测试时发现组件在localhost正常,但部署到内网IP地址之后就不触发了,可以先看浏览器是否允许clipboard-read权限。没有HTTPS的内网环境,建议优先使用备用按钮方案。
5.2 修订模式插入的“红色文字”要不要留
Word的修订模式下,一段被修改过的文字会被包裹成带特殊标记的内容。直接复制到剪贴板后,这些修订痕迹有时会变成颜色标记或者<ins>/<del>标签,有时则是普通的带红色样式的<span>。
很多内容管理场景下,编辑希望粘贴的是“最终版”,而不是带着红色删除线和批注的“修订稿”。但在一些合同、法务场景里,批注和修订痕迹又是必须保留的。这个逻辑不能写死在组件里,我在index.js里加了一个配置项:
javascript复制var DEFAULT_OPTIONS = {
keepTrackChanges: false, // 是否保留修订痕迹,默认不保留
keepComments: false // 是否保留批注,默认不保留
};
当keepTrackChanges为false时,组件会把<ins>标签里的内容视为最终内容,去掉所有变色样式,把<del>直接移除,保持文本干净地插入编辑器。反之则保留这些标签。这个需求如果一开始就走“全部剥离样式”的路线,后面会很难补。
5.3 XSS白名单与内容安全边界
每次把HTML插回UEditor之前,都要重新过一遍白名单。Word本身不会产生<script>,但Word文档里可以插入链接,链接的href可能是javascript:协议。尤其在从网页复制到Word再复制出来的文档里,这种危险链接很容易被“洗白”成普通HTML。还有一类情况是员工从营销邮件里复制内容进入Word,再把邮件里的跟踪像素或者隐藏标签带进来。
我不会只依赖UEditor自带的XSS过滤,而是在html-cleaner.js里做一次DOM级别的安全检查:
- 通过
DOMParser解析清洗后的HTML; - 遍历所有
<a>,把所有不是http:、https:、mailto:、tel:的href清空; - 遍历所有元素,删除
on*事件属性; - 删除所有
<iframe>、<object>、<embed>、<form>标签; - 如果配置了
filterStyle,还会检查style里的expression(...)、url(javascript:...)等危险写法。
做完这些再交给UEditor,相当于把“Word转换层”和“XSS过滤层”隔离开。Word那套转换只管“格式翻译”,安全由专门的清洗层负责,边界清晰了,出问题也好排查。
5.4 空行黑洞:<o:p>和<p class="MsoNormal">批量生成空段
Word生成的HTML里有个很招人烦的东西:<o:p></o:p>。它代表Word里的一些“空对象占位”,在网页上没有任何可见内容,但会占据段落位置。如果用户每敲一个回车就产生一个这样的空段落,粘贴后编辑器里会多出一大堆空<p><br></p>,导致整篇文章行距异常。
清除这类空段落的逻辑很简单,但要小心别把真正的内容段删掉。我的做法是:先用正则粗略替换掉<o:p>,再用DOM遍历删除那些“没有子元素、没有内容、没有类名中有MsoNormal之外的标记”的段落。听起来简单,但实际做的时候要注意不要误删那些“以图片为唯一内容”的段落,因为图片是<img>节点,它算有子节点。
javascript复制function removeEmptyParagraphs(container) {
var paragraphs = container.querySelectorAll('p');
Array.prototype.forEach.call(paragraphs, function (p) {
if (!p.textContent.trim() && !p.querySelector('img, table, ol, ul')) {
p.parentNode && p.parentNode.removeChild(p);
}
});
}
5.5 跨域代理下图片无法显示
Word文档里的图片如果原本来自网络链接,复制进HTML后可能会保留http://内部资产库/xxx.png这样的远程地址。开发环境下你可能是localhost:8080,图片服务器在另一个域名,编辑器里可能没问题,但用户保存到正式环境后,这些图片链接如果不在后端配置的白名单里,就会全部裂开。
这里没有万能解法,组件能做的,是把所有非data:协议的图片地址都收集出来,交由后端判断是否转存。我曾经踩过这个坑:本地跑得好好,上传到测试环境后图片集体消失,排查了半天才发现问题不在UEditor,而在后端接口的图片域名白名单。
6. 多走一步:把上传的docx文件解析成可编辑正文
6.1 复制粘贴之外的刚需:纯客户端解析docx
做内容管理系统时,除了复制粘贴,还有一个高频入口:编辑手里有一篇写好的docx文件,希望“上传Word文档后自动变成编辑器里的正文”。这个需求本质上是另一个“版本兼容”任务,得从docx压缩包里把word/document.xml拿出来解析。
直接在浏览器端解析docx是可行的,因为docx本质是一个ZIP包,里面主体文本是XML,用JSZip读取后转HTML即可。相比从剪贴板拿HTML,解析docx反而更稳定,因为它不受浏览器和Office版本影响,拿到的XML结构非常规范。唯一的问题是转换工作量不小,还要处理图片、目录、分页符、页眉页脚等一堆东西。
6.2 mammoth.js + 自定义清理器的双保险
为了避免从零解析WordprocessingML,我直接用了mammoth.js的浏览器版本。它能把docx转换成较干净的HTML,对标题、列表、表格都有基础支持。但实测下来,mammoth输出的HTML比较“素”,比如标题只是普通<p>加粗而不是<h1>,列表样式也不一定复合编辑器的皮肤。所以我的做法是两层配合:
- 用mammoth把docx解成HTML;
- 把这段HTML交给前面写的
normalizeWordHtml,复用list-normalizer和table-normalizer做二次加工。
代码大致长这样:
javascript复制var mammoth = require('mammoth/mammoth.browser.js');
async function handleDocx(file) {
var arrayBuffer = await file.arrayBuffer();
var result = await mammoth.convertToHtml({ arrayBuffer: arrayBuffer });
var normalized = normalizeWordHtml(result.value);
editor.execCommand('insertNormalizedHtml', normalized);
}
mammoth会把docx里很多语义化内容处理好,但它默认不处理Word里那些乱糟糟的mso-样式。而normalizeWordHtml正好补齐这一环,特别是列表和表格。两者结合,比你只挂一个mammoth再手工修样式要省力很多。
6.3 场景取舍:前端解析不是所有时候都合适
这一套在浏览器端解析的方案,最大的优势是不用把文件先传到后端再等待转换。但有一个致命前提:前端运行环境必须允许JSZip解压大文件。如果一篇docx有几十个高清截图,解压和转HTML的过程会让页面卡死好几秒,移动端尤其明显。
我在生产环境里加了限制:超过10MB的docx文件不直接前端解析,提示用户另存为PDF或者拆分成小文件。如果你们后端的文档处理中间件已经能转HTML,那更稳妥的路线是“后端转换,前端只负责接收纯HTML”,前端组件只需要处理后续的列表重建和样式收口就够了。
开发这套wordCompat组件的过程中,我的最深体会是:Word版本兼容处理容易陷入“越适配越多规则”的死胡同。真正让项目稳定下来的,是先把问题收敛到“统一标准HTML”这个出口,再让UEditor自身的过滤链处理剩余的安全与样式问题。以后不管来了什么新版本的Office,只要它还在走“剪贴板生成HTML”这一条路,都可以先过一遍isWordHtml和normalizeWordHtml,不需要为每个版本专门写一套Adapter。如果你也在维护带UEditor的内容编辑后台,不妨先从isWordHtml识别和htmlCleaner两层开始搭,处理真实文档库里的几篇经典稿子,再接列表重建和表格兜底,后面会顺手很多。
