写点什么

xlsx-handlebars 用于处理 XLSX 文件 Handlebars 模板的 Rust 库,支持多平台使用

作者:黄智勇
  • 2025-10-12
    广东
  • 本文字数:8891 字

    阅读完需:约 29 分钟

xlsx-handlebars


中文文档 | English | 在线演示


一个用于处理 XLSX 文件 Handlebars 模板的 Rust 库,支持多平台使用:


  • 🦀 Rust 原生

  • 🌐 WebAssembly (WASM)

  • 📦 npm 包

  • 🟢 Node.js

  • 🦕 Deno

  • 🌍 浏览器端

  • 📋 JSR (JavaScript Registry)

功能特性

  • 极致性能: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}}
复制代码


注意事项


  • mergeCell 不产生输出,仅收集合并信息

  • 合并范围格式必须是 起始单元格:结束单元格(如 "A1:B2"

  • 相同的合并范围会自动去重

  • 合并信息会在渲染完成后自动添加到 Excel 文件中

超链接 Helper

hyperlink - 在 Excel 单元格中添加超链接:


<!-- 基础用法:链接到其他工作表 -->{{hyperlink (_cr) "Sheet2!A1" "查看详情"}}
<!-- 链接到外部网址(需在模板中预设) -->{{hyperlink (_cr) "https://example.com" "访问网站"}}
<!-- 动态链接 -->{{#each items}} {{hyperlink (_cr) (concat "详情!" name) name}}{{/each}}
复制代码


参数说明


  • 第一个参数:单元格引用,通常使用 (_cr) 获取当前单元格

  • 第二个参数:链接目标(工作表引用或 URL)

  • 第三个参数:显示文本(可选)


注意事项


  • hyperlink 不产生输出,仅收集超链接信息

  • 超链接会在渲染完成后自动添加到 Excel 文件中

  • 支持工作表内部引用(如 "Sheet2!A1"

  • 外部链接需要在模板 Excel 文件中预先配置关系

数字类型 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}}
复制代码


特性


  • ✅ 支持 PNG、JPEG、WebP、BMP、TIFF、GIF 等常见图片格式

  • ✅ 自动检测图片实际尺寸

  • ✅ 可选指定宽度和高度(像素)

  • ✅ 图片定位在当前单元格位置

  • ✅ 图片不受单元格大小限制,保持比例

  • ✅ 支持同一 sheet 插入多张图片

  • ✅ 支持多个 sheet 各自插入图片

  • ✅ 使用 UUID 避免 ID 冲突


完整示例


// 在 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}}
复制代码


使用技巧


  • 如果只指定宽度,高度会等比例缩放

  • 如果只指定高度,宽度会等比例缩放

  • 如果都不指定,使用图片原始尺寸

  • 图片会放置在调用 {{img}} 的单元格位置

  • base64 数据不包含 data:image/png;base64, 前缀,只需要纯 base64 字符串

工作表管理 Helpers

deleteCurrentSheet - 删除当前正在渲染的工作表:


<!-- 基础用法 -->{{deleteCurrentSheet}}
<!-- 条件删除 -->{{#if shouldDelete}} {{deleteCurrentSheet}}{{/if}}
<!-- 删除非活跃工作表 -->{{#unless isActive}} {{deleteCurrentSheet}}{{/unless}}
复制代码


特性


  • ✅ 从工作簿中移除工作表及其关系

  • ✅ 清理相关文件(rels、content types)

  • ✅ 保留 drawing 文件(安全考虑)

  • ✅ 不能删除最后一个工作表(Excel 要求)

  • ✅ 延迟执行,所有渲染完成后统一删除


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}}
复制代码


隐藏级别


  • hidden - 普通隐藏,用户可通过 Excel 右键菜单取消隐藏

  • veryHidden - 超级隐藏,需要 VBA 或属性编辑器才能取消隐藏


特性


  • ✅ 不能隐藏所有工作表(Excel 要求至少一个可见)

  • ✅ 两种隐藏级别:普通隐藏和超级隐藏

  • ✅ 适用于权限控制和敏感数据保护


常见使用场景


<!-- 多语言报表:删除未使用的语言工作表 -->{{#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),    }}
复制代码


这些工具函数帮助你:


  • ✅ 在插入前验证图片尺寸

  • ✅ 动态生成单元格引用和公式

  • ✅ 处理 Excel 日期格式

  • ✅ 避免加载笨重的外部库

  • ✅ 同时支持 Rust 和 JavaScript/TypeScript

技术特性

性能和兼容性

极致性能表现 ⚡

xlsx-handlebars 凭借 Rust 实现了业界顶尖的性能表现


性能对比 (处理 10 万行数据):


为什么这么快?


  • 🦀 Rust 零成本抽象:编译期优化,无运行时开销

  • 🔄 流式架构:直接在内存中处理 ZIP 条目,避免文件 I/O

  • 事件驱动 XML 解析:使用 quick-xml 高效解析,无需构建完整 DOM 树

  • 🎯 单次遍历渲染:一次迭代完成所有模板替换

兼容性

  • 零拷贝: Rust 和 WASM 之间高效的内存管理

  • 流式处理: 适合处理大型 XLSX 文件

  • 跨平台: 支持 Windows、macOS、Linux、Web

  • 现代浏览器: 支持所有支持 WASM 的现代浏览器

许可证

本项目采用 MIT 许可证 - 详见 LICENSE-MIT 文件。

发布于: 2025-10-12阅读数: 2
用户头像

黄智勇

关注

还未添加个人签名 2018-08-29 加入

还未添加个人简介

评论

发布
暂无评论
xlsx-handlebars 用于处理 XLSX 文件 Handlebars 模板的 Rust 库,支持多平台使用_黄智勇_InfoQ写作社区