React 前端将 Excel 工作表导出为图片的技术实践

0 阅读5分钟

在前端报表处理场景中,将 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,文件较大时内存占用值得关注,批量转换场景需要评估是否需要分页或串行处理。