
前阵子单位内部系统做信创适配接到一个看起来特别简单的工单把Word里的内容复制到xhEditor编辑器里图片要能正常显示。我一开始以为这活儿半天就能搞定结果在测试环境一复现就看到了那个经典到不能再经典的红叉和空白块——从Word复制过去的内容文字格式都在唯独图片全部裂掉。更麻烦的是这个问题在国产操作系统加国产浏览器的环境下被放大了Windows下勉强能用的老办法换到信创环境直接失效。这个问题的本质其实不复杂浏览器出于安全限制不允许网页读取本地文件的file:///路径而Word复制到剪贴板里的HTML内容图片src恰恰是本地临时路径。xhEditor自带的“粘贴传图”能力又只认拖拽上传或URL地址遇到Word生成的这种file:///图片根本不会处理。所以你需要自己接管paste事件从剪贴板里把真正的图片文件“抠”出来上传到服务器再把src回写进编辑器。这篇文章我就把从问题定位、代码实现到信创环境适配的完整过程都写出来给最近正在做信创系统适配、或者手里还维护着老xhEditor项目的朋友一个可以直接抄作业的方案。1. 先搞明白Word复制过来的图片为什么总变“裂图”1.1 剪贴板里的图片不是一条路径而是三种格式很多人以为从Word里复制一张图片到浏览器剪贴板里传过去的就是一个图片文件其实不是。当你在Word里选中一段带图片的内容按CtrlC时剪贴板里同时存了好几种格式的数据纯文本、HTML片段、可能还有图片的二进制数据PNG或BMP。浏览器在粘贴时编辑器默认拿到的是其中的HTML片段而这段HTML里的图片标签长这样img srcfile:///C:/Users/yourname/AppData/Local/Temp/Word/ClipboardImage001.png width300 height200看着好像图片路径都给你了但这玩意儿是Word自己临时生成的本地文件路径。浏览器出于安全策略禁止网页里的JavaScript去读取file:///协议下的本地文件所以这个img标签在编辑器里渲染出来的就是一张裂图。那这些图片的二进制数据去哪了其实它就躺在剪贴板的clipboardData.items里类型是image/png或image/bmp。也就是说图片数据一直都在只是浏览器默认不会把它和HTML里的file:///路径自动对应起来更不会帮你上传到服务器。这就是为什么你要自己接管粘贴逻辑。1.2 浏览器为什么打死不让你显示file:///图片这里涉及一个很多前端开发者容易忽视的安全策略网页里的脚本没有权限访问本地文件系统。这是浏览器最基础的沙箱隔离机制之一。如果一个网页能随便读取你电脑里的本地文件那你在网上随便打开一个页面你硬盘里的文件就全裸奔了。所以Chrome、Firefox这些主流浏览器对file:///协议限制得死死的img标签的src指向file:///路径时浏览器根本不会去加载直接给你一张空图。那为什么很多人说“以前在IE里好像能显示”那是因为老版本IE的沙箱策略相对宽松在某些权限设置下确实允许网页加载本地文件路径的图片但这也是巨大的安全隐患后面各家浏览器都把这个口子封死了。到了信创环境浏览器基本都是Chromium内核的国产浏览器比如奇安信、红莲花、360安全浏览器极速模式它们的file:///限制和Chrome一致所以这个问题表现得特别彻底——Windows上还可能靠老IE或某些魔改内核糊弄过去信创环境里完全没戏。1.3 信创环境下这个问题为什么更突出在做信创适配之前很多系统跑在Windows加老版本IE或360兼容模式上Word粘贴图片偶尔能“碰巧”显示用户也就没怎么报障。但信创环境换了操作系统比如统信UOS、麒麟和国产浏览器这些浏览器大多采用较新的Chromium内核对本地文件访问的限制更严格于是原本被“将就”过去的问题全部暴露出来。另外还有一个被低估的差异信创环境的网络往往是内网隔离的系统部署可能同时有前端域名、后端服务域名、文件上传域名之分跨域问题比单机部署时要频繁得多。你在Windows本机开发时上传接口和前端页面同源一点问题没有部署到信创服务器后前端页面在8080端口上传服务在8081端口或者中间还隔着一个统一网关Cookie、跨域全都变成新的坑。后面我会专门用一节来讲这部分。2. 上手前必须确认的编辑器边界xhEditor的两个关键事实2.1 xhEditor到底有没有“自带粘贴传图”能力xhEditor这个编辑器现在用的人不算多了但存量项目特别是在政府和事业单位内网系统里它的出现频率还真不低。先说清楚它的能力边界xhEditor有一个html5Upload参数置为true之后支持拖拽上传和粘贴上传图片。但这个“粘贴上传”处理的场景是你从网页或其他地方复制的图片URL或者直接把一张图片文件从文件管理器拖进来它并不处理Word复制出来的file:///图片。我实测过Word复制粘贴时图片依然是裂的。有朋友可能会问那我设置upImgUrl之后是不是遇到img标签它就会自动上传不会。xhEditor的源码里对粘贴HTML中的img标签没有做“提取剪贴板文件然后上传”的处理它只拦截本地上传行为和拖拽文件。所以你指望通过配置参数解决问题基本是死路。正确思路是绕开它自带的粘贴处理在编辑器的iframe文档上自己监听paste事件完全接管Word图片的提取和上传。2.2 源码模式与可视化模式对粘贴事件的影响xhEditor有两种编辑模式可视化模式iframe里的contenteditable区域和源码模式直接编辑textarea里的HTML代码。这两种模式下粘贴事件挂载的对象完全不同。可视化模式下你监听的是iframe的contentWindow.document或body的paste事件可以拿到完整的clipboardData包括HTML和图片文件所有处理逻辑都适用。源码模式下焦点落在textarea里textarea属于普通表单元素它的paste事件拿不到text/html格式的数据更拿不到图片文件只有纯文本内容。如果你不做任何处理用户在源码模式下粘贴Word内容图片会直接丢失连裂图都没有彻底消失。所以你要做的第一件事是在项目里明确用户操作路径要求用户必须在可视化模式下粘贴。如果系统允许自动切换模式你可以在粘贴时检测当前是否源码模式如果是就切回可视化模式再触发粘贴。这里有一个实现上的小技巧xhEditor实例方法getSource()能不能拿到源码模式状态不同版本实现不太一样稳妥的做法是记录编辑器初始化时的模式以及监听它的模式切换事件。如果你拿到的项目版本比较老直接强制在编辑器上方放一句提示“请在可视化模式下粘贴内容源码模式不支持图片”这是最省事也最靠谱的方案。3. 接管paste事件的完整实现抢占、取图、放行3.1 拿到编辑器iframe的documentxhEditor初始化后编辑器实例可以用$(selector).xheditor(true)取到。拿到实例后关键一步是获取iframe里的document对象。在xhEditor 3.x版本里编辑器实例上有一个getDoc()方法返回的就是iframe的document。如果你的项目版本没暴露这个方法也可以这样取var editor $(#editorContent).xheditor(true); var iframe editor.getDoc().window.frameElement; // 或者通过编辑器DOM容器的iframe元素 var iframeDoc iframe.contentDocument || iframe.contentWindow.document;实际操作中我更建议同时给编辑器外层容器的iframe绑定事件因为xhEditor可能在你初始化之后才创建iframe如果你在初始化前就绑定需要等到ready回调里再挂。以下是初始化时注册paste监听的完整写法var editor $(#editorContent).xheditor({ tools: full, skin: default, html5Upload: false // 关掉自带的免得它先拦截 }); editor.ready(function() { var doc editor.getDoc(); if (doc) { doc.addEventListener(paste, handlePaste, true); } });注意绑定时用了true也就是在捕获阶段就拦截这样可以抢在编辑器自带处理逻辑之前拿到事件避免它先做了默认粘贴再让我们擦屁股。3.2 从clipboardData中提取图片文件核心代码来了。粘贴事件触发后要从clipboardData里把图片文件提取出来。现代浏览器里图片数据藏在clipboardData.items中遍历每一个item凡是kind file且type以image/开头的就是我们要的图片文件function extractImagesFromClipboard(clipboardData) { var files []; var items clipboardData.items; if (!items) return files; for (var i 0; i items.length; i) { var item items[i]; if (item.kind file item.type.indexOf(image/) 0) { var file item.getAsFile(); if (file) { files.push(file); } } } return files; }这里有一个容易被忽略的细节Word复制时剪贴板中的图片可能是PNG格式也可能是BMP格式还可能是增强型图元文件EMF。EMF这种格式在Chromium内核的浏览器里getAsFile()返回的File对象可能拿不到或拿到后无法识别。我实测中遇到的情况是遇到EMF时item.type有时候是image/emf这时候不要硬传直接跳过或者把它当作“无法处理的图片”在界面上提示用户用截图工具重新粘贴。这个后面专门讲兼容性时再扩展。3.3 构造带占位图的HTML并插入编辑器这一步是整个方案的关键。我们不能简单地把图片文件直接插入编辑器因为要从Word粘贴的内容可能是一整段带格式的文字加图片。如果只插入图片而丢弃文字格式用户粘贴体验会很差。所以正确做法是取到剪贴板里的HTML解析它把其中需要处理的img标签替换成带>function buildHtmlWithPlaceholder(clipboardData, imageFiles) { var html clipboardData.getData(text/html); if (!html) { // 剪贴板里没有HTML说明用户只复制了图片本身没有附带文字格式 var imgTags ; for (var i 0; i imageFiles.length; i) { imgTags img>function insertHtmlToEditor(editor, html) { editor.focus(); var doc editor.getDoc(); doc.execCommand(insertHTML, false, html); }3.4 上传图片并逐个回写图片地址插入占位图之后接下来就是异步上传。每成功一个就根据>function uploadImages(editor, imageFiles) { for (var i 0; i imageFiles.length; i) { (function(index) { var file imageFiles[index]; var fd new FormData(); fd.append(file, file); fetch(/api/upload/image, { method: POST, body: fd, credentials: include }) .then(function(res) { return res.json(); }) .then(function(data) { if (data.code 0) { var doc editor.getDoc(); var img doc.querySelector(img[data-wp-index index ]); if (img) { img.src data.url; img.removeAttribute(data-wp-index); } } }) .catch(function() { // 上传失败时移除占位图或在图片位置显示一个失败提示 var doc editor.getDoc(); var img doc.querySelector(img[data-wp-index index ]); if (img) { img.src /static/images/upload-error.png; img.title 图片上传失败; } }); })(i); } }这一段有几个细节值得说明第一fetch是ES6的API如果你要兼容特别老的内核建议用XMLHttpRequest。信创环境里很多国产浏览器的版本虽然叫“极速模式”但内核版本可能还停留在Chrome 60左右fetch能用但为了兼容IE内核就完全不行了。后面我会给出一版兼容性更好的实现。第二上传请求必须带同源Cookie凭证也就是credentials: include。很多信创系统的登录状态依靠Cookie维持如果你上传接口和页面不同源不带凭证的话请求会401或者无法识别用户身份。第三上传失败的回退逻辑一定不能省。用户粘贴了10张图如果第3张失败了你不处理用户保存内容后那张就是一张破碎的占位图排查起来让人头大。3.5 兜底没有图片文件的剪贴板内容正常放行那位说了我只想处理含图片的粘贴如果剪贴板里纯文字是不是不需要接管对有一个很重要的判断判断完extractImagesFromClipboard的结果是空数组且HTML里也没有file:///或blob:开头的img时不要调用preventDefault()直接return让编辑器走默认粘贴流程。否则你接管了纯文本粘贴还要额外处理文本格式白白增加出问题的概率。完整的事件处理函数应该是这个样子的function handlePaste(e) { var cbd e.clipboardData || window.clipboardData; if (!cbd) return; var imageFiles extractImagesFromClipboard(cbd); var html cbd.getData(text/html) || ; // 没有任何图片相关数据放行让编辑器默认处理 if (imageFiles.length 0 !/(file:\/\/|blob:|data:image)/i.test(html)) { return; } e.preventDefault(); e.stopPropagation(); var insertHtml buildHtmlWithPlaceholder(cbd, imageFiles); insertHtmlToEditor(editor, insertHtml); if (imageFiles.length 0) { uploadImages(editor, imageFiles); } }这一步“能放行就放行”的原则能最大程度减少对原有编辑器行为的干扰。我在实际项目中见过反面教材有同事把paste事件完全接管所有粘贴内容都被强制走一遍自定义逻辑结果粘贴的文本变成无格式纯文本行内样式全部丢失用户骂了一个月。所以记住只在需要处理图片的时候才插手。4. 后端上传接口和信创中间件的对接细节4.1 图片上传接口的校验维度前端代码写得再花哨后端不配合也是白搭。图片上传接口除了基本的接收文件和返回URL我建议至少要做这几层校验校验维度具体配置说明文件类型白名单jpg、jpeg、png、gif、webp、bmp不要用黑名单攻击者可以伪造扩展名白名单更安全文件大小默认单张不超过5MB可配置Word图片一般不大但截图、扫描件可能很大内容嗅探校验文件头魔数而不是只看扩展名防止有人上传伪装成图片的可执行文件存储路径/upload/2025/06/按日期分目录避免单目录文件过多也方便后期清理访问权限根据系统需要决定是否鉴权内网系统一般公开读外网系统需要加访问控制一个简单的Spring Boot接口示例PostMapping(/api/upload/image) public Result uploadImage(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(文件为空); } String originalFilename file.getOriginalFilename(); String ext getExtension(originalFilename).toLowerCase(); if (!ALLOWED_EXTENSIONS.contains(ext)) { return Result.error(不支持的图片格式: ext); } if (file.getSize() 5 * 1024 * 1024) { return Result.error(图片大小不能超过5MB); } // 检查文件头魔数防止伪装文件 byte[] header new byte[8]; try { file.getInputStream().read(header); if (!isValidImage(header, ext)) { return Result.error(图片内容校验失败); } } catch (IOException e) { return Result.error(读取文件失败); } String newFilename UUID.randomUUID().toString().replace(-, ) . ext; String datePath new SimpleDateFormat(yyyy/MM).format(new Date()); String relativePath /upload/ datePath / newFilename; // 实际存储代码省略可以使用本地磁盘或对象存储 // storage.store(file.getInputStream(), relativePath); return Result.ok(relativePath); }4.2 跨域与会话内网网关下最容易出的问题跨域这个问题在信创环境里极容易踩坑。因为信创项目的前端、后端、文件服务往往是独立部署的甚至经过统一网关。你本地开发时页面在localhost:8080上传接口也在localhost:8080同源一切正常。部署到测试环境后页面变成了http://10.10.1.100:8080后端接口在http://10.10.1.100:8081跨域就出现了。处理这个问题的方案有几种第一种在代码里用相对路径请求上传接口比如前端把请求地址配置成一个全局变量根据当前域名自动拼接。如果前端和后端通过同一网关映射到相同域名的不同路径下比如/api/upload/image正好在同一个域名下那就不存在跨域。第二种后端开启CORS。在Spring Boot里加一个跨域配置Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }addAllowedOriginPattern(*)配合allowCredentials(true)要注意不能用addAllowedOrigin(*)因为带凭证的请求不允许通配符Origin必须明确指定或使用Origin Pattern。这个坑很多人踩过请求带Cookie时跨域配置不生效表现为登录状态丢失。第三种如果信创环境中间层有专门的网关比如东方通、金蝶天燕等国产中间件最好让网关统一代理前端和上传请求做成同源。这是最省心的方案因为内网环境里很多时候网络策略封闭跨域请求可能被防火墙拦截CORS配置写好了也不一定生效。4.3 返回URL的路径规划别让图片下次变成死链上传接口返回的URL怎么设计直接影响图片在编辑器里的可用性。我见过很多项目图省事后端直接返回完整URL比如http://10.10.1.100:8080/upload/2025/06/xxx.png。问题来了如果系统换个域名或者换台服务器部署数据库里存的图片URL全部变成死链。我的建议是后端返回相对路径比如/upload/2025/06/xxx.png前端在回写img的src时用当前站点的域名拼一下。这样系统换域名时图片跟着前端域名走不会因为硬编码IP导致访问失败。如果图片存储在独立文件服务器上那你需要在后端配置里维护一个file.base.url变量统一输出文件服务的基础地址这样换存储位置只改配置不用动代码。5. 兼容性适配老Chromium内核、IE内核与国产浏览器5.1 不同浏览器内核的API支持差异信创环境的浏览器生态和你在Windows上用Chrome完全是两个概念。常见的国产浏览器里有的用Chrome 80以上的新内核有的还在Chrome 60附近还有的提供所谓“兼容模式”实际上就是IE内核。这些差异直接决定了你的粘贴处理代码能用到什么程度浏览器环境clipboardData.itemsfetchDOMParserFormData结论Chromium 86较新国产浏览器支持支持支持支持完整方案可用Chromium 55-70老内核支持部分支持支持支持建议用XHR替代fetchIE 11 / 360兼容模式不支持不支持支持部分支持拿不到图片文件只能降级提示这里特别要说明的是clipboardData.items在IE内核下完全不支持。IE有自己的剪贴板API也就是window.clipboardData但它只能读取文本和URL拿不到图片的二进制数据。所以在兼容模式下你从Word复制图片粘贴无论如何都不可能自动上传。对这种情况我的处理方式是弹一个提示“当前浏览器兼容模式不支持粘贴Word图片请切换至极速模式或使用Chrome内核浏览器。”这个提示一定要给不然用户以为自己操作错了反复试然后报障。5.2 针对老内核的降级写法前面示例代码里用的fetch和DOMParser在老内核下都可能有兼容性问题。我在信创环境实测时把上传逻辑统一改成了XMLHttpRequest代码虽然啰嗦一点但稳定性高很多。下面是一版兼容性更稳妥的上传函数function uploadImage(file, successCallback, errorCallback) { var xhr new XMLHttpRequest(); var fd new FormData(); fd.append(file, file); xhr.open(POST, /api/upload/image, true); xhr.withCredentials true; xhr.onload function() { if (xhr.status 200 xhr.status 300) { try { var data JSON.parse(xhr.responseText); if (data.code 0) { successCallback(data.url); } else { errorCallback(data.msg || 上传失败); } } catch (e) { errorCallback(响应解析失败); } } else { errorCallback(HTTP错误: xhr.status); } }; xhr.onerror function() { errorCallback(网络错误); }; xhr.send(fd); }withCredentials true对应fetch里的credentials: include用来携带Cookie。如果你不需要跨域带凭证这行可以不加但在信创环境里还是加上比较稳妥。对于DOMParser我记得在非常老的内核里也能用但如果你的目标环境里有更极端的浏览器可以用一个隐藏iframe来解析HTML字符串这个老掉牙的方案兼容性最好function parseHTML(html) { var iframe document.createElement(iframe); iframe.style.display none; document.body.appendChild(iframe); var doc iframe.contentDocument || iframe.contentWindow.document; doc.open(); doc.write(html); doc.close(); var result doc.body; document.body.removeChild(iframe); return result; }5.3 实在拿不到图片文件时怎么给用户提示即使浏览器内核不新有时候也拿不到图片文件。比如用户从Word复制了“嵌入型”图片粘贴时clipboardData.items里确实有文件但用户复制的是旧版本Word里的对象或者是从WPS里复制出来的粘贴到网页时可能效果不同。WPS文字复制图片到浏览器某些版本拿到的不是标准的image/png文件格式而是application/x-qt-windows-mime;valuePNG之类的私有类型这个就只能在item.type里做兼容判断了。遇到拿不到文件的情况比较务实的做法有两个第一提示用户使用“截图后粘贴”的方式。比如先用截图工具截取Word里的内容然后直接CtrlV粘贴到编辑器这种情况下剪贴板里是纯图片imageFiles一定能提取到。第二把粘贴的HTML里的img标签的src替换成一个“无法加载”的占位图并且在图片上加上提示文字“该图片需重新上传”。不要直接把图片丢掉至少用户能看到内容位置。6. 实测中的翻车现场和修复方案6.1 粘贴时焦点不在编辑器里插进去的全是空白这个问题是我自己在测试时踩到的。用户在页面上先点了文本框输入了标题然后直接CtrlV期望内容粘贴到下面的编辑器里结果粘贴的内容没有出现在编辑器或者出现在了编辑器开头。原因很简单execCommand(insertHTML)要求焦点必须在编辑器内部的contenteditable区域如果焦点不在编辑器里插入操作要么失败要么把内容插到错误的位置。解决办法是在粘贴处理前调用editor.focus()但还要注意一种特殊情况如果用户根本没有点击过编辑器编辑器内部的焦点状态可能是空的光调用focus还不够最好再把光标设置到编辑器内容的末尾避免覆盖已有内容。下面这段代码可以帮忙function focusAndPlaceCaretAtEnd(editor) { editor.focus(); var doc editor.getDoc(); var body doc.body; var range doc.createRange(); range.selectNodeContents(body); range.collapse(false); // false表示折叠到末尾 var selection doc.getSelection(); selection.removeAllRanges(); selection.addRange(range); }在插入内容前先调用这个函数确保光标在编辑器里且位置合理。6.2 多图粘贴时顺序错乱Word里如果贴了多张图片复制到浏览器时剪贴板里的imageFiles顺序和HTML里img标签的顺序并不保证完全一致。我这里实测的情况是大多数时候一致但偶尔会乱。如果你直接按imageFiles的顺序上传回写时就可能出现图1和图2位置互换的情况。解决思路有两种第一种放弃按顺序匹配用文件类型和大小做模糊匹配。但这种不太靠谱也不推荐。第二种更稳妥的方案是在粘贴时把HTML里的file:///路径的img顺序和imageFiles按顺序一一对应然后给每个img打上>