前端开发那些事儿

前端导出word的两种方案

2020-06-19  本文已影响0人  人猿Jim
hello world

继导出pdf之后,需求又来了个导出word的需求(你开心就好)
于是在无数次尝试后,总结用到的两个word的方案
word导出比较坑的点是图片的导出,一般的库进行导出必须要把图片转成base64才能导出,所以如果dom元素中存在图片,要进行图片转base64的处理,而涉及到图片转base64(甚至是svg转base64),还有必须提到一点就是blob的存储操作,当然图方便你可以装一个flieSaver就不用自己写原生blob保存了

图片转base64

       //获取div下的img标签转base64
       const url = 'https://img.haomeiwen.com/i14626058/37ab230cd97ccf87.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp'
       const imgSection = document.querySelector('#test-img')
      // 通过构造函数来创建的 img 实例,在赋予 src 值后就会立刻下载图片,相比 createElement() 创建 <img> 省去了 append(),也就避免了文档冗余和污染
       let Img = new Image();
       Img.setAttribute('crossOrigin', 'Anonymous') // 配置图片跨越
       Img.src = url;
       let dataURL = '';
       Img.onload = () => { 
          let canvas = document.createElement('canvas'); // 创建canvas元素
          let width = Img.width; // 确保canvas的尺寸和图片一样
          let height = Img.height;
          canvas.width = width;
          canvas.height = height;
          canvas.getContext('2d').drawImage(Img, 0, 0, width, height); // 将图片绘制到canvas中
          dataURL = canvas.toDataURL('image/jpeg'); // 转换图片为dataURL
          console.log(dataURL)
          imgSection.children[0].setAttribute('src',dataURL)
       };

html2canvas或者domtoimage

通过html2canvas.js将dom元素"截图",在函数参数的canvas调用canvas.toDataURL方法,可以直接转base64,然后再配合word导出

// import html2canvas from 'html2canvas'
import domtoimage from 'dom-to-image'
// 将dom元素下的img的src转为base64
const convertImagesToBase64 = (contentDocument, imgWidth, imgHeight) => {
  if (typeof contentDocument === 'string') {
    contentDocument = document.querySelector(contentDocument)
  }

  var regularImages = contentDocument.querySelectorAll('img')
  var canvas = document.createElement('canvas')
  var ctx = canvas.getContext('2d');
  [].forEach.call(regularImages, function(imgElement) {
    imgElement.width = imgWidth || 100
    imgElement.height = imgHeight || 140
    // preparing canvas for drawing
    ctx.clearRect(0, 0, canvas.width, canvas.height)

    const img = new Image() // 创建新的图片对象
    img.src = imgElement.src
    img.setAttribute('crossOrigin', 'Anonymous') // 配置图片跨越
    img.onload = function() { // 图片加载完,再draw 和 toDataURL
      ctx.drawImage(img, 0, 0, imgElement.width, imgElement.height)
      const base64 = canvas.toDataURL('image/png')
      imgElement.setAttribute('src', base64)
    }
  })
  canvas.remove()
}

// 使用domtoimage将html转为base64的图片
const convertHtmlToBase64 = (contentDocument, callBack) => {
  if (typeof contentDocument === 'string') {
    contentDocument = document.querySelector(contentDocument)
  }
  domtoimage.toPng(contentDocument).then(function(dataUrl) {
    callBack(dataUrl)
  })
}

// 使用事例
convertHtmlToBase64('#example', function(imageBase64) {
  const img = new Image()
  img.src = imageBase64

  //dom.appendChild(img)
})

export {
  convertImagesToBase64,
  convertHtmlToBase64
}

一行琉璃

1.html-docx-js

这是一个叫html-docx-js的库,能够将指定dom元素(最好是完整的html,包括 DOCTYPE,html 和 body 标记)进行word导出,注意需要把图片转成base64,而且dom的样式必须写在style里面,导出时才能检测到。
库地址:https://github.com/evidenceprime/html-docx-js,用于参考html-docx-js的页面调整参数

//封装了一个exportDocx方法
// 注意:file-saver 依赖 Blob 对象
import { saveAs } from 'file-saver'
// htmlDocx 的作用就是将html字符串转成Blob对象
import htmlDocx from 'html-docx-js/dist/html-docx'

