前端导出的两种技术方案:模板导出和页面直接导出,vue3+ts

一,前端HTML转canvas技术方案

这种方案主要是利用现有模板编写html页面样式后将HTML转成canvas图片导出,类似于页面截图,好处是可以自定义页面样式导出且过程较为简单,缺点就是无法1:1还原标准模板样式,有一定偏差需要兼容页面大小且无法满足直接导出word

1.安装依赖

npm install --save html2canvas // 页面转图片
npm install jspdf --save // 图片转pdf

2.示例代码带水印添加

import html2Canvas from 'html2canvas'
import JsPDF from 'jspdf'
// title:下载文件的名称  htmlId:包裹的标签的id
const htmlToPdf = (title: string, htmlId: string) => {
  var element = document.querySelector(htmlId) as HTMLElement
  window.pageYOffset = 0
  document.documentElement.scrollTop = 0
  document.body.scrollTop = 0
  setTimeout(() => {
    // // 以下注释的是增加导出的pdf水印
    // const value = '我是水印'
    // //创建一个画布
    // let can = document.createElement('canvas')
    // //设置画布的长宽
    // can.width = 400
    // can.height = 500
 
    // let cans = can.getContext('2d') as any
    // //旋转角度
    // cans.rotate((-15 * Math.PI) / 180)
    // cans.font = '18px Vedana'
    // //设置填充绘画的颜色、渐变或者模式
    // cans.fillStyle = 'rgba(200, 200, 200, 0.40)'
    // //设置文本内容的当前对齐方式
    // cans.textAlign = 'left'
    // //设置在绘制文本时使用的当前文本基线
    // cans.textBaseline = 'Middle'
    // //在画布上绘制填色的文本(输出的文本,开始绘制文本的X坐标位置,开始绘制文本的Y坐标位置)
    // cans.fillText(value, can.width / 8, can.height / 2)
    // let div = document.createElement('div')
    // div.style.pointerEvents = 'none'
    // div.style.top = '20px'
    // div.style.left = '-20px'
    // div.style.position = 'fixed'
    // div.style.zIndex = '100000'
    // div.style.width = element.scrollHeight + 'px'
    // div.style.height = element.scrollHeight + 'px'
    // div.style.background =
    //   'url(' + can.toDataURL('image/png') + ') left top repeat'
    // element.appendChild(div) // 到页面中
 
    html2Canvas(element, {
      allowTaint: true,
      useCORS: true,
      scale: 2, // 提升画面质量,但是会增加文件大小
      height: element.scrollHeight, // 需要注意,element的 高度 宽度一定要在这里定义一下,不然会存在只下载了当前你能看到的页面   避雷避雷!!!
      windowHeight: element.scrollHeight,
    }).then(function (canvas) {
      var contentWidth = canvas.width
      var contentHeight = canvas.height
      // console.log('contentWidth', contentWidth)
      // console.log('contentHeight', contentHeight)
      // 一页pdf显示html页面生成的canvas高度;
      var pageHeight = (contentWidth * 841.89) / 592.28
      // 未生成pdf的html页面高度
      var leftHeight = contentHeight
 
      // console.log('pageHeight', pageHeight)
      // console.log('leftHeight', leftHeight)
      // 页面偏移
      var position = 0
      // a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高  //40是左右页边距
      var imgWidth = 595.28 - 40
      var imgHeight = (592.28 / contentWidth) * contentHeight
 
      var pageData = canvas.toDataURL('image/jpeg', 1.0)
 
      var pdf = new JsPDF('p', 'pt', 'a4')
 
      // 有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)
      // 当内容未超过pdf一页显示的范围,无需分页
      if (leftHeight < pageHeight) {
        // console.log('没超过1页')
        pdf.addImage(pageData, 'JPEG', 20, 20, imgWidth, imgHeight)
      } else {
        while (leftHeight > 0) {
          // console.log('超过1页')
          pdf.addImage(pageData, 'JPEG', 20, position, imgWidth, imgHeight)
          leftHeight -= pageHeight
          position -= 841.89
          // 避免添加空白页
          if (leftHeight > 0) {
            pdf.addPage()
          }
        }
      }
      pdf.save(title + '.pdf')
    })
  }, 1000)
}
 
export default htmlToPdf

页面方法使用

import htmlToPdf from '@/utils/pdf'//引入封装好的ts文件
   const exportPdf = (text:string) => {
    htmlToPdf(text, '#exportWrapper')
  }

