一,前端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) //直接传入数据源字段