1. 前情
在后台管理平台开发过程中,有这样一个需求:从后端获取到数据后,前端生成指定模板内容的word文件并下载到本地。当导出的文件过多时,为了减少响应时间,也加上了进行压缩下载部分。使用过程中可根据实际需求作出调整。
2. 准备
wqqqq
依赖
js
npm install docxtemplater
npm install jszip
npm install pizzip
npm install file-saver
npm install jszip-utils
npm install angular-expressions
模板文件
需要按导出样式准备一个模板文件,放到 public 目录下。模板中占位符包含以下部分:
- 单一变量:{变量名}。直接显示改变量的值。
- 遍历:{#变量名}内容{/变量名}。会对该数据进行遍历展示
- 显示/隐藏:{#变量名}内容{/变量名}。其中为true的时候显示,false则不显示。
- 图片:{%变量名}。变量值为base64格式。
- if-else:{#变量名}A{/变量名}{^变量名}B{/变量名}。其中值为true的时候显示"A",为false显示"B";
- 复选框:{#变量名}选中{/变量名}{^变量名}非选中{/变量名} 以上部分是我在实际需求中使用到的,其他使用可参考:docxtemplater.com/docs/
js
let obj={
name:'张三',
age:12,
hobby:['basketball','swimming'],
url:'xxxxxxxxxxxxxx.png',
isPic:false
}
这里定一个名为 obj 的对象,接下来按照下面的模板生成:

3. 实现
docFiles.js
js
import { imageHandle } from '../common/image'
import { saveAs } from 'file-saver';
import JsZip from 'jszip'
import PizZip from "pizzip";
import docxtemplater from "docxtemplater";
import JSZipUtils from "jszip-utils";
import expressions from "angular-expressions";
let promises: any[] = [];
下载单个文件
js
/**导出单个word文件
* @param {object} opts 配置项
*/
export const exportDocx = (opts) => {
let {
//模板文件路径(必填)
tempDocxpath,
//模板文件数据(必填)
data,
//导出文件名
fileName,
//是否包含图片
imageable,
//导出成功后的回调
onSuccess,
//导出失败后的回调
onError,
} = Object.assign({
imageable: false,
fileName:'新建文件1',
}, opts)
const promise = new Promise((resolver, reject) => {
JSZipUtils.getBinaryContent(tempDocxpath, (error, content) => {
if (error) {
throw error;
}
expressions.filters.size = function (input, width, height) {
return {
data: input,
size: [width, height],
};
}
//创建一个PiZip示例,内容为模板的内容
const zip = new PizZip(content);
//创建并加载docxtemplater实例对象
let doc = new docxtemplater();
if (imageable) {
let opts = imageHandle()
doc.attachModule(new ImageModule(opts));
}
doc.loadZip(zip);
doc.setData(data);
try {
//用模板变量的值替换所有模板变量
doc.render();
} catch (error) {
const e = {
"message": error.message,
"name": error.name,
"stack": error.stack,
"properties": error.properties
};
console.log({ error: e });
throw error;
}
//生成一个代表docxtemplater对象的zip文件(在内存中表示)
const out = doc.getZip().generate({
type: "blob",
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
});
resolver(out);
})
})
Promise.all([promise]).then((out) => {
if(onSuccess) onSuccess()
saveAs(out, fileName);
}).catch(() => {
if(onError) onError()
})
}
压缩包形式批量下载
js
/**多级目录的形式导出文件
* @param {object} opts 配置项
*/
export const exportFile_MultiLevelDirectory = (opts) => {
let {
//模板文件路径
tempDocxpath,
//模板文件数据
data,
//一级目录名
zipName,
//下级目录对应数据路径
path,
//下级目录名称类型
subFolderNameType,
//文件名称类型
fileNameType,
//是否包含图片
imageable,
//导出成功后的回调
onSuccess,
//导出失败后的回调
onError,
} = Object.assign({
imageable: false,
zipName:'新建文件1'
}, opts)
const zips = new JsZip();
//创建一级目录的压缩包
const folder = zips.folder(zipName);
let creatOpts = {
tempDocxpath,
data,
path,
fileNameType,
subFolderNameType,
imageable,
folder,
zips,
}
create_Directory(creatOpts)
Promise.all(promises).then(() => {
zips.generateAsync({ type: "blob" }).then(content => {
//生成二进制流
if(onSuccess) onSuccess()
saveAs(content, zipName);
}).catch(() => {
if(onError) onError()
})
})
}
/**创建下级目录
* @param {object} opts 配置项
*/
function create_Directory(opts) {
let {
tempDocxpath,
data,
path,
folder,
subFolderNameType,
fileNameType,
imageable,
zips
} = opts
//遍历数据,依次形成下级目录/文件
data.forEach((item, index) => {
if (item[path] || item[path]?.length) {
item[path].forEach((item2, index2) => {
//创建下级目录
let subFolderName = item2.folderName
let subFolder = folder.folder(subFolderName);
let creatOpts = {
tempDocxpath,
data: item[path],
fileNameType,
subFolderNameType,
imageable,
zips,
folder: subFolder
}
create_Directory(creatOpts)
})
}else {
const fileName = item.fileName
const promise = new Promise((resolver, reject) => {
JSZipUtils.getBinaryContent(tempDocxpath, (error, content) => {
if (error) {
throw error;
}
expressions.filters.size = function (input, width, height) {
return {
data: input,
size: [width, height],
};
};
const zip = new PizZip(content);
let doc = new docxtemplater();
if (imageable) {
let opts = imageHandle()
doc.attachModule(new ImageModule(opts));
}
doc.loadZip(zip);
doc.setData(item);
try {
doc.render();
} catch (error) {
const e = {
"message": error.message,
"name": error.name,
"stack": error.stack,
"properties": error.properties
};
console.log({ error: e });
throw error;
}
const out = doc.getZip().generate({
type: "blob",
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
});
folder.file(fileName, out, { binary: true });
resolver('success');
});
})
promises.push(promise);
}
}
}
注意:导出多个word文件时需注意文档命名问题,避免因名称重复产生的文件覆盖问题。
image.js
js
/**
* 图片处理选项配置生成函数。
* @returns {Object} 返回一个包含图片处理选项的对象。
*/
export const imageHandle = () => {
// 图片处理
let opts = {}
opts = {
//图像是否居中
centered: false
};
opts.getImage = (chartId) => {
// 将base64的数据转为ArrayBuffer
return base64DataURLToArrayBuffer(chartId);
}
opts.getSize = function (img, tagValue, tagName) {
//自定义指定图像大小
return [500, 400];
}
return opts
}
/**
* 将base64格式的数据转为ArrayBuffer
* @param {Object} dataURL base64格式的数据
*/
export function base64DataURLToArrayBuffer(dataURL) {
const base64Regex = /^data:image\/(png|jpg|jpeg|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;
}
应用页面文件
注释部分为批量导出
js
<template>
<div>
<n-button type="primary" @click="downWord">导出word</el-button>
</div>
</template>
<script setup>
let obj={
name:'张三',
images:[]
}
/*let objList=[
{
folderName:'济南市',
children:[
{
fileName:'张村申请合同',
name:'张村',
count:72
},
{
fileName:'大王村申请合同',
name:'大王村',
count:56
},
]
},
{
folderName:'德州市',
children:[
{
fileName:'高家村申请合同',
name:'高家村',
count:10
},
]
},
{
fileName:'居户里村申请合同',
name:'居户里村',
count:74
},
]*/
function downWord(){
let opts={
tempDocxpath:'/moBan.docx',
data:obj,
fileName:'申请表'
}
exportDocx(opts)
/*let opts={
tempDocxpath:'/moBan.docx',
data:objList,
zipName:'合同申请'
}*/
//exportFile_MultiLevelDirectory(opts)
}
</script>
4. 问题
(1)End of data reached (data length = 0, asked index = 4). Corrupted zip ?
原因:模板文件为空文件;
排查:检查模板文件引入是否正确
(2)导出的文件中图片显示undefined
原因:图片路径转换成base64后的格式不对
(3)文件数量不对
排查:是不是文件名重复覆盖导致数量不对