@sailimuhu/xlsxtemplater v1.2.1
xlsxtemplater 是什么
xlsxtemplater 使用 JavaScript 对象或 JSON 作为数据输入来渲染 xlsx 文件。
xlsxtemplater 基于标记的概念,每种类型的标记都为模板编写者提供特定的功能。
xlsxtemplater 的标记语法与 docxtemplater 的 xlsx 模块 兼容,但只支持标记语法的一个子集。最重要的是,xlsxtemplater 是免费的。
xlsxtemplater 基于 ExcelJS 构建,这是一个功能强大的 Excel 文件读取、操作和写入库。
安装
npm install @sailimuhu/xlsxtemplater
# 或者
yarn add @sailimuhu/xlsxtemplater依赖
- ExcelJS: ^4.3.0
使用方法
Node.js (CommonJS)
const XlsxTemplater = require('@sailimuhu/xlsxtemplater');
// 创建模板实例
const templater = new XlsxTemplater('./template.xlsx');Node.js (ESM)
import XlsxTemplater from '@sailimuhu/xlsxtemplater';
// 创建模板实例
const templater = new XlsxTemplater('./template.xlsx');浏览器
<!-- UMD 版本 -->
<script src="node_modules/@sailimuhu/xlsxtemplater/dist/xlsxtemplater.umd.js"></script>
<script>
const templater = new XlsxTemplater(file); // file 可以是 Blob 或 ArrayBuffer
</script>
<!-- ES Module -->
<script type="module">
import XlsxTemplater from '@sailimuhu/xlsxtemplater';
const templater = new XlsxTemplater(file);
</script>浏览器使用注意事项
在浏览器环境中使用时:
1. 输入文件应该是 Blob 或 ArrayBuffer 类型,而不是文件路径
2. 使用 templater.renderToBlob() 替代 templater.save()
// 浏览器示例
const response = await fetch('template.xlsx');
const templateFile = await response.arrayBuffer();
const templater = new XlsxTemplater(templateFile);
await templater.render(data);
// 获取结果为 Blob
const resultBlob = await templater.renderToBlob();
// 或直接下载
const link = document.createElement('a');
link.href = URL.createObjectURL(resultBlob);
link.download = 'result.xlsx';
link.click();快速开始
const XlsxTemplater = require('@sailimuhu/xlsxtemplater');
// 创建模板实例
const templater = new XlsxTemplater('./template.xlsx');
// 使用数据渲染
await templater.render({
company: '华杰数能',
date: '2024-03-20',
items: [
{ name: '产品1', price: 100 },
{ name: '产品2', price: 200 }
]
});
// 保存渲染后的文件
await templater.save('./output.xlsx');支持的标记:
- 普通标记,
{tagName} - 循环标记,
{#loopTag} ... {/loopTag} - 内部循环标记,
{#loopTag} ... {/} - 图片标记,
{%imageTag}
扩展标记:
- 对象标记,
{@tagName} ...{/tagName},用于使用 JSON 数据中的嵌套对象。
待办事项
- 当数据是数组且作为最终数据使用时,应该将其连接并展开。
如何使用
概述
简单来说,支持5种类型的标记:普通标记、对象标记、循环标记、内部循环标记和图片标记。
普通标记和内部循环标记最灵活,可以嵌套在对象标记和循环标记内部。循环标记可以包含嵌套的对象标记、内部循环标记、图片标记,但不能嵌套另一个循环标记。对象标记可以包含嵌套的对象标记、图片标记、内部循环标记,但不能嵌套循环标记,并且可以跨多行。
示例
以下是��本用法。 这个例子展示了所有支持的数据格式:
let XlsxTemplater = require('XlsxTemplater')
let templater = new XlsxTemplater('./data/month_sale_report.xlsx')
templater.render({
company: '华杰',
createTime: '2022-12-09 05:25:00',
reporters: ['张三', '李四'],
summary:{
salesAmount: 5200000,
newCustomer: 3,
orderAmount: 6200000,
productList: ['产品A', '产品B', '产品C']
},
orders: [
{
date: '2022-11-1',
number: 'X221101001',
customer: '大族',
products: ['产品A', '产品B'],
salesAmount: 520000,
remark: ''
},
{
date: '2022-11-3',
number: 'X221103002',
customer: 'vivo',
products: ['产品A', '产品C'],
salesAmount: 320000,
remark: ''
}
]
})标记语法
1. 普通标记 {xxx}
使用大括号包围字段名称,如:
{someTag}| {hostCompany} | {createTime} | {productList} |
|---|
数据:
templater.render({
hostCompany: '华杰',
createTime: '2022-12-09 05:25:00',
productList:['产品A', '产品B']
})渲染后,注意 productList 是一个数组,会被转换��逗号分隔的字符串:
| 华杰 | 2022-12-09 05:25:00 | '产品A', '产品B' |
|---|
缺失标记自动填充空字符串
在下面的模板中,如果实际数据中缺少 'remark' 字段,它将被替换为空字符串:
| {hostCompany} | {createTime} | {remark} |
|---|
templater.render({
hostCompany: '华杰',
createTime: '2022-12-09 05:25:00'
})渲染后:
| 华杰 | 2022-12-09 05:25:00 |
|---|
2. 循环标记 {#xxx}...{/xxx}
循环标记用于处理数组数据,支持单行循环和多行循环两种方式。 循环标记包含开始和结束标记:
- 开始标记:大括号内以
#开头,例如{#someTag} - 结束标记:大括号内以
/开头,例如{/someTag}
2.1 单行循环
单行循环的开始和结束标记在同一行内,用于处理简单的数组数据:
{#items} {name}| {quantity} | {@price}{value} {type}{/price} | {/items}示例数据:
templater.render({
items: [
{
name: "产品A",
quantity: 5,
price: {
type: "元",
value: 10,
},
},
{
name: "产品B",
quantity: 1,
price: {
type: "元",
value: 20,
},
},
],
});结果: |产品A | 5 | 10 元| |:-|:-|:- |产品B | 1 | 20 元|
2.2 多行循环
多行循环允许开始和结束标记在不同行,适用于需要重复整块内容的场景:
| 问题描述 | 整改措施 | 负责人 |
|---|---|---|
| {#defects}{description} | {rectify_plan} | {responsible_party.name} |
| 联系电话:{responsible_party.tel} | ||
| 整改结果:{rectify_result} {/defects} |
示例数据:
{
defects: [
{
description: '问题1',
rectify_plan: '整改1',
rectify_result: '已完成',
responsible_party: {
name: '张三',
tel: '13800000001'
}
},
{
description: '问题2',
rectify_plan: '整改2',
rectify_result: '进行中',
responsible_party: {
name: '李四',
tel: '13800000002'
}
}
]
}多行循环特性: 1. 自动复制循环标记之间的所有行 2. 保持单元格格式(包括合并单元格、样式等) 3. 正确处理公式引用 4. 支持在循环内部使用其他类型的标记(对象标记、普通标记、图片标记) 5. 自动调整后续行的公式引用和条件格式
注意事项:
3. 内部循环标记
用于需要在单个单元格内填充数组中每个项目的值时使用。 "内部"意味着开始和结束标记必须在同一个单元格内。
内部循环标记包括:
- 开始标记:与普通循环相同,
{#someTag} - 结束标记:仅包含
{/}
示例:
|{#items} {name} | {#tags}{value},{/} | {quantity} | {price} | {/items}
|:-|:-|:-|:-|:-
数据:
templater.render({
"items": [
{
"name": "软件A",
"tags": [{ "value": "好用" }, { "value": "优秀" }],
"quantity": 10,
"price": "¥100"
}
]
})结果: |软件A | 好用,优秀 | 10 | ¥100 | |:-|:-|:-|:-|
4. 对象标记 {@xxx}...{/xxx}
用于访问 JSON 中的嵌套对象数据。包含开始和结束标记:
- 开始标记:大括号内以 @ 开头,例如
{@someTag} - 结束标记:大括号内以 / 开头,例如
{/someTag}
示例: |{@basic}{hostCompany}| | | | |:-|:-|:-|:-| |{createTime}|{contactName}|{contactPhone}|{/basic}|
数据:
templater.render({
basic:{
hostCompany: '华杰',
contactName: '张三',
contactPhone: '13088888888',
createTime: '2022-12-09 05:25:00'
}
})注意事项:
5. 图片标记 {%tag}
用于在单元格中插入图片。示例: |{%beforePic}|{%afterPic}|
渲染后,这些标记将被替换为目标图片。图片会填充整个单元格,所以需要根据需要调整单元格大小。
条件格式支持
当使用循环标记添加或删除行时,工作表中的条件格式可能会受到影响。因此:
- 复制行时,会复制相应的条件格式并调整行号
- 删除行时,会删除相应的条件格式并调整行号
注意:假设条件格式的引用和表达式仅使用同一行内的单元格,不存在跨行引用。
解决Exceljs库的一个bug:Worksheet.spiceRows()函数在删除行时会将后续所有行的合并单元格取消合并
如果你使用的是4.4.0及其更早版本的exceljs,请在node_modules/exceljs/lib/doc/worksheet.js, 然后找到 spiceRows() 在下面这个分支,原来是下面这样的
if (nExpand < 0) {
// remove rows
if (start === nEnd) {
this._rows[nEnd - 1] = undefined;
}
for (i = nKeep; i <= nEnd; i++) {
rSrc = this._rows[i - 1];
if (rSrc) {
const rDst = this.getRow(i + nExpand);
rDst.values = rSrc.values;
rDst.style = rSrc.style;
rDst.height = rSrc.height;
// eslint-disable-next-line no-loop-func
rSrc.eachCell({includeEmpty: true}, (cell, colNumber) => {
rDst.getCell(colNumber).style = cell.style;
// remerge cells accounting for insert offset
if (cell._value.constructor.name === 'MergeValue') {
const cellToBeMerged = this.getRow(cell._row._number + nExpand).getCell(colNumber);
const prevMaster = cell._value._master;
const newMaster = this.getRow(prevMaster._row._number + nExpand).getCell(prevMaster._column._number);
cellToBeMerged.merge(newMaster);
}
});
this._rows[i - 1] = undefined;
} else {
this._rows[i + nExpand - 1] = undefined;
}
}
}改为下面这样,主要是增加了对于合并单元格的处理逻辑
if (nExpand < 0) {
// remove rows
if (start === nEnd) {
this._rows[nEnd - 1] = undefined;
}
for (i = nKeep; i <= nEnd; i++) {
rSrc = this._rows[i - 1];
if (rSrc) {
const rDst = this.getRow(i + nExpand);
rDst.values = rSrc.values;
rDst.style = rSrc.style;
rDst.height = rSrc.height;
// eslint-disable-next-line no-loop-func
rSrc.eachCell({includeEmpty: true}, (cell, colNumber) => {
rDst.getCell(colNumber).style = cell.style;
// new added, fix the unmerged cell bug
// remerge cells accounting for insert offset
if (cell._value.constructor.name === 'MergeValue') {
const cellToBeMerged = this.getRow(cell._row._number + nExpand).getCell(colNumber);
const prevMaster = cell._value._master;
const newMaster = this.getRow(prevMaster._row._number + nExpand).getCell(prevMaster._column._number);
cellToBeMerged.merge(newMaster);
}
});
this._rows[i - 1] = undefined;
} else {
this._rows[i + nExpand - 1] = undefined;
}
}
}