背景描述
项目中需要生成word报告给客户。
项目本身以前就在前端生成了html格式的报告,打印之后可以生成PDF文档。所以一开始的方向是调研html转docx的方案,但是先后使用了几个库都不太行,包括Spire.doc这样的收费方案。主要的原因是报告的格式相对来说比较复杂,文字和图片都有,因此转出来的结果很不理想。
在这种情况下开始调研利用模板生成word文档的库,于是就发现了docx-templates这个库。实际用下来文档比较丰富,容易上手,效果令人满意。相比较其他的收费库(如docxtemplater)而言,虽然功能稍微欠缺一点,star数少一些,但是它是MIT license的,而且还在持续维护中。用爱发电,不能要求的太多。
这个库能干啥?
这个库能做的:
- 替换Word模板中的文字
- 实现FOR和IF操作
- 在文档指定位置插入图片
- 在模板里写JavaScript代码(!)
- 在文档指定位置插入HTML,并让word尝试转换
不能做的:
- 以编程从头开始编写文档;你需要先有一个模板
- 嵌套模板,即一个主模板里嵌套了子模板
简单Demo
docx-templates是一个npm库,可以在前端或者node中使用。这里在node中demo,因为实践中也是这样做的。
安装
安装过程很简单,运行npm命令即可:
npm install docx-templates
或者是yarn命令:
yarn add docx-templates
Node用法
package.json中添加"type": "module"
在index.js中添加如下内容
import {createReport} from 'docx-templates';
import fs from 'fs';
const template = fs.readFileSync('myTemplate.docx');
const buffer = await createReport({
template,
data: {
title: 'Test',
description: 'This is a test!'
},
cmdDelimiter: ['<<','>>'],
});
fs.writeFileSync('report.docx', buffer)
docx-templates的主要用法就是使用createReport这个函数。第一个参数是读取的模板,第二个参数是数据,直接放入一个对象,第三个参数则是用来定义模板中包裹数据的分隔符。分隔符默认是+++,这里自定义成<<和>>了
创建myTemplate.docx,也就是我们要用的模板,添加如下内容:
运行 node index.js,即可得到填充好的文档:
怎么样,过程不难吧!
进阶用法
FOR和END-FOR
在报告中,经常会遇到列表类的数据,可以使用FOR和END-FOR的命令来实现展示。
模板中:
<<FOR person IN project.people>>
<<$person.name>>
<<END-FOR person>>
请注意,循环中对当前元素在做操作时需要加上$前缀,就像上面的$person一样。
可以支持嵌套循环
<<FOR company IN companies>>
<<$company.name>>
<<FOR person IN $company.people>>
<<INS $person.firstName>>
<<FOR project IN $person.projects>>
<<$project.name>>
<<END-FOR project>>
<<END-FOR person>>
<<END-FOR company>>
如果想拿到循环到当前元素的Index,这里支持用$idx来取到这个值,但是只能取到最内层循环的值。
表格也可以用这种方法填充数据:
模板:
数据:
data: {
title: 'Test',
description: 'This is a test!',
items: [
{
name: 'A',
description: 'A is good'
},
{
name: 'B',
description: 'B is awesome'
}
]
}
成品:
IF和END-IF
根据IF条件式的值,来有条件的显示包含的内容:
<<IF person.name === 'Alice'>>
<<person.title>>
<<END-IF>>
目前还没有支持else的功能。
插入图片
利用的是IMAGE标签来插入图片,这个标签里面需要放入一个返回image对象的函数。该函数的定义会放在createReport的additionalJsContext里面。
模板示例:
<<IMAGE injectImage(chart.base64, chart.width, chart.height)>>
代码:
additionalJsContext: {
// width height: cm
injectImage: (base64: string, width: number, height: number) => {
return { width: width, height: height, data: base64, extension: '.png' };
}
}
image对象需要包括四个必需的参数:
- width: 图片宽度,单位厘米
- height: 图片高度,单位厘米
- data: 图片的数据,可以是ArrayBuffer或base64字符串(去掉前缀)
- extension:图片的格式,支持
'.png','.gif','.jpg','.jpeg','.svg'
Tips:
- 插入图片的版式与你Word中默认的粘贴图片的版式一致,我的默认是“嵌入式”。
- 如果你的图片尺寸原来是以像素为单位的,那需要进行单位转换,因为Word中的尺寸都是物理的尺寸,与打印的尺寸保持一致的,而像素在屏幕上的大小是与分辨率(DPI)相关的,大小并不绝对。换算关系式:CM = PX / Resolution (DPI) * 2.54。Word默认的DPI是220。
- 如果在FOR循环中插入图片,FOR和IMAGE写在同一行的时候才会让图片尽量插入在同一行;否则每张图片都会另起一行。
INS和EXEC运行js代码
INS
INS和=号还有空白是相同的效果,也就是<<name>>和<<=name>> <<INS name>>效果是相同的。它会插入后面这段js代码的值。
比如在实际应用中,如果有一段Unix的时间戳,我们要根据客户需要在模板中显示特定的时间格式,就可以在additionalJsContext中加入函数
// 此处引入了Day.js库
dateInFormat: (date: number, format: string) => {
return dayjs(date).format(format);
}
在模板中插入
<<dateInFormat(date, `YYYY-MM-DD`)>>
就可以在Word中显示YYYY-MM-DD格式的时间了。
EXEC
EXEC与INS类似,都能执行js代码,但是区别在于EXEC不会去插入它后面这段js代码的值。因此可以用它来定义常量或者函数。 EXEC也有缩写方式,使用!可以起到同样的效果。
插入HTML
使用docx-templates可以插入HTML,并且让Word将其尝试转换为Word中的格式。这种用法在某些情况下很有用,甚至是唯一的可行方案。例如,如果要动态设定一段文字的颜色,就没法在Word模板中直接做到这一点。这时候就可以用HTML动态指定文字颜色样式。
在word模板中你可以这样直接插入HTML,其中desiredColor就是你传给它的颜色:
<<HTML `
<body>
<p>
<strong style="color: ${desiredColor};">This paragraph should be colored and strong</strong>
</p>
</body>
`>>
你也可以将生成HTML的方法放到Word里面:
<<HTML insertHtml()>>
在additionalJsContext中加入函数
insertHtml: ()=>{
return htmlHelper.getHtml(templateHtml, data)
}
然后就可以用你想用的方法来生成HTML传进去了。
注意几点:
- 不要传特别复杂样式的HTML进去,word识别不了的
- 经过测试,word只能识别p标签上的样式,识别不了div标签的样式。我猜是p正好对应着word里的段落的概念,能够对的上,div就没有对应的,所以不行
- 段前段后用margin,行高用line-spacing设置
总结
docx-templates这个库使用起来还是比较全面的,基本上能够满足大部分生成Word的需求。
文章稍微有点标题党,欢迎互相交流学习!