
KindEditor 这东西老一点的后台系统里几乎都能见到。轻量、API 清楚、中文文档齐全做个富文本编辑区域非常顺手。但只要需求里出现“把 Excel 表格粘贴进来还要保留格式”一票人就开始挠头单元格底色没了、列宽全乱、合并单元格散架甚至整个表格被拍扁成一坨纯文本。我在项目里为了这个功能前前后后折腾了好几轮最后得出的结论是——Excel 粘贴到 KindEditor 丢格式根子不在于编辑器“不能保留”而是默认粘贴流程根本没去读剪贴板里的 HTML 格式再加上 KindEditor 自身的过滤规则把样式又洗掉了一层。这篇就把“KindEditor 如何处理 Excel 数据源格式保留”这件事的完整思路、可以直接抄的代码、以及我踩过的坑一起讲清楚适合正在集成 KindEditor、被表格粘贴问题折磨的 Web 开发者参考。1. 先弄明白粘贴Excel数据时格式是怎么丢的1.1 剪贴板里其实放着好几份Excel数据要解决问题先得知道数据从哪来。当你在 Excel 里选中一段区域按下 CtrlCExcel 不是只复制了一份“网格数据”进去而是按照操作系统的剪贴板协议把同一份内容以好几种格式同时丢进剪贴板纯文本、Unicode 文本、HTML Format、RTF甚至位图。这也是为什么同一份表格粘到记事本变成制表符分隔的文字粘到 Word 却还能保留表格线条和底色。浏览器里的 paste 事件能接触到的就是event.clipboardData这个对象你可以用getData(text/html)把剪贴板里那份 HTML 格式直接取出来。我把一份带底色、带合并单元格的 Excel 表格复制后在 Chrome 控制台打印过这份text/html开头是一堆 XML 声明和命名空间往下就是完整的table结构单元格的背景色、边框、行高、列宽全都在style属性里写得好好的。换句话说格式数据在复制这一刻就已经存在了并没有丢。真正的问题出在后面浏览器把 HTML 格式交给页面页面能不能接得住、接得干净才是关键。1.2 KindEditor默认不是保留而是主动清洗KindEditor 对粘贴内容的处理逻辑核心是配置里的filterMode。它默认是true含义是按内置的htmlTags白名单把粘贴进来的 HTML 过滤一遍。这套白名单最初主要是为了处理 Word 粘贴内容设计的table、td、th虽然在里面但一大堆style属性、Excel 专有的mso-前缀样式都不在放行范围内结果就是一个单元格里的颜色、字体、宽度、边框被剥得干干净净。同时 KindEditor 还有一个pasteType参数0 表示禁止粘贴1 表示只粘贴纯文本2 表示允许带格式粘贴。很多人以为设成 2 就万事大吉但实际大多版本只是走了一套“默认带格式粘贴”逻辑并不会针对 Excel 生成的私有 HTML 做转换。等于有两层过滤叠在数据上一层是编辑器主动洗样式一层是没有针对 Excel 做样式映射。所以排查方向也清楚了一是不读 HTML 剪贴板格式二是读了也被过滤清洗掉。pasteType 值行为表现适合场景0禁止粘贴内容基本不用1只粘贴纯文本所有格式丢失留言板、评论区2允许带格式粘贴普通网页内容没问题但 Excel 私有格式会乱常规富文本编辑场景1.3 两层过滤叠加带来的典型现象如果你现在打开一个默认配置的 KindEditor粘贴 Excel 数据常见的表现有三种整个表格变成一个 Tab 分隔的纯文本这种是pasteType为 1 或者事件被某些浏览器默认处理成纯文本。表格结构还在行列没乱但所有背景色、字体颜色、边框、列宽都没了这种是filterMode把样式全剥了。表格样式部分保留但插进来一堆空白行、奇怪的o:p占位符、乱码属性这种是 HTML 里 Office 私有节点没清理直接硬塞进编辑器。这三种现象指向的修复点各不相同但大方向是一致的自己接管粘贴流程先把剪贴板里的 HTML 拿回来处理再决定什么能进、什么该扔。2. 直接可用的方案接管粘贴事件自己保留数据源格式2.1 核心思路在编辑器动手之前截胡剪贴板既然默认流程会丢那就干脆不让它处理。思路很直接自己监听paste事件从clipboardData里把 HTML 拿出来清洗掉 Excel 的私有标记映射成浏览器能正常渲染的样式再用 KindEditor 的insertHtml接口写回编辑器。这个思路本质上是在编辑器“看到”粘贴数据之前就接管了事件所以它不受filterMode是 true 还是 false 的影响也不依赖pasteType的值。KindEditor 是基于 iframe 的编辑器实际的编辑区是 iframe 里的contentDocument监听事件时不要只给 textarea 绑给它内部的 document 绑更直接事件对象里照样能拿到clipboardData。还有一点必须提醒不要尝试绕过 paste 事件主动读取剪贴板。浏览器安全模型不允许页面脚本直接访问剪贴板内容只能通过用户的真实粘贴动作去拿。网上的方案说“粘贴后自动保留格式”可行但“点一下按钮主动导入剪贴板内容”基本都行不通原因就在这。2.2 关键代码取到剪贴板里的HTML数据最基础的一段代码就是绑定 paste 事件并取数据function initExcelPaste(editor) { var doc editor.edit.doc; var handler function (e) { e e || window.event; // 老IE的事件对象在window.event上clipboardData在window.clipboardData上 var cb e.clipboardData || window.clipboardData; if (!cb) { return; } var html ; try { html cb.getData(text/html); } catch (err) { html ; } // 只有拿到的确实是Excel表格结构时才接管 if (html html.indexOf(table) -1) { e.preventDefault(); if (e.stopPropagation) { e.stopPropagation(); } var cleanedHtml cleanExcelHtml(html); editor.insertHtml(cleanedHtml); } // 拿不到HTML或不是表格就交回默认行为 }; if (doc.addEventListener) { doc.addEventListener(paste, handler, false); } else { doc.attachEvent(onpaste, handler); } }这里有个细节值得说一下为什么要判断html.indexOf(table) -1。因为要确保只在粘贴 Excel 表格时才接管避免把网页里复制的普通文本或者其他 HTML 内容也强行走清洗流程造成误伤。用editor.insertHtml()的好处是编辑器会自己处理光标位置插入结果就在用户当前焦点处符合“粘贴”的语义。特别提醒必须先preventDefault()再insertHtml()顺序反了会出现内容被插入两次——浏览器默认粘贴一次你程序又插入一次。2.3 清洗Excel私有样式mso-前缀与命名空间处理Excel 生成的 HTML 和浏览器能直接使用的 HTML最大区别在于带了一堆 Office 专用标记。最常见的有这几类标签属性里的xmlns:o、xmlns:w、xmlns:x等命名空间声明style属性里大量mso-number-format、mso-pattern、mso-ignore、mso-border-alt等私有样式HTML 里混着w:WordDocument、o:SmartTagType、xml一类的垃圾节点单元格内容被套了一层p标签Excel 里的换行变成br或完整段落。这些内容如果不处理直接塞进编辑器轻则 style 属性里一堆无效代码重则插入后出现奇怪的空白块、乱码符号甚至把表格撑出无法解释的宽度。清洗的思路是先把 HTML 字符串放进一个脱离文档流的div容器里让浏览器解析成 DOM然后遍历节点对 style 做一遍“去 mso 化”把mso-xxx以及部分 Office 私有属性移除把style里真正有用的 CSS 保留例如background-color、color、font-weight、text-align、font-size、border、width、height。重点是不能误删表格结构和合并单元格标记colspan、rowspan。清洗函数的核心片段function cleanExcelStyle(styleStr) { if (!styleStr) { return ; } var parts styleStr.split(;); var kept []; for (var i 0; i parts.length; i) { var part parts[i].trim(); if (!part) { continue; } // 跳过Excel私有样式 if (/^mso-/.test(part)) { continue; } // position:relative这类对表格布局没什么用的可以丢 if (/^position\s*:/.test(part)) { continue; } kept.push(part); } return kept.join(;); } function cleanExcelHtml(html) { var container document.createElement(div); container.innerHTML html; var all container.getElementsByTagName(*); for (var i 0; i all.length; i) { var node all[i]; // 清理节点里命名空间相关的属性 var attrs node.attributes; for (var j attrs.length - 1; j 0; j--) { var attrName attrs[j].name; if (attrName.indexOf(xmlns) 0 || attrName.indexOf(o:) 0 || attrName.indexOf(w:) 0) { node.removeAttribute(attrName); } // 顺手清理事件属性减少XSS面 if (/^on/i.test(attrName)) { node.removeAttribute(attrName); } } // 清理style里的mso私有样式 var style node.getAttribute node.getAttribute(style); if (style) { node.setAttribute(style, cleanExcelStyle(style)); } } // 处理Excel特有的o:p段落节点把里面的内容提出来节点本身删掉 var oPs container.getElementsByTagName(o:p); for (var k oPs.length - 1; k 0; k--) { var oNode oPs[k]; var parent oNode.parentNode; while (oNode.firstChild) { parent.insertBefore(oNode.firstChild, oNode); } parent.removeChild(oNode); } return container.innerHTML; }这段代码的思路已经能解决大部分 Excel 粘贴乱码问题。如果你连mso-number-format里定义的日期格式也想保留那就需要更精细的映射把 Excel 的日期格式串转成YYYY-MM-DD之类的文本这属于进阶需求底子就是这套清洗逻辑。2.4 把清洗后的HTML交给KindEditor插入清洗完毕最后一步是写入编辑器。调用insertHtml时有一个常见问题插入前要确保编辑器已经获取焦点否则插入位置可能不正确。粘贴事件本身发生在编辑器内部焦点一般没大问题但有些浏览器对程序化插入敏感会在光标附近出现奇怪跳动。稳妥做法是editor.focus(); editor.insertHtml(cleanedHtml); editor.update();update()的作用是把当前编辑区的 HTML 同步到编辑器内部的 textarea 或者数据缓存里避免后续取值拿到旧内容。有人习惯用editor.html(editor.html())这种格式化操作不推荐因为会重新渲染整个编辑器、丢失光标位置。用insertHtml是最贴近“粘贴”语义的方式。3. 一个完整的保留格式插件可直接抄作业3.1 插件结构设计把上面这些逻辑收拢一下可以封装成一个独立模块。我在实际项目里是这样设计的一个初始化函数bindExcelPaste接收两个参数编辑器实例和一个配置对象。配置项也不需要多三个就够maxRows最大允许粘贴行数超过就弹提示终止插入防止大表格卡死页面xssClean是否清理事件属性和危险标签默认开启useFallback非 Excel HTML 是否交给编辑器默认处理默认开启。模块内部只需要三块逻辑取数据、清洗、插入。对外只暴露一个bind方法不污染全局空间。这样后续在别的项目里复用时直接把模块文件拷过去改两个参数就能接入。3.2 完整实现代码一个可以直接用的简化版插件代码如下(function (global) { use strict; function cleanExcelStyle(styleStr) { if (!styleStr) { return ; } var parts styleStr.split(;); var kept []; for (var i 0; i parts.length; i) { var part parts[i].trim(); if (!part) { continue; } if (/^mso-/.test(part)) { continue; } if (/^position\s*:/.test(part)) { continue; } kept.push(part); } return kept.join(;); } function cleanExcelHtml(html, options) { var container document.createElement(div); container.innerHTML html; var all container.getElementsByTagName(*); for (var i 0; i all.length; i) { var node all[i]; var attrs node.attributes; for (var j attrs.length - 1; j 0; j--) { var attrName attrs[j].name; if (attrName.indexOf(xmlns) 0 || attrName.indexOf(o:) 0 || attrName.indexOf(w:) 0) { node.removeAttribute(attrName); } if (options.xssClean /^on/i.test(attrName)) { node.removeAttribute(attrName); } } var style node.getAttribute node.getAttribute(style); if (style) { node.setAttribute(style, cleanExcelStyle(style)); } } var oPs container.getElementsByTagName(o:p); for (var k oPs.length - 1; k 0; k--) { var oNode oPs[k]; var parent oNode.parentNode; while (oNode.firstChild) { parent.insertBefore(oNode.firstChild, oNode); } parent.removeChild(oNode); } return container.innerHTML; } function bindExcelPaste(editor, options) { options options || {}; options.maxRows options.maxRows || 500; options.xssClean options.xssClean ! false; var doc editor.edit.doc; var handler function (e) { e e || window.event; var cb e.clipboardData || window.clipboardData; if (!cb) { return; } var html ; try { html cb.getData(text/html); } catch (err) { return; } if (!html || html.indexOf(table) -1) { return; } var rowCount (html.match(/tr/gi) || []).length; if (rowCount options.maxRows) { alert(表格行数过多最多允许粘贴 options.maxRows 行); e.preventDefault(); return; } e.preventDefault(); if (e.stopPropagation) { e.stopPropagation(); } var cleanedHtml cleanExcelHtml(html, options); editor.focus(); editor.insertHtml(cleanedHtml); editor.update(); }; if (doc.addEventListener) { doc.addEventListener(paste, handler, false); } else { doc.attachEvent(onpaste, handler); } return handler; } global.ExcelPaste { bind: bindExcelPaste }; })(window);接入 KindEditor 的方式也很简单K.create(#contentEditor, { // 这里正常配你的items、themeType等 }, function () { window.ExcelPaste.bind(this, { maxRows: 500 }); });这个插件已经能覆盖绝大多数“用户从 Excel 复制一小块区域粘贴到编辑器保留格式”的需求。代码里加了行数限制超过 500 行直接阻止粘贴并提示能有效防住几百上千行数据把页面拖死。3.3 配置KindEditor的几个关键参数有了插件KindEditor 自身的配置也不能完全放飞。有几个参数必须盯着否则可能出现“插件清洗好了编辑器又给过滤一遍”的情况。第一个是pasteType建议保持 2。插件在preventDefault()之后接管了粘贴行为所以这个参数本身不再直接决定成败但把它设成 2 可以在某些边缘场景下兜底。第二个是filterMode。如果设成false编辑器不再主动过滤内容插件清洗后的样式能原样进入但风险是用户从其他地方复制来的危险 HTML 也少了被拦截的机会XSS 压力会转移到服务端。如果设成true那就必须修改htmlTags白名单把表格相关的标签和属性放行否则清洗好的td stylebackground-color:red还是会被剥掉。我个人的建议是filterMode保持true然后在htmlTags里把需要放行的表格标签和属性加进去安全性和样式保留都能兼顾。K.create(#editor, { pasteType: 2, filterMode: true, htmlTags: { table: [style, border, cellspacing, cellpadding, width], tr: [style, width, height], td: [style, rowspan, colspan, width, height], th: [style, rowspan, colspan, width, height], col: [width, style], br: [], p: [style] } }, function () { window.ExcelPaste.bind(this, { maxRows: 500 }); });这里有一股常常被忽略KindEditor 的htmlTags白名单里默认没有col标签。Excel 生成的表格里经常用col控制整列宽度col被过滤掉后列宽信息在插入前就没了所以必须手动把它加进去。同时td的rowspan、colspan是合并单元格的命根子属性名一个都不能少。标签需要保留的属性用途tablestyle、width、border表格整体样式和边框trstyle、width、height行高、行样式td / thstyle、rowspan、colspan、width、height单元格样式和合并单元格colwidth、style列宽控制br无单元格内换行如果你图省事直接设filterMode: false也不是不行很多项目就是这么干的但服务端必须做好白名单过滤不能把信任交给前端。4. 实战踩坑与排查这些问题我基本都遇到过4.1 Excel生成的HTML里全是“洋代码”不清理就乱版第一次拿到 Excel 复制的 HTML 时我打开控制台看了一眼整个人是懵的。里面除了常规的 table 结构还有大量o:p、xml、w:WordDocument之类的节点style 属性里塞满了mso-number-format、mso-ignore。如果把这段 HTML 直接塞给编辑器界面上的表现就是表格后面跟着好几个空行、光标死活对不准、单元格底部多了诡异的空白条。清理的时候有一个容易踩的坑正则把mso-开头的东西一删结果把mso-number-format对应的日期格式也废了用户原先希望粘过去还是日期展示变成了一串浮点秒数。这个问题后面细说先说更常见的。用前面清洗函数里的getElementsByTagName(o:p)处理 Office 段落节点时一定注意不能把o:p里的文本丢掉而是把它的子节点提出来替换到父位置否则单元格里的文字会凭空消失。还有个实用技巧clipboardData.getData(text/html)拿到的内容有时会包含!--StartFragment--和!--EndFragment--标记解析前把中间那段截出来能省去很多头部垃圾。代码很简单var match html.match(/!--StartFragment--([\s\S]*?)!--EndFragment--/); if (match) { html match[1]; }提示调试时先把剪贴板里的 text/html 原样打印出来看一眼。大多数格式问题在看到原始 HTML 之后就能明白八九成比瞎猜清洗规则高效得多。4.2 不同浏览器拿剪贴板数据的方式不一样剪贴板数据在不同浏览器里的表现差异很大这是整个方案里最容易翻车的点。老 IE 里事件对象挂在window.event上剪贴板数据在window.clipboardData而且它不一定支持getData(text/html)。Chrome、Firefox、新版 Edge 都比较规整用event.clipboardData.getData(text/html)就能拿到。macOS 的 Safari 有一个特别坑的行为在 paste 事件处理函数里同步读取剪贴板数据有时拿不到必须把读取操作放到setTimeout里但preventDefault()必须同步调用否则数据已经被浏览器自己处理了一轮你拿到的是处理完的版本。针对 Safari 的兼容写法doc.addEventListener(paste, function (e) { var cb e.clipboardData; if (!cb || typeof cb.getData ! function) { return; } // 能同步拿到HTML就按同步流程走 var html ; try { html cb.getData(text/html); } catch (err) { } if (html) { e.preventDefault(); handleHtml(html); } else { // Safari兜底先阻止默认行为再异步取一次 e.preventDefault(); setTimeout(function () { try { var h2 cb.getData(text/html); if (h2) { handleHtml(h2); } } catch (err) { } }, 0); } }, false);写完这层兼容之后基本能做到主流浏览器表现一致。还有一点要心里有数用户自己电脑上的 Excel 版本、Office 系列组件是否完整也会影响剪贴板里 HTML 内容的形态。同一段数据Excel 2016 和 WPS 复制出来的 HTML 结构可能有细微差别最好在测试阶段就把这两种来源都覆盖到。4.3 大表格粘贴会卡到怀疑人生Excel 里一个包含几百行、每行十几列数据的区域复制出来的 HTML 可能轻松超过几百 KB甚至上 MB。这段字符串要经过innerHTML解析、DOM 遍历、样式清洗、再序列化回去最后再交给insertHtml插入每一步都有性能损耗。我实测过在普通配置的办公电脑上粘贴 2000 行表格浏览器差不多要卡住 3 到 5 秒严重的时候直接无响应。我的处理策略是三管齐下第一插件里加maxRows限制。超过指定行数就弹窗提示阻止粘贴。这算不上优雅但确实能拦住最严重的情况。第二清洗逻辑里尽量少用复杂的全局正则能用 DOM API 就用 DOM API字符串反复 replace 大体积文本代价很高。第三插入前可以顺手压缩连续空白把不必要的缩进空格和换行去掉虽然对性能提升有限但聊胜于无。如果业务上确实需要粘贴几千行数据粘贴方案就不是最优解了应该走文件上传解析路线这个在第 5 节会展开讲。4.4 保存到后端后格式又丢了很多时候编辑器里看着已经没问题一保存到后端再拿出来又变得光秃秃表格颜色全没了。这时候问题不再在前端清洗而在后端安全组件。很多 Java 后端项目会加 XSS 过滤拦截器默认会剥掉 HTML 里的style属性和各种标签因为安全策略里style是高风险字段容易藏恶意内容。如果后端过滤逻辑里没有给table、tr、td、th加白名单你前端费劲保留的样式就会在提交那一刻被清掉。排查方法很简单把保存接口收到的原始 HTML 打出来看一眼确认是入库前丢的还是入库后丢的。如果接口收到的就已经没样式了那就是后端过滤的问题需要和后端同事核对白名单。另一个经常被忽略的坑是 KindEditor 的同步机制。用表单直接提交 textarea 值时如果编辑器内容还没从 iframe 同步到 textarea拿到手的就是空的或旧内容。正确做法是在提交时先调用同步方法$(#submitBtn).click(function () { editor.sync(); var content $(#contentEditor).val(); // content里此时才是编辑器里的完整HTML });同步之后再做统一处理格式就不会莫名其妙在前端就丢了。4.5 两个常被误会的边角问题除了上面这些还有两个边角问题也值得提一下。第一个是用户反映“Excel 里能复制网页里 CtrlV 没反应”。这种往往不是网页的问题而是用户本机 Excel 的加载项或者输入法、键盘钩子软件冲突导致的 CtrlC/CtrlV 失效。可以先让用户在记事本里试一下复制粘贴是否正常先在客户端排掉再来看编辑器的事件绑定。第二个是用户复制的内容特别多超出屏幕可见区域Excel 在部分情况下不会把数据放进 HTML Format而是放进一张图片。这时候getData(text/html)根本拿不到表格结构拿到的是img标签的 base64 数据。如果产品不允许粘贴图片就要在插件里把这种情况也拦下来并给用户明确提示不然你会看到用户贴了一张表格截图到编辑器里格式是保住了但内容根本没法再次编辑。5. 另一种路线直接读Excel文件而不是复制粘贴5.1 什么时候别纠结粘贴格式保留粘贴方案虽好但有几个场景确实不适合硬上。第一是移动端手机上的 Excel/WPS 复制表格后很多浏览器根本拿不到完整的 HTML Format 数据粘贴体验非常不稳定。第二是大数据量几千行、几十列的表格剪贴板里那份 HTML 的体积会大到影响性能和体验。第三是需要读取公式或者原始值的场景粘贴只能拿到显示值拿到的是公式计算结果不是公式本身。在这些情况下更合理的方案是让用户直接上传 Excel 文件后端解析后转成 HTML 存库或者用前端的解析库处理后再插入编辑器。5.2 用xlsx.js解析并生成编辑器可用的表格HTML前端解析 Excel 文件绕不开 SheetJS 这个库。用XLSX.read()读取上传的文件拿到的Workbook对象里每个 sheet 对应一个二维数组!merges字段记录合并单元格的位置。生成 HTML 的时候只需要遍历所有单元格遇到合并关系时给td加上colspan和rowspan最后拼成一个 table 字符串插入编辑器。一个非常简化的示意代码var workbook XLSX.read(arrayBuffer, { type: array }); var firstSheet workbook.Sheets[workbook.SheetNames[0]]; var jsonData XLSX.utils.sheet_to_json(firstSheet, { header: 1 }); var tableHtml table border1 cellpadding4 styleborder-collapse:collapse;; for (var r 0; r jsonData.length; r) { tableHtml tr; var row jsonData[r]; for (var c 0; c row.length; c) { var cellValue row[c] null ? : String(row[c]); // 单元格值转义 cellValue cellValue.replace(//g, lt;).replace(//g, gt;); tableHtml td styleborder:1px solid #ccc;padding:4px; cellValue /td; } tableHtml /tr; } tableHtml /table; editor.insertHtml(tableHtml);要认清一个现实SheetJS 社区版拿不到单元格的视觉样式背景色、字体颜色、边框粗细这些只能拿到数据和合并关系。如果你的产品需要完整还原 Excel 的视觉样式那就得考虑付费版或者后端用 POI 这类库解析时把样式一并输出成 HTML。大部分后台管理系统的实际需求其实只是把表格内容、行列结构、合并关系保住视觉样式用一套默认 CSS 也能接受。所以选路线之前先问清楚需求用户要的是“数据表格能看能编辑”还是“完全复刻 Excel 的打印效果”。前者用文件解析方案绰绰有余后者不管是粘贴还是解析都是一场硬仗。我个人做下来最关键的体会是遇到这种格式保留问题先别急着写一堆清洗正则第一步永远是把剪贴板里的 text/html 原样打印出来看一眼弄明白数据结构长什么样再决定下一步怎么处理。很多新手一上来就想着用 replace 把 mso-xxx 删光结果连 Excel 里真正有用的样式也一起删掉了。如果你只是需要把一段表格完整地弄进编辑器、还能继续编辑自己接管 paste 事件是最直接的路子如果数据量大、来源复杂该换上传解析就换上传解析。把这条原则想清楚KindEditor 的表格粘贴格式问题就不会再让你大半夜对着控制台挠头了。