日常开发中,将页面表格或报表数据导出为 Excel 文件,是一项出现频率极高的产品需求。无论内部管理系统、数据运营平台,还是面向外部用户的 SaaS 应用,都绕不开这个功能。
传统实现方案通常由后端服务借助相关库(如 Apache POI、EPPlus 等)生成文件后,再通过响应流返回前端下载。这套链路虽然成熟稳定,但也存在几项固有成本:每次导出都需要发起一次网络请求,后端需要额外处理并发及内存开销,同时数据需经过服务端中转,可能增加敏感信息暴露面。
近年来,随着 WebAssembly 技术在前端领域落地,陆续出现了一些可在浏览器端直接处理 Excel 文件的方案。这些方案将原本依赖服务端的能力迁移至客户端,为上述问题提供了不同的解决路径。
本文以 Spire.XLS for JavaScript 为例,介绍如何在不依赖后端服务的情况下,在原生 JavaScript 和 React 项目中完成 Excel 文件的创建、内容填充与导出下载。文章所述的方法与代码仅作为技术选型参考,不代表对该产品的推荐或背书。
使用前提与初始化
Spire.XLS for JavaScript 是一个基于 WebAssembly 的 Excel 操作库,支持 XLS、XLSX、XLSB、ODS 等常见格式,并提供单元格样式、公式、图表、数据透视表等功能。其核心工作逻辑与多数 WASM 库类似:先加载运行时环境,再通过 JavaScript 调用导出的 API。
安装
通过 npm 安装依赖包:
bash
npm i spire.xls
安装完成后,node_modules 中会包含 spire.office 目录,内含 WebAssembly 运行时文件及核心库脚本。
初始化 WASM 模块
该库基于 WebAssembly,因此在调用任何 Excel API 之前,需要先完成模块的初始化。初始化过程会加载所需资源并设置运行时环境。这一步骤通常在应用启动时执行:
javascript
// 首先导入并初始化公共模块
import('/node_modules/spire.office/spire.common.js').then(async (commonModule) => {
// 初始化 WASM 运行时
await commonModule.initializeWasm();
// 加载 XLS 模块
await import('/node_modules/spire.office/spire.xls.js');
console.log('Spire.XLS 已就绪');
});
初始化完成后,库的 API 会挂载到全局对象 window.spirexls 或 window.xlswasm 上。后续所有 Excel 操作均通过该全局对象调用。
不同版本中 API 挂载位置可能略有差异,建议在实际使用前通过 console.log(window.spirexls || window.xlswasm) 确认。
原生 JavaScript 环境下的导出实现
以下是一个完整的浏览器端导出流程:创建工作簿、填充数据、保存到虚拟文件系统,最后触发浏览器下载。
javascript
// 确保 WASM 模块已初始化
if (!window.spirexls && !window.xlswasm) {
console.error("Spire.XLS 未初始化。");
return;
}
// 获取已初始化的 WebAssembly 模块
const wasmModule = window.spirexls || window.xlswasm;
// 创建新工作簿
const workbook = new wasmModule.Workbook();
const worksheet = workbook.Worksheets.get(0);
// 创建示例数据
const products = [
["产品", "数量", "价格"],
["笔记本电脑", 10, 999.99],
["鼠标", 50, 24.99]
];
// 将数据插入工作表
for (let i = 0; i < products.length; i++) {
for (let j = 0; j < products[i].length; j++) {
const cell = worksheet.Range.get({ row: i + 1, column: j + 1 });
if (typeof products[i][j] === "string") {
cell.Text = products[i][j];
} else {
cell.NumberValue = products[i][j];
}
}
}
// 添加总计列(第四列)
worksheet.Range.get({ row: 1, column: products[0].length + 1 }).Text = "总计";
worksheet.Range.get({ row: 2, column: products[0].length + 1 }).Formula = "=B2*C2";
worksheet.Range.get({ row: 3, column: products[0].length + 1 }).Formula = "=B3*C3";
// 将工作簿保存到虚拟文件系统 (VFS)
const outputFileName = "Report.xlsx";
workbook.SaveToFile({
fileName: outputFileName,
version: wasmModule.ExcelVersion.Version2016
});
// 释放工作簿资源
workbook.Dispose();
// 从 VFS 读取生成的文件
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
// 创建 Blob 对象
const excelBlob = new Blob(
[fileArray],
{
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
}
);
// 触发浏览器下载
const url = URL.createObjectURL(excelBlob);
const a = document.createElement("a");
a.href = url;
a.download = outputFileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
上述代码的几个关键点
- 虚拟文件系统 (VFS) :该库的
SaveToFile()不会将文件写入操作系统磁盘,而是存放在 WebAssembly 实例的内存文件系统中。导出时需通过window.dotnetRuntime.Module.FS.readFile()读出原始二进制数据,再转换为浏览器可识别的 Blob 对象。 - 资源管理 :每个
Workbook实例在完成保存后,建议显式调用Dispose()方法释放内存。对于频繁导出的场景,这一点尤为重要,否则可能导致内存占用逐步上升。 - 行列索引:Excel 的行号和列号从 1 开始,而数组下标从 0 开始,赋值时需注意偏移。
- 全局对象依赖 :
window.dotnetRuntime由 WASM 运行时自动注入,无需额外声明。
在 React 项目中封装导出组件
在 React 框架中使用该库时,推荐将导出逻辑抽离为独立的 Hook 或组件,以便在不同页面复用。同时需要借助 useState 管理加载状态,防止用户重复点击。
组件示例
jsx
import { useState } from 'react';
const ExcelExportButton = ({ data, fileName = 'Export.xlsx' }) => {
const [isExporting, setIsExporting] = useState(false);
const handleExport = async () => {
// 确保 WASM 模块已初始化
const wasmModule = window.spirexls || window.xlswasm;
if (!wasmModule || isExporting) {
console.error("Spire.XLS 未初始化或正在导出中");
return;
}
setIsExporting(true);
try {
const workbook = new wasmModule.Workbook();
const worksheet = workbook.Worksheets.get(0);
// 根据数据对象动态生成表头
const headers = Object.keys(data[0] || {});
// 写入表头(第一行)
headers.forEach((key, index) => {
worksheet.Range.get({ row: 1, column: index + 1 }).Text = key;
});
// 写入数据行(从第二行开始)
data.forEach((item, rowIndex) => {
headers.forEach((key, colIndex) => {
const value = item[key];
const cell = worksheet.Range.get({
row: rowIndex + 2,
column: colIndex + 1
});
if (typeof value === 'string') {
cell.Text = value;
} else if (typeof value === 'number') {
cell.NumberValue = value;
} else if (value instanceof Date) {
cell.Text = value.toLocaleDateString();
}
// 其他类型(如 boolean)可根据需要扩展处理
});
});
const exportFileName = fileName.endsWith('.xlsx') ? fileName : `${fileName}.xlsx`;
workbook.SaveToFile({
fileName: exportFileName,
version: wasmModule.ExcelVersion.Version2016
});
workbook.Dispose();
const fileArray = window.dotnetRuntime.Module.FS.readFile(exportFileName);
const blob = new Blob([fileArray], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = exportFileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
console.error('导出失败:', error);
// 此处可补充用户提示逻辑,如 toast 消息
} finally {
setIsExporting(false);
}
};
return (
<button onClick={handleExport} disabled={isExporting}>
{isExporting ? '生成中...' : '导出 Excel'}
</button>
);
};
export default ExcelExportButton;
使用示例
jsx
const App = () => {
const tableData = [
{ 姓名: '张三', 部门: '技术部', 工时: 40 },
{ 姓名: '李四', 部门: '产品部', 工时: 38 }
];
return (
<div>
<ExcelExportButton data={tableData} fileName="月度工时表" />
</div>
);
};
备选方案:从已有 HTML 表格导出
如果页面上已经存在一个渲染好的 HTML 表格,可以直接将其转换为 Excel 文件,无需重新构建数据结构。官方示例中同时展示了如何为导出的表格应用内置样式(如标题行、隔行交替色)以及自动调整列宽。
javascript
async function exportTableToExcel() {
if (!window.spirexls && !window.xlswasm) {
alert("Spire.XLS 模块尚未加载。");
return;
}
const button = document.getElementById("exportBtn");
button.disabled = true;
button.innerText = "正在导出...";
const wasmModule = window.spirexls || window.xlswasm;
try {
// 获取 HTML 表格
const tableHtml = document.getElementById("salesTable").outerHTML;
// 移除内联样式(避免样式干扰)
const safeTableHtml = tableHtml.replace(/style="[^"]*"/g, '');
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
${safeTableHtml}
</body>
</html>
`;
const htmlFileName = "Table.html";
// 将 HTML 内容写入 VFS
window.dotnetRuntime.Module.FS.writeFile(htmlFileName, htmlContent);
// 加载 HTML 文件并转换为 Excel 工作簿
const workbook = new wasmModule.Workbook();
workbook.LoadFromHtml(htmlFileName);
const sheet = workbook.Worksheets.get(0);
// 获取表格的实际行列范围
const lastRow = Number(sheet.LastRow);
const lastCol = Number(sheet.LastColumn);
// 为表头行应用内置标题样式
const headerRow = sheet.Range.get_Item(1, 1, 1, lastCol);
headerRow.BuiltInStyle = wasmModule.BuiltInStyles.Heading3;
// 为数据行应用隔行交替背景色
for (let i = 2; i <= lastRow; i++) {
const row = sheet.Range.get_Item(i, 1, i, lastCol);
row.BuiltInStyle = i % 2 === 0
? wasmModule.BuiltInStyles.Accent3_20
: wasmModule.BuiltInStyles.Accent3_60;
}
// 自动调整所有列的宽度以适应内容
for (let j = 1; j <= lastCol; j++) {
sheet.AutoFitColumn(j);
}
const outputFileName = "SalesReport.xlsx";
workbook.SaveToFile({
fileName: outputFileName,
version: wasmModule.ExcelVersion.Version2016
});
workbook.Dispose();
const fileData = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileData], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = outputFileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (error) {
alert("导出失败:" + error.message);
} finally {
button.disabled = false;
button.innerText = "导出 Excel";
}
}
此方式的关键点在于:
- 通过
FS.writeFile()将 HTML 内容写入虚拟文件系统,再使用LoadFromHtml()加载。 - 移除表格内联样式可以避免某些样式属性在转换时产生意外效果。
BuiltInStyle属性对应库内置的预定义样式集,可用于快速美化导出表格。AutoFitColumn()方法可自动调整列宽,提升生成文件的可用性。
该方法适用于表格结构相对规整、对样式要求不高的场景。对于合并单元格、自定义字体颜色等复杂样式,转换效果可能因库的实现细节而有所差异,建议在实际使用前进行充分测试。
常见问题与注意事项
中文字体显示异常
某些环境下导出的 Excel 文件打开后,中文可能显示为方框或乱码。这通常是因为虚拟文件系统中缺少中文字体文件。解决方法是在初始化后,将字体文件(如 simsun.ttc、arial.ttf)通过 FetchFileToVFS 方法加载到 /Library/Fonts/ 目录下。具体代码可参考官方示例中的字体加载部分。
模块加载与初始化时机
WebAssembly 文件体积较大,首次加载需要一定时间。建议在应用启动阶段异步执行初始化,而非等到用户点击导出按钮时才开始加载,以改善交互体验。
内存管理与大数据量
浏览器端处理 Excel 受限于 JavaScript 堆内存和 WASM 线性内存。对于超过 10MB 或数万行以上的文件,客户端方案可能遇到性能瓶颈。这种情况仍建议采用服务端生成方案。
浏览器兼容性
WebAssembly 已被所有现代桌面浏览器(Chrome 96+、Firefox 90+、Safari 15+、Edge 96+)原生支持。需注意 Internet Explorer 及部分旧版移动端浏览器不支持该技术。
总结
Spire.XLS for JavaScript 提供了一套基于 WebAssembly 的浏览器端 Excel 操作能力,使开发者可以在不依赖后端服务的前提下完成文件的创建、编辑与导出。其核心工作流程为:
初始化 WASM 模块 → 创建/操作 Workbook 对象 → 保存至虚拟文件系统 → 读取二进制数据 → 构造 Blob 并触发下载
在 React 项目中,可将导出逻辑封装为组件或自定义 Hook,配合状态管理提升用户体验。同时,该库也支持从 HTML 表格直接转换,并提供了内置样式应用、列宽自动调整等实用功能,为已有页面快速添加导出功能提供了便利。
与所有客户端方案类似,该方法在数据量、字体处理、兼容性方面有其适用边界。实际选型时,建议结合项目的数据规模、目标浏览器环境及团队维护成本综合考量。