// 测试函数
export const exportDocxTest = (domString, fileName) => {
  var converted = htmlDocx.asBlob(domString)
  // var blob = new Blob(['Hello, world!'], { type: 'text/plain;charset=utf-8' })
  // saveAs(blob, 'test.docx')
  saveAs(converted, fileName + '.docx')
}

/**
 * 1.导出局部的html页面
 * @param {} dom 局部的html页面
 * @param {*} fileName 导出文件的名称
 * @param {*} title
 */
export const exportDocx = (dom, fileName, config, { title = document.title, width } = {}) => {
  if (!dom) return
  config = config || {}
  let copyDom = document.createElement('span')
  // const styleDom = document.querySelectorAll('style, link, meta')
  const titleDom = document.createElement('title')
  titleDom.innerText = title

  copyDom.appendChild(titleDom)
  // Array.from(styleDom).forEach(item => {
  //   copyDom.appendChild(item.cloneNode(true))
  // })
  const cloneDom = dom.cloneNode(true)
  if (width) {
    const domTables = cloneDom.getElementsByTagName('table')
    if (domTables.length) {
      for (const table of domTables) {
        table.style.width = width + 'px'
      }
    }
  }
  copyDom.appendChild(cloneDom)

  const htmlTemp = copyDom.innerHTML
  copyDom = null
  // console.log('htmlTemp=', htmlTemp)
  const iframeDom = document.createElement('iframe')
  const attrObj = {
    height: 0,
    width: 0,
    border: 0,
    wmode: 'Opaque'
  }
  const styleObj = {
    position: 'absolute',
    top: '-999px',
    left: '-999px'
  }
  Object.entries(attrObj).forEach(([key, value]) => {
    iframeDom.setAttribute(key, value)
  })
  Object.entries(styleObj).forEach(([key, value]) => {
    iframeDom.style[key] = value
  })
  document.body.insertBefore(iframeDom, document.body.children[0])
  const iframeWin = iframeDom.contentWindow // 1.获取iframe中的window
  const iframeDocs = iframeWin.document // 2.获取iframe中的document
  iframeDocs.write(`<!doctype html>`)
  iframeDocs.write(htmlTemp)

  const htmlDoc = `
  <!DOCTYPE html>
  <html lang="en">
  ${iframeDocs.documentElement.innerHTML}
  </html>
  `
  var converted = htmlDocx.asBlob(htmlDoc, config)
  saveAs(converted, fileName + '.docx')
  document.body.removeChild(iframeDom)
}


// 使用事例
exportDocx(document.querySelector('#excep-detail-p-basic-info'),
  '服刑人员个人资料-' + getDateTimeStr(),
  {
    // orientation: 'landscape', margins: { top: 150 }
      margins: { top: 150 }
   }
)
一行琉璃

2.jquery.wordexport

Jquery,永远滴神。如果你考虑兼容的问题(不单止浏览器的兼容,还涉及到word版本的兼容),那首选当然是jquery.wordexport.js,同样样式需要style写入,而且dom需要转成jquery对象,使用的时候$("#example").wordExport()方法就行


结构

jquerys.js(JQ引入)
vue项目可以配置webpack的plugin全局引入jq,也可以单独定义jquerys.js文件

// 两种方法选一种

// 1.新建js文件
import $ from "jquery";
window.$ = $;
window.jQuery = $;
export default $;

// 2.webpack全局引入
const isProduction = process.env.NODE_ENV === 'production'
module.exports = {
   ...
  configureWebpack: (config) => {
    if (isProduction) {
      // 1.排除哪些库不需要打包 import Vue from 'vue'
      // 用cdn方式引入
      config.externals = {
        // 'vue': 'Vue', // key 是 require 的包名,value 是全局的变量
        // 'vuex': 'Vuex',
        'jQuery': 'jQuery'
      }
      // 2.减少 vendor-jsxxxx.js的大小
      config.optimization = {
        splitChunks: {
          cacheGroups: {
            commons: {
              name: 'commons',
              chunks: 'initial',
              minChunks: 2
            },
            vendors: {
              test: /[\\/]node_modules[\\/]/,
              name: 'vendors',
              chunks: 'all',
              minSize: 540000,
              maxSize: 840000
              // priority: 4
            }
          }
        }
      }
    // 开发环境
    } else {
      // 1.直接引入第三方法的框架
      config.externals = {
        'jQuery': 'jQuery'
      }
    }
  }
}

jquery.wordexport.js

import { saveAs } from "file-saver";

