距离我上一次被“样式丢失”这个需求折腾到抓狂,其实还不到两个月。事情本身不复杂:用户把一份排好版的Word文档复制到后台富文本编辑器,点发布,结果正文里的标题、加粗、颜色、行距全部变成默认值,表格线也断了大半。最尴尬的是,测试环境怎么复现都正常,最后跑到用户电脑前才发现,同样是“Ctrl+C / Ctrl+V”,Windows版Word和Mac版WPS送出来的剪贴板数据完全不是一个东西。
这个问题的搜索热度一直不低,说明它不只是接入层没写好,而是很多前端团队第一次做富文本功能时都会踩的同一个坑。它看起来是样式问题,本质上是数据格式问题、清洗策略问题和编辑器底层API问题三件事纠缠在一起。这篇文章我把完整的排查思路和一套已验证的清洗流程写出来,希望能帮你少走点弯路。
1. 先把“样式丢失”拆成四种不同症状,再谈方案
1.1 剪贴板里的Word内容并不只有一个“版本”
在处理“Word转存编辑器”之前,必须建立一个认知:从Word按Ctrl+C之后,剪贴板里放进去的不是一个“纯文本文件”,而是同时投放了多种格式的数据。Windows和macOS的剪贴板机制都支持多格式并存,浏览器读到的其实是其中一部分。
最常见到的三种格式:
| 格式 | 内容 | 浏览器读取方式 |
|---|---|---|
| text/plain | 去掉了所有格式的纯文本 | clipboardData.getData('text/plain') |
| text/html | 带HTML标签的富文本结构 | clipboardData.getData('text/html') |
| Files | 图片、附件等二进制对象 | clipboardData.files |
很多前端一上来就调getData('text/html'),拿到什么就插什么,这是第一个坑。因为不同浏览器对剪贴板格式的暴露策略不一样:Windows Chrome返回的HTML通常是完整的一段文档,里面带着微软Office专用的命名空间;macOS Safari有时把text/html字段裁剪得很少,甚至数据为空,只剩text/plain可用;还有的浏览器会把Word里插入的图片从HTML引用中剥离,独立放到Files里。
所以第一步永远是先打印剪贴板内容,确认数据长什么样:
document.addEventListener('paste', (e) => { const cd = e.clipboardData || window.clipboardData; if (!cd) return; console.log('types:', [...cd.types]); for (const type of cd.types) { const text = cd.getData(type); console.log('=== ' + type + ' ==='); console.log(text ? text.slice(0, 500) : '(empty)'); } console.log('files:', cd.files.length); });这段排查代码建议直接留到项目里,线上问题定位会非常有用。“样式全丢了”和“只有样式丢了”是两种完全不同的故障,前者往往是数据源没有HTML可用,后者才是清洗逻辑的问题。
1.2 四种常见丢失症状和它们的真实成因
不同用户说的“样式丢失”,背后的原因经常不是同一件事。我自己把这些反馈拆成四种症状,方便对照定位:
| 用户看到的现象 | 真正原因 |
|---|---|
| 字体、颜色、字号全部变成默认 | 编辑器只拿到了text/plain,或者清洗层把style直接删光了 |
| 文字内容在,但段落缩进、行距、标题层级没了 | Word用CSS类名和内联样式混合控制排版,清洗时丢了类名对应的样式表 |
| 表格结构还在,但列宽、边框、合并单元格错乱 | Word生成的HTML里包含大量colgroup、固定pt宽度、mso边框属性,普通浏览器渲染规则不兼容 |
| 图片显示不出来,或者发布后变成一串神秘base64 | 图片以Files对象或二进制格式存在,HTML里只有一个引用占位,没有走图片上传流程 |
| 公式变成乱码或图片 | Word公式用的是OMML/MathType对象,HTML里无法直接映射 |
这里面最容易被误判的是第一种。如果你在清洗代码里用了innerHTML.replace(/<style[\s\S]*?>/gi, ''),那Word嵌在head里的样式表确实被删了,但正文的内联font-family、font-size也被某些粗放的正则顺带清掉了。用户看到的结果就是“样式丢了”。
1.3 判断粘贴来源的简单指纹
处理之前先识别来源,这一步不是强迫症,而是为了采用不同的清洗策略。Word、WPS、普通网页复制的HTML结构差异非常大,用同一套规则去洗,一定会出现要么洗太狠、要么洗不干净的结果。
识别方法很简单,在HTML字符串里找特征:
function detectPasteSource(html) { if (!html) return 'plain'; if (/class="?Mso|urn:schemas-microsoft-com:office:office|\/\/\[if gte mso/i.test(html)) { return 'word'; } if (/WPS|wps\.cn/i.test(html)) { return 'wps'; } if (/<html[\s\S]*?<body/i.test(html)) { return 'web'; } return 'unknown'; }这个函数我放在工具库最前面,所有后续的样式映射逻辑都围绕它展开。Word和WPS虽然都是国产办公环境高频出现的,但WPS生成的私有标签和Word并不完全一样,需要单独处理。
2. 为什么“原样粘进去”和“纯文本粘进去”两条路都走不通
2.1 原样粘贴:浏览器会留下一堆你看不见的私人订制
把Word生成的HTML直接塞进编辑器,表面上是保留了样式,实际上等于把一个带私有命名空间的文档硬塞进Web页面。Word输出的HTML里充满了类似这样的标记:
<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns:v="urn:schemas-microsoft-com:vml"> <head> <!--[if gte mso 9]><xml><w:WordDocument>...</w:WordDocument></xml><![endif]--> <style> @page WordSection1 {size:595.3pt 841.9pt; margin:72.0pt 72.0pt 72.0pt 72.0pt;} p.MsoNormal, li.MsoNormal, div.MsoNormal {margin:0cm; margin-bottom:.0001pt; ...} </style> </head> <body> <p class="MsoNormal" style="text-indent:24.0pt;"> <span style="font-size:14.0pt;font-family:'等线';color:#333333;">内容</span> </p> </body> </html>注意这里的几个特征:o:p这种标签在HTML5里根本不存在,MsoNormal这类类名没有任何全局CSS定义,pt单位在浏览器里虽然能识别,但如果你没有对应的CSS规则,这些类名就是一堆死代码。
直接插入的结果是:页面上看起来也许正常,因为部分内联样式还能生效,但一旦编辑器需要把内容转存到另一个平台,比如生成公众号文章、导出PDF、同步到CMS,这些私有标签就会原形毕露。典型的例子就是Word表格里的列宽设置,它依赖colgroup里的width值和table-layout: fixed,脱离Word环境后经常出现“列宽无法拖动”的诡异现象,这在热搜词里也频繁出现。
2.2 纯文本粘贴:样式保住了,语义丢了
另一种极端是只读text/plain,禁止一切HTML进入。这种做法的好处是绝对安全、绝对不会出现XSS,也不会看到任何私有标签,缺点同样致命:用户从Word复制一个带标题、列表、加粗、有序层级的内容进来,全部变成一段没有结构的文字。
这时候用户反馈同样是“样式丢失”,但和第一种不同的地方在于,这次连语义都没了。前端可以把它包装成“用纯文本模式粘贴”,但对编辑者来说,他的排版意图全丢了,等于整个复制动作白做。
我之前的团队就有一版这样的实现,产品经理在验收时把一个多级列表粘进去,发现编号全部消失,当场就否了。纯文本只能作为兜底,不能作为主方案。
2.3 编辑器自带清理为何经常帮倒忙
现在很多富文本编辑器都自带粘贴过滤,比如TinyMCE、CKEditor、wangEditor、Quill都有所谓的“清除格式”机制。但它们针对的是常规网页复制场景,Word的私有结构并没有被完整覆盖。
举几个我见过的实际表现:
- 有些编辑器会把Word的
o:p标签保留成空段落,导致粘贴后出现大量空白行; - 有些编辑器会把Word的
mso-spacerun:yes处理成一连串 ,删除文字时出现“一个字符一个空格”的奇怪现象; - 有些编辑器自带规则会删除所有
style,只留下strong、em这类语义标签,导致字号颜色全部丢失。
这些行为的本质是编辑器的内置清理器不够了解Word的私有约定。它不是完全不能用,但直接默认开启,效果就是“时灵时不灵”。所以下面这种自定义清洗流程,在很多项目里是绕不开的一步。
3. 一套可落地的清洗流程:拦截、解析、白名单重建
3.1 第一步:在paste事件做数据源分流
清洗流程的入口是拦截paste事件,然后根据数据源类型走不同的分支。这里的关键点是不要阻止默认行为之前就做异步操作,否则剪贴板数据会失效。
我习惯的写法是这样的:
editorRoot.addEventListener('paste', async (e) => { const cd = e.clipboardData || window.clipboardData; const html = cd.getData('text/html'); const plainText = cd.getData('text/plain'); const source = detectPasteSource(html); if (source === 'word' || source === 'wps') { e.preventDefault(); const cleaned = cleanWordHTML(html); insertIntoEditor(cleaned); } else if (html) { // 普通网页复制,按编辑器默认规则处理 return; } else if (plainText) { e.preventDefault(); insertIntoEditor(escapeHtml(plainText)); } });这里有个容易踩的细节:e.preventDefault()必须在读取剪贴板之后、且在同步代码里调用。如果你先await了一个上传图片的接口再阻止默认行为,某些浏览器会直接清空剪贴板内容,导致getData拿到空串。
3.2 第二步:用DOMParser剥掉微软私有标记
拿到Word的完整HTML字符串之后,我不会用正则去做全局替换,而是先用DOMParser把它变成真实的DOM树,再做元素级清洗。正则处理嵌套标签太容易出错,比如<span style="...">里的style值可能包含>字符,或属性值里有引号,都可能导致一次性match失败。
基本的清洗函数骨架:
function cleanWordHTML(html) { const doc = new DOMParser().parseFromString(html, 'text/html'); // 删掉微软专用的命名空间声明 document.querySelectorAll('*').forEach(el => { [...el.attributes].forEach(attr => { const name = attr.name.toLowerCase(); const value = attr.value || ''; // 剥掉 xmlns:o / xmlns:w / xmlns:v 等 if (name.startsWith('xmlns:')) { el.removeAttribute(attr.name); } // 剥掉所有 mso- 开头的属性或值 if (/^mso-/.test(name) || value.includes('urn:schemas-microsoft-com')) { el.removeAttribute(attr.name); } // Word 会在 class 里写 MsoNormal、MsoHeader、MsoListParagraph if (el.className && /Mso|WordSection|ms-/.test(el.className)) { el.className = el.className .split(/\s+/) .filter(c => !/Mso|WordSection|ms-/.test(c)) .join(' '); } }); }); // 处理特定标签 doc.querySelectorAll('o\\:p, w\\:p, o:p, w:p').forEach(node => { const p = doc.createElement('p'); while (node.firstChild) p.appendChild(node.firstChild); node.replaceWith(p); }); // 处理 Word 的连续空格占位 doc.querySelectorAll('[mso-spacerun="yes"], span[mso-spacerun]').forEach(node => { node.replaceWith(doc.createTextNode(node.textContent)); }); // 清理空span doc.querySelectorAll('span').forEach(span => { if (span.childNodes.length === 0 || span.textContent.trim() === '') { span.remove(); } else if (!span.getAttribute('style') && !span.getAttribute('class')) { span.replaceWith(doc.createTextNode(span.textContent)); } }); return doc.body.innerHTML; }这里的核心逻辑是:先清理命名空间和属性级的私有标记,再处理标签级的私有标记,最后压缩空节点。顺序很重要,如果先删空span再处理o:p,有些内容会被误删。
3.3 第三步:按需求映射Word样式到编辑器协议
清洗掉私有标记之后,还有一个核心问题没有解决:Word的样式体系和浏览器/编辑器的样式体系不是一一对应的。这里需要做一个“映射决策”,我列一个常用映射表:
| Word / MS私有表现 | 推荐处理方式 | 说明 |
|---|---|---|
字体font-family:'等线', '宋体', sans-serif | 保留为内联样式,但补一份通用字体栈 | 用户本机字体≠读者本机字体,需要兜底 |
字号font-size:14.0pt | 转成14px或换算成em | 如果目标是打印,保留pt;如果是H5展示,转px |
首行缩进text-indent:21.0pt | 转成text-indent:2em或21px | 视觉一致性更好,也适配移动端 |
行距line-height: 150% | 保留百分比 | 浏览器支持良好 |
主题色mso-themecolor | 提取对应的十六进制色值 | Word主题色在网页上没有定义,必须落到具体色值 |
表格width:415.3pt | 转成百分比或auto | 固定pt宽在响应式布局下会撑破页面 |
边框border:.5pt solid windowtext | 转成1px solid #000或按需简化 | windowtext不是合法CSS颜色 |
单位换算这部分,如果项目里用的是px体系,我在工具函数里写了一个简化版的pt转px:
function ptToPx(val) { const num = parseFloat(val); if (isNaN(num)) return val; // 浏览器标准:1pt = 4/3 px return Math.round(num * 4 / 3) + 'px'; }把Word的—.0pt统一转成px之后,编辑器和最终渲染端对样式的解析就没有歧义了。这段映射逻辑取决于你的目标场景,如果是做仿A4纸打印,那保留pt反而更好。关键在于“先明确目标,再决定映射规则”,不要一刀切。
3.4 第四步:把结果交给编辑器并防止二次粘贴
清洗完的HTML最终要插入编辑器。不同编辑器的插入API不同,但思路大同小异:
// 原生contenteditable function insertIntoEditor(html) { const sel = window.getSelection(); if (sel && sel.rangeCount > 0) { const range = sel.getRangeAt(0); range.deleteContents(); const frag = range.createContextualFragment(html); range.insertNode(frag); } } // wangEditor 或类似编辑器 // editor.insertHTML(cleanedHTML) // Quill // quill.clipboard.dangerouslyPasteHTML(cleanedHTML) // TinyMCE // editor.execCommand('mceInsertContent', false, cleanedHTML)有个细节:清洗后插入会触发新的DOM操作,某些编辑器会再次执行粘贴过滤器。所以要在插入前设置一个标志位:
let isHandlingPaste = false; editorRoot.addEventListener('paste', (e) => { if (isHandlingPaste) { e.preventDefault(); return; } isHandlingPaste = true; try { // 清洗并插入 } finally { setTimeout(() => { isHandlingPaste = false; }, 0); } });这个标志能避免“插进去的内容又被编辑器二次清洗”的问题,尤其是Quill这类会监听DOM变化的编辑器,不加这层保护很容易出现粘贴内容再被处理一遍的情况。
4. 哪些场景直接用编辑器内置能力,哪些必须自己动手
4.1 常用编辑器对Word粘贴支持的现状
如果项目只是简单后台,文档格式不复杂,那么优先使用编辑器自带能力是更理性的选择,自己维护清洗函数是有成本的,尤其在后续迭代中很容易因为边界case反复修补。
几个主流编辑器的情况:
| 编辑器 | 对Word粘贴的内置支持 | 适用场景 |
|---|---|---|
| TinyMCE | paste插件提供paste_word_valid_elements等配置,能处理大部分常规标签 | 只需要基础标题、段落、列表、加粗的团队 |
| CKEditor 5 | 官方提供Paste from Word插件,对Word样式映射做得比较细 | 需要保留色彩、字体、表格形状的中后台系统 |
| Quill | clipboard模块有matcher机制,但没有专门针对Word的完整方案 | 定制性强,但需要自己补一套Word过滤器 |
| wangEditor | 内置了基础的粘贴过滤,复杂表格仍会出问题 | 国内项目用的多,适合快速交付 |
这些编辑器内置的方案能解决80%的“样式丢失”问题。那剩下的20%恰恰是导致用户崩溃的场景:合并单元格、图片跨域、公式、多级列表编号、文档目录域。
4.2 何时要自己维护过滤函数
我个人的判断标准很简单:看你的产品有没有“样式必须严格还原”的需求。如果用户从Word复制了一份合同,里面表格有复杂的合并单元格、固定列宽、页眉页脚,那编辑器内置插件基本撑不住。
这种情况下,建议自建清洗流程,但不要把清洗函数挂在页面里,而是放到独立的utils/paste-sanitizer.ts模块里,方便单元测试。这个模块至少包含下面这些函数:
detectSource(html):判断数据来源;cleanWordHTML(html):剥离私有标记;mapWordStyles(html):统一单位、颜色、字体栈;sanitizeTags(html):白名单标签过滤,删除script、iframe、object等危险节点;stripEmptyNodes(html):清理空段落和多余空格。
值得划清边界的是:样式清洗和XSS过滤是两件事,不要合并成一个函数。样式清洗的职责是把Word结构转成可接受的HTML,XSS过滤的职责是确保任何情况下都没有可执行脚本进入页面。两者偶尔会重叠,比如<a href="javascript:...">既是样式问题也是安全问题,但它们的处理逻辑完全不同。
4.3 前端过滤和后端消毒,职责怎么划分
我在团队内部一直强调:前端清洗是体验,后端消毒是底线。前端做得再好,也不能保证所有编辑器都走同一套流程。某些用户可能绕过前端直接调接口提交内容,所以后端必须再做一次严格的标签白名单过滤。
常见分工是:
| 层 | 关注点 | 工具参考 |
|---|---|---|
| 前端 | 样式映射、单位换算、图片上传、粘贴体验 | 自定义清洗函数、DOMParser |
| 后端 | XSS防护、非法标签删除、属性白名单 | Java可参考OWASP Java HTML Sanitizer,Node可参考DOMPurify的服务端版 |
这里尤其要警惕的是,只依赖前端清洗会留下一条安全漏洞链路。Word粘贴的HTML可以携带<iframe>、<object>、javascript:协议链接,甚至通过data:协议嵌入内容。浏览器自己会拦一部分,但拦不住的场景比你想象的多。
一个具体例子:
<p style="text-indent:21.0pt;"> <a href="javascript:alert(1)">点我领取奖品</a> </p> <img src="x" onerror="alert(document.cookie)">如果清洗函数只看src和href而不检查协议,这两条都能造成实际风险。白名单方案是:标签白名单确认、属性白名单确认、协议白名单只允许http和https,最后再把on*开头的属性全部删掉。
5. 上线后最容易翻车的四个边界场景
5.1 图片粘贴变成base64,导致文档体积爆炸
Word里插的图片,在剪贴板里通常有两种存在形式:一种是直接嵌在HTML里的base64字符串,另一种是放在clipboardData.files里的二进制文件。前者会让整段HTML变得巨大,一个2MB的图片经过base64编码后约等于2.7MB的文本,这还没算其他格式的重复存储。
处理图片的正确方式是:在paste事件里捕获getAsFile(),提前上传到对象存储,然后用返回的URL替换HTML里的图片引用:
for (const item of cd.items) { if (item.type && item.type.startsWith('image/')) { const file = item.getAsFile(); const url = await uploadFile(file); // 上传到你的CDN或OSS // 拿到url后,替换HTML中对应的临时img占位 } }不处理这个问题的后果是内容提交时体积爆炸,接口超时、上传失败都是家常便饭。之前我们线上出现过一次事故,用户粘贴了一份带十张截图的Word进公告编辑器,提交数据接近30MB,网关直接拒收。
5.2 Word表格粘贴后列宽无法拖动
热搜词里“word 表格列宽无法拖动”说明这个问题绝对不是个例。Word自动生成的表格HTML长这样:
<table class="MsoNormalTable" style="width:415.3pt; border-collapse:collapse; table-layout:fixed;"> <colgroup> <col style="width:92.15pt"> <col span="2" style="width:77.8pt"> </colgroup> <tbody> <tr> <td style="width:92.15pt; border:solid windowtext 1.0pt;">...</td> </tr> </tbody> </table>问题在于table-layout: fixed和colgroup的固定pt宽度。网页表格的列宽在fixed模式下完全由第一行决定,用户想拖拽调整时发现拖不动,或者拖了之后表格马上错乱。
我的处理方式是在清洗阶段把colgroup直接移除,把table上的固定宽度清掉,让表格进入auto模式:
doc.querySelectorAll('table').forEach(table => { if (table.getAttribute('style')) { let style = table.getAttribute('style'); style = style.replace(/width:[^;]+/gi, ''); style = style.replace(/table-layout:\s*fixed/gi, 'table-layout: auto'); if (style.trim()) { table.setAttribute('style', style); } else { table.removeAttribute('style'); } } table.querySelectorAll('col, colgroup').forEach(col => col.remove()); });这样处理之后,表格宽度变成自适应,编辑器里也能正常拖动列宽。代价是原文档的精确版面比例会被打破,但从Web展示的角度看,反而更符合响应式需求。
5.3 样式“时灵时不灵”:字体、主题色、特殊符号的坑
有一个很经典的坑:用户说“我在Word里明明把标题设成了红色,粘到编辑器却变成黑色”。排查后发现,Word里设置的颜色不是具体色值,而是主题色。主题色在HTML里的表现是:
<span style="color:#C00000;mso-themecolor:accent1;">如果没有读到mso-themecolor对应的色值,只看到#C00000,那确实能正常显示。问题出在另一种情况:Word生成HTML时,color:#C00000没有落到内联style里,而是放在了一段公共样式表中,加了一个类似WPS的随机类名。编辑器只保留了标签,没保留类名对应的CSS,于是颜色就“凭空消失”了。
解决思路是:如果检测到mso-theme开头的属性,就用Word主题色映射表手工转成十六进制色值。这个映射表网上有公开数据,也可以让后端在解析阶段统一转换。
字体“时灵时不灵”的坑更隐蔽。Word文档常用的字体如“等线”“微软雅黑”“宋体”,用户本机有,但发布之后读者本机未必有。内联样式写成font-family:'等线';在Windows上正常,在macOS或手机上就会回退到默认字体,用户同样会理解为“样式丢了”。所以清洗时我习惯把中文字体栈补全:
font-family:'Microsoft YaHei','PingFang SC','Hiragino Sans GB','sans-serif';这样至少能保证不同设备上的视觉落差不会太大。
5.4 安全风险:Word粘贴内容离XSS只差一步
上面提到过,Word生成的HTML里有命名空间、有注释条件块、有object标签,天然就是HTML注入的温床。更麻烦的是,用户从网页复制一段内容再粘到Word,再复制出来,这个过程中浏览器的解析机制可能把原本安全的HTML重新编码成诡异的结构,里面的script不一定能按原样活下来,但iframe、javascript:协议链接完全可能残留。
所以清洗函数必须做成白名单制,而不是黑名单制。黑名单是“我知道哪些危险,所以删掉哪些”,但Word粘贴产生的标签变体太多了,根本列不完。白名单是“我只留下列表里的标签和属性,其他全部删掉”。
一个最小的白名单配置:
const ALLOWED_TAGS = new Set([ 'p', 'div', 'br', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'table', 'thead', 'tbody', 'tr', 'td', 'th', 'img', 'a', 'strong', 'em', 'u', 's', 'blockquote', 'pre', 'code' ]); function sanitizeNode(node) { if (node.nodeType === Node.ELEMENT_NODE) { const tag = node.tagName.toLowerCase(); const allowedAttrs = new Set(); if (!ALLOWED_TAGS.has(tag)) { node.replaceWith(document.createTextNode(node.textContent)); return; } ['src', 'href', 'alt', 'title', 'style', 'colspan', 'rowspan'].forEach(attr => { if (node.hasAttribute(attr)) { allowedAttrs.add(attr); } }); // 属性级过滤 if (node.tagName === 'A') { const href = node.getAttribute('href') || ''; if (!/^https?:\/\//i.test(href) && href !== '#') { node.removeAttribute('href'); } } if (node.tagName === 'IMG') { const src = node.getAttribute('src') || ''; if (!/^https?:\/\/|^data:image\//i.test(src)) { node.removeAttribute('src'); } } ['onclick', 'onerror', 'onload', 'onmouseover'].forEach(evt => node.removeAttribute(evt)); } }后端必须再做一次同样级别的过滤,不要觉得前端做了就万事大吉。前端过滤的目的是给用户一个可阅读的结果,后端过滤的目的是保护读者和服务器,两者缺一不可。
6. 我的实践体会与一点小建议
如果你问我“Word转存编辑器样式丢失”这个问题到底要怎么根治,我会说没有一套代码能覆盖所有场景。Word能输出的HTML变体实在太多了,不同版本、不同语言、不同字体插件、不同表格结构,都会产生新的特征。我们能做的是把方案设计得足够健壮:数据源识别、标签白名单、单位换算映射、图片上传机制、后端二次过滤,每一层都承担一部分职责,而不是指望一个正则函数解决所有问题。
我个人踩过最值的一次坑是在排查表格边框丢失时,发现用户看到的“边框消失”不是标签问题,而是Word输出的border-color:windowtext被浏览器当成非法值丢掉了。把windowtext识别成黑色才能解决。这类边界问题,只有靠线上真实用户的粘贴数据反复喂养你的清洗函数,才会越来越稳。
最后分享一个小习惯:在所有粘贴入口加上来源指纹和关键阶段的日志输出,记录粘贴源类型、清洗前后HTML长度、删除标签数量、是否有图片上传失败。线上出了“样式丢失”的工单,不要靠猜,直接看日志定位到是读取、清洗、映射还是上传环节出了问题。这套流程修下来,再复杂的Word文档也不会让你连着加班两周了。