在前端报表处理场景中,将 Excel 工作表导出为图片是一种常见需求。图片格式跨平台、无需安装表格软件即可查看,适合嵌入报告、聊天窗口或网页中分发。基于 WebAssembly 的浏览器端 Excel 处理方案,使得在 React 应用中直接完成工作表到图片的转换成为可能,整个过程无需后端服务参与。本文记录一套可运行实现的关键环节、两个导出场景的差异,以及实践中容易忽略的细节。
整体流程与运行时结构
这套方案在浏览器中的运行链路可以拆为四步:加载 WASM 模块、向虚拟文件系统(VFS)载入字体与 Excel 文件、调用工作表 API 渲染图片、从 VFS 读回结果并触发下载。其中 VFS 是理解整条链路的关键——WASM 运行时并不直接访问浏览器文件系统,输入文件与输出图片都存在于内存中的虚拟文件系统里,需要显式载入和读回。
模块通过 window.wasmModule.spirexls 暴露。由于 WASM 初始化是异步的,转换函数在入口处先做存在性判断,避免在模块未就绪时调用:
const xlsModule = window.wasmModule?.spirexls;
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
模块的加载与挂载通常放在组件外部或应用初始化阶段,转换函数只负责消费已就绪的实例。这种职责划分让转换逻辑保持简单,也便于在多个组件间复用。
字体载入:影响渲染还原度的前置条件
在 Excel 转图片的过程中,字体是一个容易被低估的变量。WASM 环境没有系统字体可用,若 VFS 中缺少 Excel 实际使用的字体,文本会回退到默认字体,表现为排版错位、字宽变化甚至字符缺失。因此字体文件必须在转换前载入 VFS。
载入通过 FetchFileToVFS 完成,它把浏览器可访问的静态资源写入虚拟文件系统的指定路径:
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'SalesDashboard.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
三个参数分别是文件名、VFS 目标路径、浏览器侧资源前缀。字体放入 /Library/Fonts/,Excel 文件放入根路径即可。这里有一个值得注意的细节:字体路径以 / 结尾,数据路径未以 / 结尾,两处拼接方式不一致。实际项目中建议统一路径拼接规则,否则很容易出现资源 404 而难以定位。字体文件体积通常不小,首次加载有网络开销,将其放到 CDN 或配置缓存是常见的优化手段。
两个导出场景的实现差异
转换的核心是 Worksheet.ToImage,它接受起始行、起始列、结束行、结束列四个参数,返回一个图片对象,调用其 Save 方法即可将结果写入 VFS。两个场景的完整组件代码如下。
导出整张工作表的已用区域
传入工作表的 FirstRow、FirstColumn、LastRow、LastColumn 时,转换范围即该表的已用区域,空白行列不会进入图片:
function App() {
const worksheetToImage = async () => {
const xlsModule = window.wasmModule?.spirexls;
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'SalesDashboard.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
const sheet = workbook.Worksheets.get(0);
const image = sheet.ToImage(sheet.FirstRow, sheet.FirstColumn, sheet.LastRow, sheet.LastColumn);
const outputFileName = 'WorksheetToImage.png';
image.Save(outputFileName);
workbook.Dispose();
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'image/png' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>在 React 中将 Excel 转换为图片</h1>
<button id="worksheet-to-image" onClick={worksheetToImage}>将工作表转换为图片</button>
</div>
);
}
export default App;
导出指定单元格区域
与整表导出的差别仅在于 ToImage 传入固定的行列范围。下面截取第 9 行第 1 列到第 13 行第 3 列:
function App() {
const rangeToImage = async () => {
const xlsModule = window.wasmModule?.spirexls;
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'SalesDashboard.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
const sheet = workbook.Worksheets.get(0);
const image = sheet.ToImage(9, 1, 13, 3);
const outputFileName = 'RangeToImage.png';
image.Save(outputFileName);
workbook.Dispose();
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'image/png' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>在 React 中将 Excel 转换为图片</h1>
<button id="range-to-image" onClick={rangeToImage}>将指定单元格区域转换为图片</button>
</div>
);
}
export default App;
两段代码的载入、读取、下载流程完全一致,实际项目中可以把公共部分抽成独立函数,只保留 ToImage 的参数差异。区域导出的意义在于裁剪范围可控,适合只导出汇总小表或某个数据区块的场景。
资源管理与工程细节
workbook.Dispose() 在每次转换后被调用,用于释放工作簿对象占用的 WASM 内存。WASM 堆内存不受 JS 垃圾回收直接管理,频繁转换而不释放会让内存占用持续增长,这一步不应省略。
其余几个细节在实践中值得关注:
window.dotnetRuntime与window.spire同样是运行时挂载的全局对象,读取 VFS 或载入文件前应做存在性检查,否则会抛出难以定位的异常。- 字体加载失败不会中断转换,而是静默回退到默认字体,因此图片“看起来不对”时优先排查 VFS 中的字体是否齐全。
- 同一页面若需切换不同字体的 Excel,需在每次转换前确保相应字体已载入,不能假定字体常驻。
ToImage的渲染结果依赖 VFS 中的字体与样式支持程度,复杂表格(图表、条件格式、特殊边框)的保真度需要用目标文件实测。
适用场景与限制
这套方案适合在浏览器端将表格数据可视化为图片分发的场景,例如把报表快照发给移动端用户、在即时通讯工具中分享数据摘要,或在不支持嵌入 Excel 的文档系统中插入表格图片。转换完全在客户端完成,数据不离开浏览器,在隐私敏感场景中有一定优势。
限制同样明显。WASM 模块的初始加载体积和初始化时间会影响首屏体验,仅偶尔使用转换功能的页面可考虑懒加载。渲染保真度受字体与样式支持程度制约,复杂工作表可能出现与 Excel 显示不一致的情况,上线前应以实际文件验证。此外,本方案的输入输出都经过 VFS,文件较大时内存占用值得关注,批量转换场景需要评估是否需要分页或串行处理。