xlsx-handlebars
中文文档 | English | 在线演示
一个用于处理 XLSX 文件 Handlebars 模板的 Rust 库,支持多平台使用:
功能特性
⚡ 极致性能:2.12 秒渲染 10 万行数据(约 4.7 万行/秒)- 比 Python 快 14-28 倍,比 JavaScript 快 7-14 倍
✅ 智能合并:自动处理被 XML 标签分割的 Handlebars 语法
✅ XLSX 验证:内置文件格式验证,确保输入文件有效
✅ Handlebars 支持:完整的模板引擎,支持变量、条件、循环、Helper 函数
✅ 跨平台:Rust 原生 + WASM 支持多种运行时
✅ TypeScript:完整的类型定义和智能提示
✅ 零依赖:WASM 二进制文件,无外部依赖
安装
Rust
cargo add xlsx-handlebars
复制代码
npm
npm install xlsx-handlebars
复制代码
Deno
import init, { render_template } from "jsr:@sail/xlsx-handlebars";
复制代码
使用示例
Rust
use xlsx_handlebars::render_template;use serde_json::json;
fn main() -> Result<(), Box<dyn std::error::Error>> { // 读取 XLSX 模板文件 let template_bytes = std::fs::read("template.xlsx")?; // 准备数据 let data = json!({ "name": "张三", "company": "ABC科技有限公司", "position": "软件工程师", "projects": [ {"name": "项目A", "status": "已完成"}, {"name": "项目B", "status": "进行中"} ], "has_bonus": true, "bonus_amount": 5000 }); // 渲染模板 let result = render_template(template_bytes, &data)?; // 保存结果 std::fs::write("output.xlsx", result)?; Ok(())}
复制代码
JavaScript/TypeScript (Node.js)
import init, { render_template } from "xlsx-handlebars";import fs from 'fs';
async function processTemplate() { // 初始化 WASM 模块 await init(); // 读取模板文件 const templateBytes = fs.readFileSync("template.xlsx"); // 准备数据 const data = { name: "李明", company: "XYZ技术有限公司", position: "高级开发工程师", projects: [ { name: "E-commerce平台", status: "已完成" }, { name: "移动端APP", status: "开发中" } ], has_bonus: true, bonus_amount: 8000 }; // 渲染模板 const result = render_template(templateBytes, JSON.stringify(data)); // 保存结果 fs.writeFileSync('output.xlsx', new Uint8Array(result));}
processTemplate().catch(console.error);
复制代码
Deno
import init, { render_template } from "https://deno.land/x/xlsx_handlebars/mod.ts";
async function processTemplate() { // 初始化 WASM 模块 await init(); // 读取模板文件 const templateBytes = await Deno.readFile("template.xlsx"); // 准备数据 const data = { name: "王小明", department: "研发部", projects: [ { name: "智能客服系统", status: "已上线" }, { name: "数据可视化平台", status: "开发中" } ] }; // 渲染模板 const result = render_template(templateBytes, JSON.stringify(data)); // 保存结果 await Deno.writeFile("output.xlsx", new Uint8Array(result));}
if (import.meta.main) { await processTemplate();}
复制代码
浏览器端
<!DOCTYPE html><html><head> <title>XLSX Handlebars 示例</title></head><body> <input type="file" id="fileInput" accept=".xlsx"> <button onclick="processFile()">处理模板</button> <script type="module"> import init, { render_template } from './pkg/xlsx_handlebars.js'; // 初始化 WASM await init(); window.processFile = async function() { const fileInput = document.getElementById('fileInput'); const file = fileInput.files[0]; if (!file) return; const arrayBuffer = await file.arrayBuffer(); const templateBytes = new Uint8Array(arrayBuffer); const data = { name: "张三", company: "示例公司" }; try { const result = render_template(templateBytes, JSON.stringify(data)); // 下载结果 const blob = new Blob([new Uint8Array(result)], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'processed.xlsx'; a.click(); } catch (error) { console.error('处理失败:', error); } }; </script></body></html>
复制代码
模板语法
基础变量替换
员工姓名: {{name}}公司: {{company}}职位: {{position}}
复制代码
条件渲染
{{#if has_bonus}}奖金: ¥{{bonus_amount}}{{else}}无奖金{{/if}}
{{#unless is_intern}}正式员工{{/unless}}
复制代码
循环渲染
项目经历:{{#each projects}}- {{name}}: {{description}} ({{status}}){{/each}}
技能列表:{{#each skills}}{{@index}}. {{this}}{{/each}}
复制代码
Helper 函数
内置的 Helper 函数:
<!-- 基础 helper -->{{upper name}} <!-- 转大写 -->{{lower company}} <!-- 转小写 -->{{len projects}} <!-- 数组长度 -->{{#if (eq status "completed")}}已完成{{/if}} <!-- 相等比较 -->{{#if (gt score 90)}}优秀{{/if}} <!-- 大于比较 -->{{#if (lt age 30)}}年轻{{/if}} <!-- 小于比较 -->
<!-- 字符串拼接 -->{{concat "你好" " " "世界"}} <!-- 字符串拼接 -->{{concat "总计: " count}} <!-- 混合字符串和变量 -->
<!-- Excel 专用 helper -->{{num employee.salary}} <!-- 标记单元格为数字类型 -->{{formula "=SUM(A1:B1)"}} <!-- 静态 Excel 公式 -->{{formula (concat "=SUM(" (_c) "1:" (_c) "10)")}} <!-- 使用当前列的动态公式 -->{{mergeCell "C4:D5"}} <!-- 合并单元格 C4 到 D5 -->{{img logo.data 100 100}} <!-- 插入图片,指定宽高 -->
<!-- 列名转换 helper -->{{toColumnName "A" 5}} <!-- A + 5 偏移 = F -->{{toColumnName (_c) 3}} <!-- 当前列向右偏移 3 列 -->{{toColumnIndex "AA"}} <!-- AA 列的索引 = 27 -->
复制代码
Excel 公式 Helper
静态公式:
<!-- 在 Excel 单元格中 -->{{formula "=SUM(A1:B1)"}}{{formula "=AVERAGE(C2:C10)"}}{{formula "=IF(D1>100,\"高\",\"低\")"}}
复制代码
使用 concat 的动态公式:
<!-- 动态行引用 -->{{formula (concat "=A" (_r) "*B" (_r))}}
<!-- 动态列引用 -->{{formula (concat "=SUM(" (_c) "2:" (_c) "10)")}}
<!-- 复杂动态公式 -->{{formula (concat "=IF(" (_cr) ">100,\"高\",\"低\")")}}
复制代码
可用的位置 helper:
(_c) - 当前列字母 (A, B, C, ...)
(_r) - 当前行号 (1, 2, 3, ...)
(_cr) - 当前单元格引用 (A1, B2, C3, ...)
列名转换 Helper
toColumnName - 将列名或列索引转换为新的列名,支持偏移量:
<!-- 基础用法:从指定列名开始偏移 -->{{toColumnName "A" 0}} <!-- A (无偏移) -->{{toColumnName "A" 5}} <!-- F (A + 5) -->{{toColumnName "Z" 1}} <!-- AA (Z + 1) -->
<!-- 配合当前列使用 -->{{toColumnName (_c) 3}} <!-- 当前列向右偏移 3 列 -->
<!-- 动态公式中的应用 -->{{formula (concat "=SUM(" (_c) "1:" (toColumnName (_c) 3) "1)")}}<!-- 示例:如果当前列是 B,生成公式 =SUM(B1:E1) -->
复制代码
toColumnIndex - 将列名转换为列索引(1-based):
{{toColumnIndex "A"}} <!-- 1 -->{{toColumnIndex "Z"}} <!-- 26 -->{{toColumnIndex "AA"}} <!-- 27 -->{{toColumnIndex "AB"}} <!-- 28 -->
复制代码
合并单元格 Helper
mergeCell - 标记需要合并的单元格范围:
<!-- 静态合并单元格 -->{{mergeCell "C4:D5"}} <!-- 合并 C4 到 D5 区域 -->{{mergeCell "F4:G4"}} <!-- 合并 F4 到 G4 区域 -->
<!-- 动态合并单元格:从当前位置合并 -->{{mergeCell (concat (_c) (_r) ":" (toColumnName (_c) 3) (_r))}}<!-- 示例:如果当前在 B5,合并 B5:E5(向右合并4列) -->
<!-- 动态合并单元格:跨行跨列 -->{{mergeCell (concat (_c) (_r) ":" (toColumnName (_c) 2) (add (_r) 2))}}<!-- 示例:如果当前在 C3,合并 C3:E5(3列×3行的区域) -->
<!-- 在循环中动态合并 -->{{#each sections}} {{mergeCell (concat "A" (add @index 2) ":D" (add @index 2))}} <!-- 为每个 section 合并一行的 A-D 列 -->{{/each}}
复制代码
注意事项:
超链接 Helper
hyperlink - 在 Excel 单元格中添加超链接:
<!-- 基础用法:链接到其他工作表 -->{{hyperlink (_cr) "Sheet2!A1" "查看详情"}}
<!-- 链接到外部网址(需在模板中预设) -->{{hyperlink (_cr) "https://example.com" "访问网站"}}
<!-- 动态链接 -->{{#each items}} {{hyperlink (_cr) (concat "详情!" name) name}}{{/each}}
复制代码
参数说明:
注意事项:
数字类型 Helper
使用 {{num value}} 确保单元格在 Excel 中被识别为数字:
<!-- 不使用 num: 当作文本处理 -->{{employee.salary}}
<!-- 使用 num: 当作数字处理 -->{{num employee.salary}}
复制代码
特别适用于以下场景:
值可能是字符串但应当作数字处理
需要确保 Excel 中的数字格式正确
需要在公式中使用该值
图片插入 Helper
img - 在 Excel 中插入 base64 编码的图片:
<!-- 基础用法:插入图片并使用原始尺寸 -->{{img logo.data}}
<!-- 指定宽度和高度(单位:像素) -->{{img photo.data 150 200}}
<!-- 使用数据中的尺寸 -->{{img image.data image.width image.height}}
复制代码
特性:
完整示例:
// 在 JavaScript 中准备图片数据import fs from 'fs';
const imageBuffer = fs.readFileSync('logo.png');const base64Image = imageBuffer.toString('base64');
const data = { company: { logo: base64Image, name: "科技公司" }, products: [ { name: "产品A", photo: base64Image, width: 120, height: 120 }, { name: "产品B", photo: base64Image, width: 100, height: 100 } ]};
// 在模板中使用
复制代码
<!-- Excel 模板示例 -->公司Logo: {{img company.logo 100 50}}
产品列表:{{#each products}}产品名: {{name}}图片: {{img photo width height}}{{/each}}
复制代码
使用技巧:
工作表管理 Helpers
deleteCurrentSheet - 删除当前正在渲染的工作表:
<!-- 基础用法 -->{{deleteCurrentSheet}}
<!-- 条件删除 -->{{#if shouldDelete}} {{deleteCurrentSheet}}{{/if}}
<!-- 删除非活跃工作表 -->{{#unless isActive}} {{deleteCurrentSheet}}{{/unless}}
复制代码
特性:
setCurrentSheetName - 重命名当前工作表:
<!-- 静态名称 -->{{setCurrentSheetName "销售报表"}}
<!-- 动态名称 -->{{setCurrentSheetName (concat department.name " - " year "年")}}
<!-- 基于循环的命名 -->{{#each departments}} {{setCurrentSheetName (concat "部门" @index " - " name)}}{{/each}}
复制代码
特性:
✅ 自动过滤非法字符:\ / ? * [ ]
✅ 自动限制长度为 31 个字符
✅ 自动处理重名,添加数字后缀
✅ 支持动态名称生成
hideCurrentSheet - 隐藏当前工作表:
<!-- 普通隐藏(用户可通过右键取消隐藏) -->{{hideCurrentSheet}}{{hideCurrentSheet "hidden"}}
<!-- 超级隐藏(需要 VBA 才能取消隐藏) -->{{hideCurrentSheet "veryHidden"}}
<!-- 条件隐藏 -->{{#unless (eq userRole "admin")}} {{hideCurrentSheet "veryHidden"}}{{/unless}}
复制代码
隐藏级别:
特性:
常见使用场景:
<!-- 多语言报表:删除未使用的语言工作表 -->{{#if (ne language "zh-CN")}} {{deleteCurrentSheet}}{{/if}}
<!-- 动态部门报表:按部门重命名工作表 -->{{setCurrentSheetName (concat department.name " 报表")}}
<!-- 权限控制:对普通用户隐藏管理员工作表 -->{{#unless (eq userRole "admin")}} {{hideCurrentSheet "veryHidden"}}{{/unless}}
<!-- 条件工作流:根据状态删除、重命名或隐藏 -->{{#if (eq status "inactive")}} {{deleteCurrentSheet}}{{else}} {{setCurrentSheetName (concat "活跃 - " name)}} {{#if isInternal}} {{hideCurrentSheet}} {{/if}}{{/if}}
复制代码
复杂示例
=== 员工报告 ===
基本信息:姓名: {{employee.name}}部门: {{employee.department}}职位: {{employee.position}}入职时间: {{employee.hire_date}}
{{#if employee.has_bonus}}💰 奖金: ¥{{employee.bonus_amount}}{{/if}}
项目经历 (共{{len projects}}个):{{#each projects}}{{@index}}. {{name}} 描述: {{description}} 状态: {{status}} 团队规模: {{team_size}}人 {{/each}}
技能评估:{{#each skills}}- {{name}}: {{level}}/10 ({{years}}年经验){{/each}}
在表格中若需要删除一整行, 只需要在任意单元格上添加:{{removeRow}}
{{#if (gt performance.score 90)}}🎉 绩效评级: 优秀{{else if (gt performance.score 80)}}👍 绩效评级: 良好{{else}}📈 绩效评级: 需改进{{/if}}
复制代码
构建和开发
构建 WASM 包
# 构建所有目标npm run build
# 或分别构建npm run build:web # 浏览器版本npm run build:npm # Node.js 版本 npm run build:jsr # Deno 版本
复制代码
运行示例
# Rust 示例cargo run --example rust_example
# Node.js 示例node examples/node_example.js
# Deno 示例 deno run --allow-read --allow-write examples/deno_example.ts
# 浏览器示例cd tests/npm_testnode serve.js# 然后在浏览器中打开 http://localhost:8080# 选择 examples/template.xlsx 文件测试
复制代码
工具函数
xlsx-handlebars 提供了一系列实用工具函数,帮助你更高效地处理 Excel 相关操作。
获取图片尺寸
从原始图片数据中检测图片尺寸,无需依赖完整的图片处理库。
use xlsx_handlebars::get_image_dimensions;
// 读取图片文件let image_data = std::fs::read("logo.png")?;
// 获取尺寸if let Some((width, height)) = get_image_dimensions(&image_data) { println!("图片尺寸: {}x{}", width, height);} else { println!("不支持的图片格式");}
复制代码
支持的格式:
PNG
JPEG
WebP (VP8, VP8L, VP8X)
BMP
TIFF (II/MM 字节序)
GIF (87a/89a)
Excel 列名转换
在 Excel 中进行列名和列索引之间的转换。
use xlsx_handlebars::{to_column_name, to_column_index};
// 列名递增assert_eq!(to_column_name("A", 0), "A");assert_eq!(to_column_name("A", 1), "B");assert_eq!(to_column_name("Z", 1), "AA");assert_eq!(to_column_name("AA", 1), "AB");
// 列名转索引 (1-based)assert_eq!(to_column_index("A"), 1);assert_eq!(to_column_index("Z"), 26);assert_eq!(to_column_index("AA"), 27);assert_eq!(to_column_index("BA"), 53);
复制代码
JavaScript/TypeScript 示例:
import { wasm_to_column_name, wasm_to_column_index } from 'xlsx-handlebars';
// 列名递增console.log(wasm_to_column_name("A", 1)); // "B"console.log(wasm_to_column_name("Z", 1)); // "AA"
// 列名转索引console.log(wasm_to_column_index("AA")); // 27console.log(wasm_to_column_index("BA")); // 53
复制代码
Excel 日期转换
在 Unix 时间戳和 Excel 日期序列号之间转换。Excel 使用从 1900-01-01 开始的序列号表示日期。
use xlsx_handlebars::{timestamp_to_excel_date, excel_date_to_timestamp};
// 时间戳转 Excel 日期let timestamp = 1704067200000i64; // 2024-01-01 00:00:00 UTClet excel_date = timestamp_to_excel_date(timestamp);println!("Excel 日期序列号: {}", excel_date); // 45294.0
// Excel 日期转时间戳if let Some(ts) = excel_date_to_timestamp(45294.0) { println!("时间戳: {}", ts); // 1704067200000}
复制代码
JavaScript/TypeScript 示例:
import { wasm_timestamp_to_excel_date, wasm_excel_date_to_timestamp } from 'xlsx-handlebars';
// 日期转 Excel 序列号const date = new Date('2024-01-01T00:00:00Z');const excelDate = wasm_timestamp_to_excel_date(date.getTime());console.log('Excel 日期:', excelDate); // 45294.0
// Excel 序列号转日期const timestamp = wasm_excel_date_to_timestamp(45294.0);if (timestamp !== null) { const convertedDate = new Date(timestamp); console.log('日期:', convertedDate.toISOString());}
复制代码
常见使用场景:
// 在模板中使用前验证图片尺寸let image_data = std::fs::read("photo.jpg")?;match get_image_dimensions(&image_data) { Some((w, h)) if w <= 1000 && h <= 1000 => { println!("有效图片: {}x{}", w, h); // 继续进行模板渲染 } Some((w, h)) => { eprintln!("图片过大: {}x{} (最大 1000x1000)", w, h); } None => { eprintln!("不支持的图片格式"); }}
复制代码
// 动态生成单元格引用let start_col = "B";let num_cols = 5;for i in 0..num_cols { let col_name = to_column_name(start_col, i); let col_index = to_column_index(&col_name); println!("列 {}: 名称={}, 索引={}", i, col_name, col_index);}
复制代码
// 在模板数据中包含日期use serde_json::json;
let date_timestamp = 1704067200000i64; // 2024-01-01let excel_date = timestamp_to_excel_date(date_timestamp);
let data = json!({ "report_date": excel_date, "employee": { "name": "张三", "hire_date": timestamp_to_excel_date(1609459200000i64) // 2021-01-01 }});
复制代码
// 批量处理图片for file in &["logo.png", "banner.jpg", "icon.gif"] { let data = std::fs::read(file)?; match get_image_dimensions(&data) { Some((w, h)) => println!("{}: {}x{}", file, w, h), None => eprintln!("{}: 不支持的格式", file), }}
复制代码
这些工具函数帮助你:
技术特性
性能和兼容性
极致性能表现 ⚡
xlsx-handlebars 凭借 Rust 实现了业界顶尖的性能表现:
性能对比 (处理 10 万行数据):
为什么这么快?
🦀 Rust 零成本抽象:编译期优化,无运行时开销
🔄 流式架构:直接在内存中处理 ZIP 条目,避免文件 I/O
⚡ 事件驱动 XML 解析:使用 quick-xml 高效解析,无需构建完整 DOM 树
🎯 单次遍历渲染:一次迭代完成所有模板替换
兼容性
零拷贝: Rust 和 WASM 之间高效的内存管理
流式处理: 适合处理大型 XLSX 文件
跨平台: 支持 Windows、macOS、Linux、Web
现代浏览器: 支持所有支持 WASM 的现代浏览器
许可证
本项目采用 MIT 许可证 - 详见 LICENSE-MIT 文件。
评论