《一个前端菜鸟的Word导入血泪史如何在Vue2TinyMCE里让Word图片和样式起死回生》第一天初生牛犊不怕虎不就是导个Word吗百度一下分分钟搞定“我自信满满地打开搜索引擎输入Vue2 TinyMCE Word导入”。10分钟后…“这特么都是些什么鬼要么是2016年的老古董教程要么是收费插件的软文还有几个直接说’不可能实现’的毒舌回答”我盯着屏幕上那个粘贴按钮仿佛听见它在嘲笑我“菜鸟连个Word都搞不定”第二天与开源库的殊死搏斗Round 1mammoth.js“这个库评分挺高啊试试”mammoth.extractRawText({arrayBuffer:fileBuffer}).then(function(result){console.log(result.value);// 哇文本出来了console.log(result.messages);// 等等...这些[image:123]是什么鬼});看着控制台里一串[image:123]的占位符我陷入了沉思“所以…图片呢被外星人绑架了”Round 2docx-preview“这个看起来更专业支持渲染”import{renderAsync}fromdocx-preview;renderAsync(fileBuffer,document.getElementById(preview-container)).then((){console.log(渲染成功);// 确实渲染了...// 但TinyMCE编辑器里还是空的啊啊啊});我盯着屏幕上那个完美的预览又看看空荡荡的编辑器突然明白了一个道理有些东西看看就好别太当真。第三天曲线救国——后端大法好既然前端搞不定那就让后端来我兴奋地冲进后端同事的工位。Java SpringBoot版救世主// 同事花了一整天写的神器PostMapping(/import-docx)publicResponseEntityimportDocx(RequestParam(file)MultipartFilefile){try{XWPFDocumentdocumentnewXWPFDocument(file.getInputStream());// 各种复杂的解析逻辑...returnResponseEntity.ok(htmlContent);// 返回的HTML}catch(Exceptione){returnResponseEntity.badRequest().body(导入失败e.getMessage());}}成了我兴奋地把返回的HTML塞进TinyMCEtinymce.activeEditor.setContent(response.data);结果…所有图片都变成了404因为后端返回的是相对路径/images/123.jpg而前端根本不知道这个路径在哪。第四天图片上传的九九八十一难看来得自己处理图片上传…我咬咬牙开始研究。前端图片提取functionextractImagesFromDocx(file){returnnewPromise((resolve){constzipnewJSZip();zip.loadAsync(file).then(zip{constimages[];// 遍历zip文件查找图片zip.forEach((relativePath,fileEntry){if(/\.(jpeg|jpg|png|gif)$/i.test(relativePath)){fileEntry.async(blob).then(blob{images.push(blob);if(images.length/* 需要知道总图片数 */){resolve(images);}});}});});});}等等…我怎么知道总共有多少张图片我盯着这个永远无法完成的Promise突然意识到自己又犯了个蠢。改进版用Web Worker偷偷处理// 在Web Worker里self.onmessagefunction(e){constfilee.data.file;constzipnewJSZip();zip.loadAsync(file).then(zip{constimagePromises[];zip.forEach((relativePath,fileEntry){if(/\.(jpeg|jpg|png|gif)$/i.test(relativePath)){imagePromises.push(fileEntry.async(blob));}});Promise.all(imagePromises).then(blobs{// 上传所有图片到服务器constuploadPromisesblobs.map(blob{constformDatanewFormData();formData.append(image,blob);returnfetch(/api/upload,{method:POST,body:formData});});Promise.all(uploadPromises).then(responses{constimageUrlsresponses.map(resres.url);self.postMessage({type:images-uploaded,urls:imageUrls});});});});};终于搞定了我兴奋地看着图片一张张上传成功结果发现…上传顺序和文档中出现的顺序不一致导致图片全部错位。第五天终极解决方案——“偷梁换柱”算了直接改TinyMCE源码吧我恶向胆边生打开了TinyMCE的paste插件代码。Step 1拦截粘贴事件tinymce.init({// ...其他配置setup:function(editor){editor.on(paste,function(e){constclipboardDatae.clipboardData||window.clipboardData;constitemsclipboardData.items;for(leti0;iitems.length;i){if(items[i].type.indexOf(image)!-1){constblobitems[i].getAsFile();// 上传图片并替换为URLuploadImage(blob).then(url{editor.insertContent();});e.preventDefault();}}});}});这个只能处理直接粘贴的图片Word里的还是不行…我叹了口气。Step 2Word转HTML再处理最终我采用了组合拳方案用mammoth.js提取文本和图片占位符用JSZip解析Word文件中的图片上传所有图片到服务器替换HTML中的占位符为真实URL最后塞进TinyMCEasyncfunctionimportWordToTinyMCE(file){// 1. 提取文本和图片信息constresultawaitmammoth.extractRawText({arrayBuffer:awaitfile.arrayBuffer()});// 2. 解析Word文件中的图片constzipawaitJSZip.loadAsync(file);constimageBlobs[];constimagePaths[];zip.forEach((relativePath,fileEntry){if(/\.(jpeg|jpg|png|gif)$/i.test(relativePath)){fileEntry.async(blob).then(blob{imageBlobs.push(blob);imagePaths.push(relativePath);});}});// 3. 上传图片简化版constuploadedUrlsawaitPromise.all(imageBlobs.map(blobuploadImageToServer(blob)));// 4. 替换占位符lethtmlresult.value;uploadedUrls.forEach((url,index){htmlhtml.replace([image:${index}],);});// 5. 设置编辑器内容tinymce.activeEditor.setContent(html);}最终成果与感悟经过五天的殊死搏斗我终于实现了Word文档的文本导入图片上传并正确显示基本样式保留虽然还不够完美经验教训不要轻信简单的需求——Word导入涉及文件解析、图片处理、样式转换等多个复杂环节开源库不是万能的——很多库只能解决部分问题需要组合使用前端不是万能的——有些操作如文件解析后端处理更合适调试是门艺术——经常需要在控制台和Network面板之间来回切换像侦探一样寻找线索现在当我看着编辑器里完美显示的Word文档时突然想起第一天那个自信满满的自己不禁笑出声来“菜鸟你成长了”复制插件文件安装jquerynpm install jquery在组件中引入添加工具栏在线代码https://gitee.com/xproer/wordpaster-vue-tinymce5/blob/master/src/components/tinymce.vue#L44添加插件在线代码https://gitee.com/xproer/wordpaster-vue-tinymce5/blob/master/src/components/tinymce.vue初始化组件在页面中引入组件下载示例点击下载完整示例
国产化OA如何实现PPT动画在TinyMCE6中的无缝转存?
《一个前端菜鸟的Word导入血泪史如何在Vue2TinyMCE里让Word图片和样式起死回生》第一天初生牛犊不怕虎不就是导个Word吗百度一下分分钟搞定“我自信满满地打开搜索引擎输入Vue2 TinyMCE Word导入”。10分钟后…“这特么都是些什么鬼要么是2016年的老古董教程要么是收费插件的软文还有几个直接说’不可能实现’的毒舌回答”我盯着屏幕上那个粘贴按钮仿佛听见它在嘲笑我“菜鸟连个Word都搞不定”第二天与开源库的殊死搏斗Round 1mammoth.js“这个库评分挺高啊试试”mammoth.extractRawText({arrayBuffer:fileBuffer}).then(function(result){console.log(result.value);// 哇文本出来了console.log(result.messages);// 等等...这些[image:123]是什么鬼});看着控制台里一串[image:123]的占位符我陷入了沉思“所以…图片呢被外星人绑架了”Round 2docx-preview“这个看起来更专业支持渲染”import{renderAsync}fromdocx-preview;renderAsync(fileBuffer,document.getElementById(preview-container)).then((){console.log(渲染成功);// 确实渲染了...// 但TinyMCE编辑器里还是空的啊啊啊});我盯着屏幕上那个完美的预览又看看空荡荡的编辑器突然明白了一个道理有些东西看看就好别太当真。第三天曲线救国——后端大法好既然前端搞不定那就让后端来我兴奋地冲进后端同事的工位。Java SpringBoot版救世主// 同事花了一整天写的神器PostMapping(/import-docx)publicResponseEntityimportDocx(RequestParam(file)MultipartFilefile){try{XWPFDocumentdocumentnewXWPFDocument(file.getInputStream());// 各种复杂的解析逻辑...returnResponseEntity.ok(htmlContent);// 返回的HTML}catch(Exceptione){returnResponseEntity.badRequest().body(导入失败e.getMessage());}}成了我兴奋地把返回的HTML塞进TinyMCEtinymce.activeEditor.setContent(response.data);结果…所有图片都变成了404因为后端返回的是相对路径/images/123.jpg而前端根本不知道这个路径在哪。第四天图片上传的九九八十一难看来得自己处理图片上传…我咬咬牙开始研究。前端图片提取functionextractImagesFromDocx(file){returnnewPromise((resolve){constzipnewJSZip();zip.loadAsync(file).then(zip{constimages[];// 遍历zip文件查找图片zip.forEach((relativePath,fileEntry){if(/\.(jpeg|jpg|png|gif)$/i.test(relativePath)){fileEntry.async(blob).then(blob{images.push(blob);if(images.length/* 需要知道总图片数 */){resolve(images);}});}});});});}等等…我怎么知道总共有多少张图片我盯着这个永远无法完成的Promise突然意识到自己又犯了个蠢。改进版用Web Worker偷偷处理// 在Web Worker里self.onmessagefunction(e){constfilee.data.file;constzipnewJSZip();zip.loadAsync(file).then(zip{constimagePromises[];zip.forEach((relativePath,fileEntry){if(/\.(jpeg|jpg|png|gif)$/i.test(relativePath)){imagePromises.push(fileEntry.async(blob));}});Promise.all(imagePromises).then(blobs{// 上传所有图片到服务器constuploadPromisesblobs.map(blob{constformDatanewFormData();formData.append(image,blob);returnfetch(/api/upload,{method:POST,body:formData});});Promise.all(uploadPromises).then(responses{constimageUrlsresponses.map(resres.url);self.postMessage({type:images-uploaded,urls:imageUrls});});});});};终于搞定了我兴奋地看着图片一张张上传成功结果发现…上传顺序和文档中出现的顺序不一致导致图片全部错位。第五天终极解决方案——“偷梁换柱”算了直接改TinyMCE源码吧我恶向胆边生打开了TinyMCE的paste插件代码。Step 1拦截粘贴事件tinymce.init({// ...其他配置setup:function(editor){editor.on(paste,function(e){constclipboardDatae.clipboardData||window.clipboardData;constitemsclipboardData.items;for(leti0;iitems.length;i){if(items[i].type.indexOf(image)!-1){constblobitems[i].getAsFile();// 上传图片并替换为URLuploadImage(blob).then(url{editor.insertContent();});e.preventDefault();}}});}});这个只能处理直接粘贴的图片Word里的还是不行…我叹了口气。Step 2Word转HTML再处理最终我采用了组合拳方案用mammoth.js提取文本和图片占位符用JSZip解析Word文件中的图片上传所有图片到服务器替换HTML中的占位符为真实URL最后塞进TinyMCEasyncfunctionimportWordToTinyMCE(file){// 1. 提取文本和图片信息constresultawaitmammoth.extractRawText({arrayBuffer:awaitfile.arrayBuffer()});// 2. 解析Word文件中的图片constzipawaitJSZip.loadAsync(file);constimageBlobs[];constimagePaths[];zip.forEach((relativePath,fileEntry){if(/\.(jpeg|jpg|png|gif)$/i.test(relativePath)){fileEntry.async(blob).then(blob{imageBlobs.push(blob);imagePaths.push(relativePath);});}});// 3. 上传图片简化版constuploadedUrlsawaitPromise.all(imageBlobs.map(blobuploadImageToServer(blob)));// 4. 替换占位符lethtmlresult.value;uploadedUrls.forEach((url,index){htmlhtml.replace([image:${index}],);});// 5. 设置编辑器内容tinymce.activeEditor.setContent(html);}最终成果与感悟经过五天的殊死搏斗我终于实现了Word文档的文本导入图片上传并正确显示基本样式保留虽然还不够完美经验教训不要轻信简单的需求——Word导入涉及文件解析、图片处理、样式转换等多个复杂环节开源库不是万能的——很多库只能解决部分问题需要组合使用前端不是万能的——有些操作如文件解析后端处理更合适调试是门艺术——经常需要在控制台和Network面板之间来回切换像侦探一样寻找线索现在当我看着编辑器里完美显示的Word文档时突然想起第一天那个自信满满的自己不禁笑出声来“菜鸟你成长了”复制插件文件安装jquerynpm install jquery在组件中引入添加工具栏在线代码https://gitee.com/xproer/wordpaster-vue-tinymce5/blob/master/src/components/tinymce.vue#L44添加插件在线代码https://gitee.com/xproer/wordpaster-vue-tinymce5/blob/master/src/components/tinymce.vue初始化组件在页面中引入组件下载示例点击下载完整示例