纯前端导出word文档(含样式、图片、canvas等元素)

打印 上一主题 下一主题

主题 873|帖子 873|积分 2619

前言

有需要不依靠后端的情况下将网页原封不动的导出为word文档, 就写了这个工具函数。
思路



  • 删除隐藏的或者排除的元素
  • 将所有样式转化为行内样式
  • 将图片和canvas转为Base64编码, 一起导入word中
  • 前端天生word有固定的头部模版, 更换内容为innerHTML即可导出work文档, 且保存行内样式
  • 手(复)写(制)的saveAs
使用技能



  • 前端:JAVASCRIPT
  • 依靠: 无
开始操作

前端

将所有元素打标记, 然后放入iframe中, 这样不会影响原来的元素

  1. // 设置标记, 方便复制样式
  2. Array.from(contEl.querySelectorAll('*')).forEach((item) => {
  3.         item.setAttribute('data-toword', Math.random().toString(32).slice(-5))
  4. })
  5. if (!window.contIframe) {
  6.         window.contIframe = document.createElement('iframe')
  7.         window.contIframe.style = 'display: none; width:100%;'
  8.         document.body.appendChild(window.contIframe)
  9. }
  10. let cloneEl = contEl.cloneNode(true)
  11. cloneEl.style.width = getComputedStyle(contEl).width
  12. window.contIframe.contentDocument.body.appendChild(cloneEl)
  13. let domWrap = cloneEl
复制代码
1. 删除隐藏的元素, 并且将元素样式设置为行内样式, 方便word识别

  1. Array.from(domWrap.querySelectorAll('*')).forEach((item) => {
  2.         let attr = item.getAttribute('data-toword')
  3.         let originItem = contEl.querySelector('[data-toword="' + attr +  '"]')
  4.         if (originItem) {
  5.                 let sty = getComputedStyle(originItem)
  6.                 if (sty.display == 'none ') return item.remove()
  7.                 setStyle(item, sty)
  8.         }
  9. })
  10. // 1.1 删除排除的元素
  11. if (Array.isArray(options.exclude) && options.exclude.length) {
  12.         options.exclude.forEach(ext => {
  13.                 Array.from(domWrap.querySelectorAll(ext)).forEach((item) => item.remove())
  14.         })
  15. }
复制代码
将图片和canvas转为Base64编码

  1. let imgList = domWrap.querySelectorAll('img')
  2. console.log('加载图片数量: ', imgList.length)
  3. await Promise.all(Array.from(imgList).filter(x => !x.src.startsWith('data')).map(tempimg => {
  4.         let img = new Image()
  5.         img.setAttribute('crossOrigin', 'anonymous')
  6.         img.src = options.proxyHost ? tempimg.src.replace(location.host, options.proxyHost) : tempimg.src
  7.         return new Promise((resolve, reject) => {
  8.                 try {
  9.                         img.onload = function () {
  10.                                 img.onload = null
  11.                                 const cw = Math.min(img.width, options.maxWidth)
  12.                                 const ch = img.height * (cw / img.width)
  13.                                 const canvas = document.createElement("CANVAS")
  14.                                 canvas.width = cw
  15.                                 canvas.height = ch
  16.                                 const context = canvas.getContext('2d')
  17.                                 context?.drawImage(img, 0, 0, cw, ch)
  18.                                 const uri = canvas.toDataURL("image/jpg", 0.8)
  19.                                 tempimg.src = uri
  20.                                 const w = Math.min(img.width, 550, options.maxWidth) // word图片最大宽度
  21.                                 tempimg.width = w
  22.                                 tempimg.height = img.height * (w / img.width)
  23.                                 console.log('img onload...', options.fileName, img.src, img.width, img.height, cw, ch, w, tempimg.height)
  24.                                 canvas.remove()
  25.                                 resolve(img.src)
  26.                         }
  27.                         img.onerror = function() {
  28.                                 console.log('img load error, ', img.src)
  29.                                 resolve('')
  30.                         }
  31.                 } catch (e) { console.log(e);resolve('') }
  32.         })
  33. }))
  34. // 将canvas转为Base64编码, 方便word保存
  35. let canvasList = domWrap.querySelectorAll('canvas')
  36. console.log('加载canvas数量: ', canvasList.length)
  37. await Promise.all(Array.from(canvasList).map(tempCanvas => {
  38.         let img = new Image()
  39.         img.setAttribute('crossOrigin', 'anonymous');
  40.         return new Promise((resolve, reject) => {
  41.                 try {
  42.                         let attr = tempCanvas.getAttribute('data-toword')
  43.                         let cvs = contEl.querySelector('[data-toword="' + attr +  '"]')
  44.                         console.log(cvs, attr);
  45.                         if (!cvs) return resolve()
  46.                         img.src = cvs.toDataURL("image/jpg", 0.8)
  47.                         const w = Math.min(cvs.width, options.maxWidth)
  48.                         const h = cvs.height * (w / cvs.width)
  49.                         img.width = w
  50.                         img.height = h
  51.                         const parent = tempCanvas.parentNode
  52.                         if (tempCanvas.nextSibling) {
  53.                                         parent.insertBefore(img, tempCanvas.nextSibling)
  54.                         } else {
  55.                                         parent.appendChild(img)
  56.                         }
  57.                         tempCanvas.remove()
  58.                         resolve('')
  59.                 } catch (e) { console.log(e);resolve('') }
  60.         })
  61. }))
