引言

在 Web 应用中处理文档格式转换一直是前端开发中的难点。传统方案通常依赖服务端接口,但这也带来了额外的服务器成本和网络延迟。随着 WebAssembly 技术的成熟,如今我们可以在浏览器端直接完成 RTF 到 PDF 的转换,本文将分享一种基于 WASM 的实现方案。

环境准备与安装配置

在开始编码之前,需要完成以下准备工作:

1. 安装依赖包

通过 npm 安装文档处理库:

npm i spire.office

该包包含了多种文档格式的处理能力,本文仅使用其中的 RTF 转 PDF 功能。

2. 部署 WASM 资源文件

安装完成后,需要将必要的运行时文件复制到项目的前端静态目录(如 public/)中,包括:

  • spire.doc.js
  • spire.common.js
  • Spire.Doc.Wasm.zip
  • Spire.Common.Wasm.zip
  • _framework/

将这些文件复制到 public/ 目录后,示例目录结构如下:

public/
├── spire.doc.js
├── spire.common.js
├── Spire.Doc.Wasm.zip
├── Spire.Common.Wasm.zip
├── _framework/
│   └── ...
└── static/
    ├── font/          # 存放字体文件(如 times.ttf 等)
    └── data/          # 存放待转换的 RTF 样本文件(可选)

3. 准备字体文件

为确保 PDF 中的文字正确渲染,需要将所需的 TrueType 字体文件放置在静态目录中(如上例的 public/static/font/)。本文示例使用 Times New Roman 系列字体,你也可以根据实际文档内容替换为其他字体。

注意:字体文件需遵循相应授权协议,请确保您有权在应用中使用这些字体。

完成上述准备后,即可开始编写转换逻辑。

技术选型与架构

本方案采用 WebAssembly(WASM) 技术,将成熟的文档处理库编译为 WASM 模块,在浏览器中运行。整体架构如下:

  • 加载层:使用动态 import 异步加载 WASM 模块
  • 虚拟文件系统(VFS):在浏览器内存中模拟文件系统,供 WASM 模块读写
  • 转换引擎:基于 WASM 的文档处理核心,负责解析 RTF 并生成 PDF
  • 输出层:从 VFS 读取生成的 PDF 并触发下载
flowchart TB A[用户触发转换] --> B[加载 WASM 模块] B --> C[初始化虚拟文件系统 VFS] C --> D[加载字体文件到 VFS] D --> E[加载 RTF 文件到 VFS] E --> F[Document.LoadFromFile 解析 RTF] F --> G[Document.SaveToFile 生成 PDF] G --> H[从 VFS 读取 PDF 数据] H --> I[创建 Blob 并触发下载] I --> J[清理资源]

核心代码解析

1. WASM 模块加载

使用 React Hooks 管理模块的加载状态,通过动态 import 实现按需加载:

useEffect(() => {
  (async () => {
    try {
      const publicUrl = process.env.PUBLIC_URL || '';
      // 动态导入 WASM 模块
      const spireModule = await import(
        /* webpackIgnore: true */ `${publicUrl}/spire.doc.js`
      );
      const rawModule = spireModule.default || spireModule;
      // 初始化 WASM 实例,指定 .wasm 文件的位置
      window.wasmModule = typeof rawModule === 'function'
        ? await rawModule({ 
            locateFile: p => p.endsWith('.wasm') 
              ? `${publicUrl}/${p}` 
              : p 
          })
        : rawModule;
      setWasmModule(window.wasmModule);
    } catch (error) {
      console.error('Failed to load WASM module:', error);
    }
  })();
}, []);

这里的关键是 locateFile 函数,它告诉 WASM 运行时从哪里加载 .wasm 二进制文件。/* webpackIgnore: true */ 注释确保 Webpack 不会尝试解析这个动态路径。

2. 虚拟文件系统(VFS)

WASM 模块运行在沙箱环境中,无法直接访问宿主操作系统的文件系统。因此需要在 WASM 的内存中构建一个虚拟文件系统(VFS),将所需的文件写入其中:

// 加载字体文件到 VFS
await window.spire.FetchFileToVFS(
  'times.ttf',           // 文件名
  '/Library/Fonts/',     // VFS 中的目标路径
  `${publicUrl}/static/font/`  // 宿主环境中的资源路径
);
await window.spire.FetchFileToVFS('timesbd.ttf', '/Library/Fonts/', `${publicUrl}/static/font/`);
await window.spire.FetchFileToVFS('timesbi.ttf', '/Library/Fonts/', `${publicUrl}/static/font/`);
await window.spire.FetchFileToVFS('timesi.ttf', '/Library/Fonts/', `${publicUrl}/static/font/`);

字体文件对于 PDF 生成至关重要,它确保了输出文档中的文字能够正确渲染。

3. 文档加载与转换

const convertRtfToPdf = async () => {
  const wasmModule = window.wasmModule.spiredoc;
  if (!wasmModule) return;

  // 将输入文件写入 VFS
  await window.spire.FetchFileToVFS(
    'input.rtf', 
    '', 
    `${process.env.PUBLIC_URL}/static/data/`
  );

  // 创建 Document 实例
  const doc = new wasmModule.Document();
  
  // 从 VFS 加载 RTF 文件
  doc.LoadFromFile('input.rtf');
  
  // 保存为 PDF 格式
  const outputFileName = 'RtfToPdf.pdf';
  doc.SaveToFile({ 
    fileName: outputFileName, 
    fileFormat: wasmModule.FileFormat.PDF 
  });
  
  // 从 VFS 读取生成的 PDF
  const pdfData = window.dotnetRuntime.Module.FS.readFile(outputFileName);
  
  // 创建 Blob 并下载
  const blob = new Blob([pdfData], { type: 'application/pdf' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = outputFileName;
  a.click();
  
  // 清理
  URL.revokeObjectURL(url);
  doc.Dispose();
};

4. 资源管理与清理

WASM 模块分配的内存需要手动释放,否则可能导致内存泄漏:

// 清理 VFS 中的文件
window.dotnetRuntime.Module.FS.unlink('input.rtf');
window.dotnetRuntime.Module.FS.unlink('RtfToPdf.pdf');

// 释放 Document 对象
doc.Dispose();

兼容性与注意事项

浏览器兼容性

该方案依赖 WebAssembly 和 Blob URL 特性,适用于所有现代浏览器(Chrome、Firefox、Safari、Edge)。对于需要兼容 IE 等老旧浏览器的场景,仍需服务端转换方案兜底。

跨域问题

如果字体或 RTF 文件存放在不同的域名下,需要确保服务器配置了正确的 CORS 头。

内存限制

WASM 模块在浏览器中运行时受限于浏览器标签页的内存限制(通常约为 2GB)。对于超大文档,建议分页处理或限制单次转换的文件大小。

总结

通过 WebAssembly 技术,我们成功在浏览器端实现了 RTF 到 PDF 的转换,避免了服务端依赖,降低了架构复杂度。这种方案的核心优势在于:

  • 低延迟:所有处理在本地完成,无需网络请求
  • 低成本:无需维护转换服务器
  • 高隐私:用户文档无需上传到第三方服务

当然,这种方案也有其适用边界:对于超大文档或复杂排版,可能需要结合服务端方案共同使用。选择何种方案,需要根据具体的业务场景和技术约束来权衡。