if (typeof jQuery !== "undefined" && typeof saveAs !== "undefined") {
    (function ($) {
        $.fn.wordExport = function (fileName) {
            fileName = typeof fileName !== 'undefined' ? fileName : "jQuery-Word-Export";
            var statics = {
                mhtml: {
                    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  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\'>\n<html  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\'>\n_html_</html>",
                    head: "<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n<style>\n_styles_\n</style>\n<!--[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]-->\n<style>.ql-align-center{text-align:center;}</style>\n</head>\n",
                    body: "<body>_body_</body>"
                }
            };
            var options = {
                maxWidth: 624
            };
            // Clone selected element before manipulating it
            var markup = $(this).clone();
 
            // Remove hidden elements from the output
            markup.each(function () {
                var self = $(this);
                if (self.is(':hidden'))
                    self.remove();
            });
 
            // Embed all images using Data URLs
            var images = Array();
            var img = markup.find('img');
            for (var i = 0; i < img.length; i++) {
                // Calculate dimensions of output image
                var w = Math.min(img[i].width, options.maxWidth);
                var h = img[i].height * (w / img[i].width);
                // Create canvas for converting image to data URL
                var canvas = document.createElement("CANVAS");
                canvas.width = w;
                canvas.height = h;
                // Draw image to canvas
                var context = canvas.getContext('2d');
                context.drawImage(img[i], 0, 0, w, h);
                // Get data URL encoding of image
                var uri = canvas.toDataURL("image/png/jpg");
                $(img[i]).attr("src", img[i].src);
                img[i].width = w;
                img[i].height = h;
                // Save encoded image to array
                images[i] = {
                    type: uri.substring(uri.indexOf(":") + 1, uri.indexOf(";")),
                    encoding: uri.substring(uri.indexOf(";") + 1, uri.indexOf(",")),
                    location: $(img[i]).attr("src"),
                    data: uri.substring(uri.indexOf(",") + 1)
                };
            }
 
            // Prepare bottom of mhtml file with image data
            var mhtmlBottom = "\n";
            for (var i = 0; i < images.length; i++) {
                mhtmlBottom += "--NEXT.ITEM-BOUNDARY\n";
                mhtmlBottom += "Content-Location: " + images[i].location + "\n";
                mhtmlBottom += "Content-Type: " + images[i].type + "\n";
                mhtmlBottom += "Content-Transfer-Encoding: " + images[i].encoding + "\n\n";
                mhtmlBottom += images[i].data + "\n\n";
            }
            mhtmlBottom += "--NEXT.ITEM-BOUNDARY--";
 
            //TODO: load css from included stylesheet
 
            //var styles=' /* Font Definitions */@font-face{font-family:宋体;panose-1:2 1 6 0 3 1 1 1 1 1;mso-font-alt:SimSun;mso-font-charset:134;mso-generic-font-family:auto;mso-font-pitch:variable;mso-font-signature:3 680460288 22 0 262145 0;}  @font-face{font-family:"Cambria Math";panose-1:2 4 5 3 5 4 6 3 2 4;mso-font-charset:1;mso-generic-font-family:roman;mso-font-format:other;mso-font-pitch:variable;mso-font-signature:0 0 0 0 0 0;}  @font-face{font-family:"\@宋体";panose-1:2 1 6 0 3 1 1 1 1 1;mso-font-charset:134;mso-generic-font-family:auto;mso-font-pitch:variable;mso-font-signature:3 680460288 22 0 262145 0;}/* Style Definitions */p.MsoNormal, li.MsoNormal, div.MsoNormal{mso-style-unhide:no;mso-style-qformat:yes;mso-style-parent:"";margin:0cm;margin-bottom:.0001pt;mso-pagination:widow-orphan;font-size:14.0pt;font-family:宋体;mso-bidi-font-family:宋体;}p.MsoHeader, li.MsoHeader, div.MsoHeader{mso-style-noshow:yes;mso-style-priority:99;mso-style-link:"页眉 Char";margin:0cm;margin-bottom:.0001pt;text-align:center;mso-pagination:widow-orphan;layout-grid-mode:char;font-size:9.0pt;font-family:宋体;mso-bidi-font-family:宋体;}p.MsoFooter, li.MsoFooter, div.MsoFooter{mso-style-noshow:yes;mso-style-priority:99;mso-style-link:"页脚 Char";margin:0cm;margin-bottom:.0001pt;mso-pagination:widow-orphan;layout-grid-mode:char;font-size:9.0pt;font-family:宋体;mso-bidi-font-family:宋体;}p.MsoAcetate, li.MsoAcetate, div.MsoAcetate{mso-style-noshow:yes;mso-style-priority:99;mso-style-link:"批注框文本 Char";margin:0cm;margin-bottom:.0001pt;mso-pagination:widow-orphan;font-size:9.0pt;font-family:宋体;mso-bidi-font-family:宋体;}span.Char{mso-style-name:"页眉 Char";mso-style-noshow:yes;mso-style-priority:99;mso-style-unhide:no;mso-style-locked:yes;mso-style-link:页眉;font-family:宋体;mso-ascii-font-family:宋体;mso-fareast-font-family:宋体;mso-hansi-font-family:宋体;}span.Char0{mso-style-name:"页脚 Char";mso-style-noshow:yes;mso-style-priority:99;mso-style-unhide:no;mso-style-locked:yes;mso-style-link:页脚;font-family:宋体;mso-ascii-font-family:宋体;mso-fareast-font-family:宋体;mso-hansi-font-family:宋体;}span.Char1{mso-style-name:"批注框文本 Char";mso-style-noshow:yes;mso-style-priority:99;mso-style-unhide:no;mso-style-locked:yes;mso-style-link:批注框文本;font-family:宋体;mso-ascii-font-family:宋体;mso-fareast-font-family:宋体;mso-hansi-font-family:宋体;}p.msochpdefault, li.msochpdefault, div.msochpdefault{mso-style-name:msochpdefault;mso-style-unhide:no;mso-margin-top-alt:auto;margin-right:0cm;mso-margin-bottom-alt:auto;margin-left:0cm;mso-pagination:widow-orphan;font-size:10.0pt;font-family:宋体;mso-bidi-font-family:宋体;}span.msonormal0{mso-style-name:msonormal;mso-style-unhide:no;}.MsoChpDefault{mso-style-type:export-only;mso-default-props:yes;font-size:10.0pt;mso-ansi-font-size:10.0pt;mso-bidi-font-size:10.0pt;mso-ascii-font-family:"Times New Roman";mso-hansi-font-family:"Times New Roman";mso-font-kerning:0pt;}/* Page Definitions */  @page WordSection1{size:595.3pt 841.9pt;margin:72.0pt 90.0pt 72.0pt 90.0pt;mso-header-margin:42.55pt;mso-footer-margin:49.6pt;mso-paper-source:0;}div.WordSection1{page:WordSection1;}';
 
            var styles = "";
 
            // Aggregate parts of the file together
            var fileContent = statics.mhtml.top.replace("_html_", statics.mhtml.head.replace("_styles_", styles) + statics.mhtml.body.replace("_body_", markup.html())) + mhtmlBottom;
 
            // Create a Blob with the file contents
            var blob = new Blob([fileContent], {
                type: "application/msword;charset=utf-8"
            });
            saveAs(blob, fileName + ".doc");
        };
    })(jQuery);
} else {
    if (typeof jQuery === "undefined") {
        console.error("jQuery Word Export: missing dependency (jQuery)");
    }
    if (typeof saveAs === "undefined") {
        console.error("jQuery Word Export: missing dependency (FileSaver.js)");
    }
}

组件调用

<template>
  <div
    class="htmlToPdf"
    id="export_word"
    style="width:1000px;margin:0 auto;line-height: 25px;"
  >
    <div style="color: #333; position: relative;padding: 30px;">
      <div style="height: 1320px; padding-top: 100px;text-align: center;">
        <h2 style="line-height: 50px;">
         // 标题
          <span>{{ wordTitle }}</span>
        </h2>
      </div>
      <div style="margin-top:40px" v-html="editContent"></div>
    </div>
  </div>
</template>
<script>
import $ from "./js/jquerys"
// import $ from 'jQuery'
import "./js/jquery.wordexport"
export default {
  props: {
    editContent: {
      type: String,
      default: () => {
        return ""
      },
    },
    wordTitle: {
      type: String,
      default: () => {
        return ""
      },
    }
  },
  methods: {
    exportWord() {
      // 等待dom渲染完成再导出
      this.$nextTick(() => {
        $("#export_word").wordExport(this.wordTitle);
      }, 500);
    }
  }
}
</script>
上一篇 下一篇

猜你喜欢

热点阅读