二,利用docxtemplater插件配合模板导出

适用于提供模板来导出,且对文档格式和排版有有严格要求的导出

1.需要使用安装的依赖

npm install docxtemplater
npm install pizzip
npm install jszip
npm install jszip-utils
npm install file-saver
npm install docxtemplater-image-module-free
npm install angular-expressions
npm install docx-preview

2.模板创建和书写

需要注意的:
(1)文档模板需使用docx文件格式,原因是docx与zip是可以相互转换的,但doc则不行,因为后续需要借助插件将模板转换成zip
(2)文档需放置于项目public文件夹下
在这里插入图片描述

(3)模板的书写规则和数据源的格式需借助angular-parser 词法解析器使用,具体格式和复杂写法可参考这个博客https://blog.csdn.net/CHANCE_wqp/article/details/133457540

3.模板读取和写入

使用PizZip解压缩读取成二进制,再使用Docxtemplater插件将模板字符替换成数据源抛出blob文件流
需要注意的是模板中图片需要转换成base64图片后再处理,如果数据源中图片为url也需要先将链接的图片转换成base64具体转换代码见下面完整代码实例


async function transformWord(data: any, callback: Function) {
  // 读取并获得模板文件的二进制内容
  function loadFile(url: string, callback: (error: any, content: any) => void) {
    PizZipUtils.getBinaryContent(url, callback)
  }

  // orderTemeplate.docx是模板。我们在导出的时候,会根据此模板来导出对应的数据
  await loadFile("/orderTemeplate.docx", function (error: Error | null, content) {
    // 抛出异常
    if (error) {
      throw error
    }
    console.log(content)

    const opts = {
      centered: true,
      fileType: "docx"
    }
    // @ts-ignore
    opts.getImage = (imagePath) => {
      if (imagePath.size && imagePath.data) {
        return base64DataURLToArrayBuffer(imagePath.data)
      }
      return base64DataURLToArrayBuffer(imagePath)
    }
    // @ts-ignore
    opts.getSize = () => {
      return [160, 80]
    }

    // 创建一个JSZip实例,内容为模板的内容
    const zip: PizZip = new PizZip(content)
    const doc = new Docxtemplater()
    doc.attachModule(new ImageModule(opts))
    doc.loadZip(zip)
    // 设置模板变量的值
    doc.setData({
      ...data
    })
    doc.setOptions({
      nullGetter: function () {
        //设置空值 undefined 为""
        return ""
      },
      parser: angularParser
    })
    try {
      // 用模板变量的值替换所有模板变量
      doc.render()
    } catch (error: any) {
      throw error
      // 当使用json记录时,此处抛出错误信息
    }
    // 生成一个代表docxtemplater对象的zip文件(不是一个真实的文件,而是在内存中的表示)
    const out = doc.getZip().generate({
      type: "blob",
      mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
    })
    callback(out)
  })
}

4.异步调用下载

直接下载文件

export const exportWordDocx = async (data: any, fileName: string) => {
  transformWord(data, (out: any) => {
    saveAs(out, fileName + ".docx")
  })
}

如果需要添加预览功能可使用docx-preview的renderAsync进行预览后续同样可使用方案一方式直接导出pdf

export const openFile = async (data: any) => {
  transformWord(data, (out: any) => {
    const container = document.getElementById("doc-preview") as HTMLElement
    renderAsync(out, container, null, {
      // renderChanges: true
      useBase64URL: true,
      ignoreWidth: true
    })
  })
}

5.完整示例代码

/**
 * 前端导出word
 * @param {object} data - 字段数据,需与文档模板字段保持一致
 * @param {number} fileName - 文件名
 * @returns {Blob} 文件流
 */
import Docxtemplater from "docxtemplater"
import PizZip from "pizzip"
import PizZipUtils from "pizzip/utils/index.js"
import { saveAs } from "file-saver"
import ImageModule from "docxtemplater-image-module-free"
import expressions from "angular-expressions"
import { renderAsync } from "docx-preview"

export const exportWordDocx = async (data: any, fileName: string) => {
  transformWord(data, (out: any) => {
    saveAs(out, fileName + ".docx")
  })
}

export const openFile = async (data: any) => {
  transformWord(data, (out: any) => {
    const container = document.getElementById("doc-preview") as HTMLElement
    renderAsync(out, container, null, {
      // renderChanges: true
      useBase64URL: true,
      ignoreWidth: true
    })
  })
}

