本文将记录如何用js
生成word
文件,并在node服务器
端将word
转换成pdf
。记录的代码均是在真实业务场景中使用成功的代码,没有记录中间踩坑的过程。想直接抄答案的家人们可以跳转到1.2 程序编写
部分,最终效果图可在1.2 程序编写
部分中4. 效果展示
模块查看。
如果有更好的解决方案,也欢迎大家在评论区讨论、分享~
本文demo存放地址:github.com/ChicKo1108/…
一、DocxTemplater:使用js生成word
老铁们,话不多说,先上链接:Docxtemplater | Word, Powerpoint, Excel generation using templates in your application | docxtemplater
DocxTemplater
是一个基于模板
生成最终文件的插件,它通过一个简单的{tag}
语法将预设的数据填入模板Word
或者Powerpoint
中,帮助开发者快速生成最终文件。
由于DocxTemplater
是基于{tag}
变量替换,得到最终的文件,因此文件的样式是非常可控的。在开发过程中,设计师只需要出一版最终的成品word,开发者将内容替换成对应的{tag}
即可(再也不用被设计师追着还原设计稿了!)。
DocxTemplater
是一个收费的库,但是它拥有免费的开源版本,对于我所涉及的业务,使用免费版本完全可以解决。
开源版包括的功能包括:
-
{tag}
替换 - 条件判断
- 循环
- 图片渲染
除免费版外,它还拥有pro plan(950€ per\year)
、Entreprise plan(24500€ per\year)
,具体功能大家可以前往官网查看。(但是白嫖的永远是最香的!)
由于我的业务只涉及生成word和pdf,所以本文只介绍word的相关内容,如果需要处理ppt,大家可前往官网自行学习。
1.1 模板语法
在了解模板语法之前,我们需要先创建一个tempalte.docx
模板文件。
变量替换
变量使用{key}
标签,在tempalte.docx
文件中输入以下模板:
Hello, my name is {name}.
然后准备以下数据:
let data = {
name: "千万",
}
最终我们生成的docx文件将会是:
Hello, my name is 千万.
条件判断
条件判断使用{#key}
开始,使用{/key}
结束,最简单的用法是使用Boolean
类型数据进行填充。
Hello, my name is {name}.
{#hasAge}I'm {age} years old.{/hasAge}
{#hasWeight}My weight is {weight}.{/hasWeight}
然后准备以下数据:
let data = {
name: "千万",
hasAge: true,
age: 23,
hasWeight: false,
}
最终我们生成的docx文件将会是:
Hello, my name is 千万.
I'm 23 years old.
除了Boolean
类型的数据以外,我们也可以填充其他类型:
type | 是否显示模块 |
---|---|
false / 空数组 | 不显示 |
非空数组 | 显示,且将循环渲染内部元素 |
对象 | 显示,且使用对象内部变量替换{tag} |
其他真值 | 显示 |
如果变量填充了数组
,其实就是我们下面要介绍的循环
语法,在下面一小节中再进行介绍。
在这里简单说一下填充对象
的情况:
准备word模板如下:
总价格:{price}
{#product} ${productName}: ${price} {/product}
准备数据如下:
let data = {
price: 159,
product: {
productName: 'pencel',
price: 1.2
}
}
最终我们生成的docx文件将会是:
总价格:159
pencel:$1.2
循环
循环
的标志与条件判断
相同,但对应的变量应使用Array
来填充。
{#examScoreList}
{exam}: {score}
{/examScoreList}
然后我们填充以下数据:
let data = {
examScoreList: [
{ exam: 数学, score: 60 },
{ exam: 语文, score: 50 },
{ exam: 英语, score: 40 },
],
}
最终我们生成的docx文件将会是:
数学: 60
语文: 50
英语: 40
表格循环
值得注意的是,循环不仅仅可以循环一段普通文字,我们也可以对表格
进行循环,包括:循环行和循环整个表。如果想要循环渲染多个表格,只需要在表格外面使用循环
语法即可,不在此处过多赘述。下面展示循环渲染一个表中的行的写法:
上图中可以看到,我在表格的第二行中使用了循环
语法进行填写,这样我们最终生成的文档中,表头和尾就不会被循环,第二行将会被多次渲染,结果如下:
图片
图片使用{%image}
进行标注即可,对于图片的数据传入需要特殊处理,后面的部分会进行介绍。
总结
根据以上语法,我们就可以准备对应的word模板文件了,大部分场景下应该都可以满足。在准备模板的时候,固定的文案和样式直接保留在文档中即可,包括页眉、页脚,各个段落的行距、间距,文字的字体、大小等。其他需要根据真实数据渲染的值,就用标签标注上。准备好模板文件以后,就可以开始脚本函数的编写了。
PS: 要善用表格进行排版布局!
1.2 程序编写
安装所需库
npm install docxtemplater
npm install docxtemplater-image-module-free // 图片模块,没有图片需求可以不装
npm install pizzip // 处理模板文件用到,且只能使用该库
客户端生成
1. 获取模板文件的binaryString
function getFileBinaryString(templateFile) { // templateFile是File对象
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
resolve(e.target.result);
}
reader.onerror = reject;
reader.readAsBinaryString(templateFile);
});
}
这里使用到了FileReader
类,用于将模板文件转换为binaryString
,需要注意浏览器的兼容性。
如果对兼容性有要求,可以是使用pizzip/utils
中提供的方法getBinaryContent
,但是此库对ts兼容性比较差,因此我在实际代码中使用了FileReader
。
import PizZipUtils from "pizzip/utils/index.js";
function loadFile(url, callback) { PizZipUtils.getBinaryContent(url, callback); }
2. 生成最终文件(无需图片)
// generate-doxc.js
import PizZip from 'pizzip';
import DocxTemplater from 'docxtemplater';
function getFileBinaryString(templateFile) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
resolve(e.target.result);
}
reader.onerror = reject;
reader.readAsBinaryString(templateFile);
});
}
export async function generateDocxFile(template, fileData) {
return new Promise((resolve, reject) => {
getFileBinaryString(template)
.then(templateData => {
const zip = new PizZip(templateData);
const doc = new DocxTemplater()
.loadZip(zip)
.render(fileData); // fileData是我们需要定义好,传给docxtempale的数据。
const out = doc.getZip().generate({
type: 'blob',
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
});
resolve(out);
})
.catch(reject);
});
}
3. 准备数据,生成最终文件
接下来我们准备一个<input type="file" />
的文件输入框(你也可以使用网络请求,或者任何方式拿到文件,只要最终获得二进制数据就可以),用来获取模板文件。同时准备好相应的数据,来对模板进行填充。
// App.jsx
import { saveAs } from 'file-saver';
import { generateDocxFile } from './utils/generate-docx';
const fileData = {
intro: '国际劳动节,又称五一国际劳动节、劳动节、国际示威游行日,是纪念工人和劳工运动的斗争和成果的日子。国际劳动节是一项由国际劳工运动所推动的节日,全世界劳工和工人阶级在一般会在五朔节(5月1日)举行的庆祝节日,而美国和加拿大在9月第一个星期一举行。是世界上80多个国家的劳动节。',
activities: [
{
name: '阿尔及利亚',
activity: '在阿尔及利亚,5月1日是公共假日,以庆祝劳动节。'
},
{
name: '安哥拉',
activity: '5月1日在安哥拉被承认为公共假日,称为劳动节。'
},
{
name: '埃及',
activity: '在埃及,5月1日被称为劳动节,是一个带薪的公共假期。在传统上,埃及总统会主持正式的五一节庆祝活动。'
},
{
name: '加纳',
activity: '5月1日是加纳的一个节日,属于庆祝全国所有工人。工会和劳工协会以游行的形式来庆祝劳动节。加纳也会举行阅兵式,通常由工会大会秘书长和各地区的区域秘书致辞。来自不同工作地点的工人通过条幅和衣着表明他们的公司。'
}
]
}
function App() {
const handleFileChange = async (e) => {
const file = e.target.files[0];
const out = await generateDocxFile(file, fileData);
saveAs(out, `${new Date().getTime()}.docx`);
}
return (
<div className="App">
<input type="file" onChange={handleFileChange} />
</div>
)
}
export default App;
4. 效果展示
模板文件如下:
生成结果如下:
5. 图片处理
如果需要在模板中使用图片,我们需要安装docxtemplater-image-module-free
模块。
引入了此模块后,需要在加载模板文件后,载入image模块,然后异步填入数据。
// generate-docx.js
// 将图片处理为base64,给模板使用
function convertImgToBase64(url, outputFormat) {
return new Promise((resolve, reject) => {
let canvas = document.createElement(
'CANVAS',
);
const ctx = canvas.getContext('2d'),
img = new Image();
img.crossOrigin = 'Anonymous';
img.onload = function () {
canvas.height = img.height;
canvas.width = img.width;
ctx.drawImage(img, 0, 0);
var dataURL = canvas.toDataURL(outputFormat || 'image/png');
canvas = null;
resolve(dataURL);
};
img.onerror = function (e) {
reject(e);
};
img.src = url;
});
}
const imageOpts = { // 图片的配置
centered: false,
getImage: function (tagValue, tagName) { // 将图片转成base64
return new Promise((resolve) => {
if (typeof tagValue === 'string' && base64Regex.test(tagValue)) {
return resolve(tagValue);
} else {
convertImgToBase64(tagValue).then((base64) => {
return resolve(base64Parser(base64));
});
}
});
},
// 设置图片宽高,可以根据tagName为每一张图片设置不同宽高
getSize: function (img, tagValue, tagName) {
// img是图片Buffer,tagValue是图片初始值,tagName是图片在模板中定义的标签key值
return [150, 150]; // [宽, 高]
}
};
export async function generateDocxFile(template, fileData) {
return new Promise((resolve, reject) => {
getFileBinaryString(template)
.then(templateData => {
const zip = new PizZip(templateData);
const doc = new DocxTemplater()
.loadZip(zip)
.attachModule(new ImageModule(imageOpts)) // 载入模块
.compile();
// 异步填充数据
doc.resolveData(fileData)
.then(() => {
doc.render();
const out = doc.getZip().generate({
type: 'blob',
mimeType:
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
});
docxLists.push({ file: out, fileName });
resolve();
});
})
.catch(reject);
});
}
对于有图片的文档生成,需要异步载入数据,且图片数据需要处理为base64,上述代码给出了处理图片的一种解决方案,如果大家有更高效的方法也可以自行使用。
node服务器端生成
该库同样支持在node
中使用,其思想与在浏览器端基本一致,在node端可以直接使用buffer
,下面贴出官方给出的代码示例。
const PizZip = require("pizzip");
const Docxtemplater = require("docxtemplater");
const fs = require("fs");
const path = require("path"); // Load the docx file as binary content
const content = fs.readFileSync( path.resolve(__dirname, "input.docx"), "binary" );
const zip = new PizZip(content);
const doc = new Docxtemplater(zip,
{
paragraphLoop: true,
linebreaks: true,
}); // Render the document (Replace {first_name} by John, {last_name} by Doe, ...)
doc.render({
first_name: "John",
last_name: "Doe",
phone: "0652455478",
description: "New Website",
});
const buf = doc.getZip().generate({
type: "nodebuffer",
// compression: DEFLATE adds a compression step.
// For a 50MB output document, expect 500ms additional CPU time
compression: "DEFLATE",
});
// buf is a nodejs Buffer, you can either write it to a file or res.send it with express for example.
fs.writeFileSync(path.resolve(__dirname, "output.docx"), buf);
1.3 总结
docxTemplater
是一个通过模板文件生成word的库,它能最大程度的保证最终生成的word的样式的完整和还原。代码搭建好后,对于类似的业务,开发者们只需要编写更多的模板文件
,并且把精力集中在对数据
的处理上即可。
配合e-charts
或其他图表库,也可以让我们实现报表文件的生成。
此外,对于pizzip
这个库,它本身是对jszip
库的一个升级,拥有对zip文件的操作能力,可以直接解压或者生成zip包,我们可以直接通过此库对批量生成的文件进行打包处理,打包主要用的api如下:
const zip = new pizZip();
zip.file(fileName, fileBuffer); // 生成的文件名 以及 文件的 arrayBuffer
zip.generate({ type: 'blob' }), `documents.zip`); // 生成zip文件
二、使用libre office将word转换成pdf
在进行此部分业务时,原本想在前端把所有的工作都做好,但是没有找到在客户端就直接转换的方法。因此,此部分在服务器端进行解决。
首先需要在机器上安装libre office
软件,具体方法可以自行搜索。
安装好后,项目中安装libreoffice-convert
库,这个库对libre office
的转换方法进行了封装,直接调用其中方法就好:
const path = require('path');
const fs = require('fs');
const libre = require('libreoffice-convert');
async function docx2pdf(docxBuf, outputPath) {
libre.convert(docxBuf, '.pdf', undefined, (err, outputBuf) => {
if (err) {
console.log(`Error converting file: ${err}`);
}
fs.writeFileSync(outputPath, outputBuf);
});
}
const inputBuf = fs.readFileSync(path.join(__dirname, 'sample.docx'));
let outputPath = path.join(__dirname, 'sample.pdf');
docx2pdf(inputBuf, outputPath);
如果是zip文件,同样可以安装jsZip
或者pizZip
进行解压、打包等处理。这里更推荐使用jsZip
,因为文档更加丰富,且对ts支持更好。
三、结束语
这是我第二次遇到此类业务,所以本着学习、记录、分享的心态,将内容分享到平台上。在开发过程中遇到了很多“坑”,并没有在本文中记录。本文主要还是以记录最终成功的代码为主,把内容分享给其他有同样需求的家人们。毕竟轮子已经这么完善了,当然要好好利用啦!