复制代码
将html内容写入word模版中并导出下载

  1. let topstr = 'xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns:m="http://schemas.microsoft.com/office/2004/12/omml" xmlns="http://www.w3.org/TR/REC-html40"'
  2. let headstr = '<!--[if gte mso 9]><xml><w:WordDocument><w:View>Print</w:View><w:TrackMoves>false</w:TrackMoves><w:TrackFormatting/><w:ValidateAgainstSchemas/><w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid><w:IgnoreMixedContent>false</w:IgnoreMixedContent><w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText><w:DoNotPromoteQF/><w:LidThemeOther>EN-US</w:LidThemeOther><w:LidThemeAsian>ZH-CN</w:LidThemeAsian><w:LidThemeComplexScript>X-NONE</w:LidThemeComplexScript><w:Compatibility><w:BreakWrappedTables/><w:SnapToGridInCell/><w:WrapTextWithPunct/><w:UseAsianBreakRules/><w:DontGrowAutofit/><w:SplitPgBreakAndParaMark/><w:DontVertAlignCellWithSp/><w:DontBreakConstrainedForcedTables/><w:DontVertAlignInTxbx/><w:Word11KerningPairs/><w:CachedColBalance/><w:UseFELayout/></w:Compatibility><w:BrowserLevel>MicrosoftInternetExplorer4</w:BrowserLevel><m:mathPr><m:mathFont m:val="Cambria Math"/><m:brkBin m:val="before"/><m:brkBinSub m:val="--"/><m:smallFrac m:val="off"/><m:dispDef/><m:lMargin m:val="0"/> <m:rMargin m:val="0"/><m:defJc m:val="centerGroup"/><m:wrapIndent m:val="1440"/><m:intLim m:val="subSup"/><m:naryLim m:val="undOvr"/></m:mathPr></w:WordDocument></xml><![endif]-->'
  3. let mhtml = {
  4.         top: "Mime-Version: 1.0\nContent-Base: " + location.href + "\nContent-Type: Multipart/related; boundary="NEXT.ITEM-BOUNDARY";type="text/html"\n\n--NEXT.ITEM-BOUNDARY\nContent-Type: text/html; charset="utf-8"\nContent-Location: " + location.href + `\n\n<!DOCTYPE html>\n<html ${topstr}>\n_html_</html>`,
  5.         head: `<head>\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8">\n${headstr}\n<style>\n_styles_\n</style>\n</head>\n`,
  6.         body: "<body>_body_</body>"
  7. };
  8. let exthtml = (options.title ? `<h1 style="text-align:center">${options.title}</h1>` : '') + (options.time ? `<p style="text-align:center">${options.time}</p>` : '')
  9. let styles = "";
  10. let fileContent = mhtml.top.replace("_html_", mhtml.head.replace("_styles_", styles) + mhtml.body.replace("_body_", exthtml + domWrap.innerHTML));
  11. let blob = new Blob([fileContent], { type: "application/msword;charset=utf-8" });
  12. console.log('即将生成文件大小: ', blob.size, (blob.size / 1024 / 1024).toFixed(2) + 'M');
  13. if (options.blob) return blob
  14. saveAs(blob, options.fileName + ".doc");
复制代码
saveAs

  1. export function saveAs(blob, fileName) {
  2.         var URL = window.URL || window.webkitURL;
  3.         var a = document.createElement('a');
  4.         fileName = fileName || blob.name || 'download';
  5.         a.download = fileName;
  6.         a.rel = 'noopener';
  7.         a.target = '_blank'
  8.         if (typeof blob === 'string') {
  9.                 a.href = blob;
  10.                 a.click()
  11.         } else {
  12.                 a.href = URL.createObjectURL(blob);
  13.                 setTimeout(() => a.click(), 0);
  14.                 setTimeout(() => URL.revokeObjectURL(a.href), 2E4); // 20s
  15.         }
  16. }
复制代码
使用



  • 详见example/index.html
  1. <script src="../dist/export-doc.js"></script>
  2. <script>
  3. ExportDoc.toWord('#test-word', {
  4.         title: '关于导出王麻子这件事情',
  5.         time: '2024年8月15日 上午10点',
  6.         fileName: '剑来.doc',
  7.         exclude: ['.not-export', '.not-export1']
  8. })
  9. </script>
复制代码
后言



  • 将元素移入新的iframe并且保存样式思考了下, 且canvas在cloneNode的时候不会将内容拷贝过去, 这点采坑
  • 后期将美满纯前端导出excel
源码地址



  • 源码和程序截图详见https://github.com/dhjz/export-doc
  • 示例 http://199311.xyz/export-doc/example/index.html

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

九天猎人

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表