async function transformWord(data: any, callback: Function) {
  // 读取并获得模板文件的二进制内容
  function loadFile(url: string, callback: (error: any, content: any) => void) {
    PizZipUtils.getBinaryContent(url, callback)
  }

  // orderTemeplate.docx是模板。我们在导出的时候,会根据此模板来导出对应的数据
  await loadFile("/orderTemeplate.docx", function (error: Error | null, content) {
    // 抛出异常
    if (error) {
      throw error
    }
    console.log(content)

    const opts = {
      centered: true,
      fileType: "docx"
    }
    // @ts-ignore
    opts.getImage = (imagePath) => {
      if (imagePath.size && imagePath.data) {
        return base64DataURLToArrayBuffer(imagePath.data)
      }
      return base64DataURLToArrayBuffer(imagePath)
    }
    // @ts-ignore
    opts.getSize = () => {
      return [160, 80]
    }

    // 创建一个JSZip实例,内容为模板的内容
    const zip: PizZip = new PizZip(content)
    const doc = new Docxtemplater()
    doc.attachModule(new ImageModule(opts))
    doc.loadZip(zip)
    // 设置模板变量的值
    doc.setData({
      ...data
    })
    doc.setOptions({
      nullGetter: function () {
        //设置空值 undefined 为""
        return ""
      },
      parser: angularParser
    })
    try {
      // 用模板变量的值替换所有模板变量
      doc.render()
    } catch (error: any) {
      throw error
      // 当使用json记录时,此处抛出错误信息
    }
    // 生成一个代表docxtemplater对象的zip文件(不是一个真实的文件,而是在内存中的表示)
    const out = doc.getZip().generate({
      type: "blob",
      mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
    })
    callback(out)
  })
}

/**
 * 将base64格式的数据转为ArrayBuffer
 * @param {Object} dataURL base64格式的数据
 */
function base64DataURLToArrayBuffer(dataURL: string) {
  const base64Regex = /^data:image\/(png|jpg|svg|svg\+xml);base64,/
  if (!base64Regex.test(dataURL)) {
    return false
  }
  const stringBase64 = dataURL.replace(base64Regex, "")
  let binaryString
  if (typeof window !== "undefined") {
    binaryString = window.atob(stringBase64)
  } else {
    binaryString = new Buffer(stringBase64, "base64").toString("binary")
  }
  const len = binaryString.length
  const bytes = new Uint8Array(len)
  for (let i = 0; i < len; i++) {
    const ascii = binaryString.charCodeAt(i)
    bytes[i] = ascii
  }
  return bytes.buffer
}

/**
 * 将图片的url路径转为base64路径
 * 可以用await等待Promise的异步返回
 * @param {Object} imgUrl 图片路径
 */
export function getBase64Sync(imgUrl: string) {
  return new Promise(function (resolve) {
    // 一定要设置为let,不然图片不显示
    const image = new Image()
    //图片地址
    image.src = imgUrl
    // 解决跨域问题
    image.setAttribute("crossOrigin", "*") // 支持跨域图片
    // image.onload为异步加载
    image.onload = function () {
      const canvas = document.createElement("canvas")
      canvas.width = image.width
      canvas.height = image.height
      const context = canvas.getContext("2d")
      context?.drawImage(image, 0, 0, image.width, image.height)
      //图片后缀名
      const ext = image.src.substring(image.src.lastIndexOf(".") + 1).toLowerCase()
      //图片质量
      const quality = 0.8
      //转成base64
      const dataurl = canvas.toDataURL("image/" + ext, quality)
      //返回
      resolve(dataurl)
    }
  })
}
//处理文档中的一些特殊标签
function angularParser(tag: string) {
  return {
    get:
      tag === "."
        ? function (s: any) {
            return s
          }
        : function (s: any) {
            return expressions.compile(tag.replace(/(’|“|”)/g, "'"))(s)
          }
  }
}

6.页面中的使用

import { openFile, getBase64Sync } from "@/hooks/exportWord.ts"

  await getData() //接口获取数据
  if (data.customerSignature) {
    //图片链接转base64
    data.customerSignature = await getBase64Sync(FILESERVER_URL + data.customerSignature)
  }
  openFile(data) //直接传入数据源字段
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值