在 Web 项目里把纯文本导出成可编辑的 .docx,是日志整理、笔记归档、配置说明交付这类场景的高频需求。传统做法要么走服务端生成,要么依赖 Office 组件,而基于 WebAssembly 的文档处理库让整条链路可以完全发生在浏览器里。下面以 React 项目为例,把 TXT 转 Word 的实现细节、代码结构和容易踩的坑拆开讲清楚。
一、为什么要把转换放到前端
原文列出的几个典型诉求,基本覆盖了这类需求的全部动机:
- 内容导出:用户上传的纯文本直接保存为 Word,方便后续编辑、批注和打印;
- 离线处理:网络不稳或数据敏感时,文件不必上传服务器,转换在本地完成;
- 格式归档:把一批历史 TXT 批量转成 .docx,便于统一管理和检索;
- 轻量替代:只需要把文本封装成 Word,不涉及复杂模板渲染,可以省掉后端接口的开发成本。
相比后端方案,纯前端最大的优势是数据不出浏览器、响应更直接;代价则是受浏览器环境限制,并且要承担 WebAssembly 模块的初始化开销。
二、核心运行模型:WASM + 虚拟文件系统
示例使用的文档处理库运行在浏览器的虚拟文件系统(VFS)里,可以在 JS 环境中创建、读取、编辑和转换 Word 文档,既不需要安装 Microsoft Word,也不需要后端服务。
整条链路可以概括成固定的一串动作:加载 WASM 模块 → 把字体和输入文件注入 VFS → 创建 Document 实例 → 加载 TXT → 另存为 .docx → 从 VFS 读回字节数组并触发下载。理解这条链路很关键,因为后面大部分问题(字体缺失、路径不对、内存不释放)都出在其中某一环。
三、第一步:异步加载 WASM 模块
WASM 模块体积较大,适合在组件挂载后异步加载。运行时文件(spire.doc.js、.wasm 及相关依赖)要放进项目的 public 目录,因为这里走的是运行时 URL 解析,和打包器的模块解析机制不是一回事。
- import React, { useState, useEffect } from 'react';
- function App() {
- const [wasmModule, setWasmModule] = useState(null);
- useEffect(() => {
- (async () => {
- try {
- const publicUrl = process.env.PUBLIC_URL || '';
- const spireModule = await import(/* webpackIgnore: true */ `${publicUrl}/spire.doc.js`);
- const rawModule = spireModule.default || spireModule;
- 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);
- }
- })();
- }, []);
- return (
- <div style={{ textAlign: 'center', height: '300px' }}>
- <h1>Convert Text to Word in React</h1>
- <button disabled={!wasmModule}>Convert</button>
- </div>
- );
- }
- export default App;
复制代码
这段代码有三个细节值得注意:
- 注释 webpackIgnore: true 让打包器跳过对动态 import 的静态分析,避免把运行时文件当成普通依赖处理;
- locateFile 回调修正 .wasm 的加载路径,项目部署在子目录时尤其需要;
- 按钮在模块加载完成前保持 disabled 状态,防止用户提前点击触发运行时错误。
四、第二步:把字体和 TXT 文件注入 VFS
WASM 沙箱里不包含系统字体。如果文档要用到特定字体(如 Calibri,或中文内容需要的中文字体),必须先把字体文件加载到虚拟文件系统;待转换的 TXT 同样要先注入,库才能读到。
- // 字体文件放入虚拟文件系统的字体目录
- await window.spire.FetchFileToVFS(
- 'CALIBRI.ttf',
- '/Library/Fonts/',
- `${process.env.PUBLIC_URL}/static/font/`
- );
- // 输入文件放入 VFS 根目录
- const inputFileName = 'input.txt';
- await window.spire.FetchFileToVFS(
- inputFileName,
- '',
- `${process.env.PUBLIC_URL}/static/data/`
- );
复制代码
按原文约定,字体放 public/static/font/,输入文件放 public/static/data/。内容含中文时还要额外准备中文字体文件,而中文字体体积通常不小,会直接体现在首屏加载时间上。
五、第三步:创建 Document、加载 TXT、另存为 docx
资源就位后,剩下的动作就是创建实例、加载、保存、读回、下载。
- const TXTtoWord = async () => {
- const wasmModule = window.wasmModule.spiredoc;
- if (!wasmModule) return;
- // 加载字体
- await window.spire.FetchFileToVFS(
- 'CALIBRI.ttf',
- '/Library/Fonts/',
- `${process.env.PUBLIC_URL}/static/font/`
- );
- const inputFileName = 'input.txt';
- const outputFileName = 'TxtToWord.docx';
- // 将输入文件加载到 VFS
- await window.spire.FetchFileToVFS(
- inputFileName,
- '',
- `${process.env.PUBLIC_URL}/static/data/`
- );
- // 创建文档实例
- const doc = new wasmModule.Document();
- // 加载 TXT,库按扩展名自动识别为纯文本,保留原始换行
- doc.LoadFromFile(inputFileName);
- // 另存为 Word 文档
- doc.SaveToFile({
- fileName: outputFileName,
- fileFormat: wasmModule.FileFormat.Docx2016
- });
- // 从 VFS 读出生成的字节数组
- const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
- const modifiedFile = new Blob([modifiedFileArray], {
- type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
- });
- const url = URL.createObjectURL(modifiedFile);
- // 触发下载
- const a = document.createElement('a');
- a.href = url;
- a.download = outputFileName;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- // 释放 WASM 堆内存
- doc.Dispose();
- };
复制代码
几个关键点:
- LoadFromFile(inputFileName):库按文件扩展名判断格式,.txt 会被当作纯文本载入,文本按原始换行组织成文档内容;
- FileFormat.Docx2016 指定输出为较新版本的 .docx,也可以换成 Docx、Docx2013 等其他枚举值;
- 生成结果先写进 VFS,再通过 window.dotnetRuntime.Module.FS.readFile 读出为字节数组,然后包成 Blob 下载;
- Dispose() 必须调用。多次转换而不释放,WASM 堆内存会持续增长。
六、第四步:适配用户动态上传的文本
官方示例从静态路径读取 input.txt,但真实项目里内容一般来自用户上传。此时用 FileReader 或 File.arrayBuffer() 读到字节,再直接写进 VFS 即可:
- const handleFileUpload = async (event) => {
- const file = event.target.files[0];
- if (!file) return;
- const arrayBuffer = await file.arrayBuffer();
- const uint8Array = new Uint8Array(arrayBuffer);
- // 将用户上传的文件写入 VFS
- window.dotnetRuntime.Module.FS.writeFile('/input.txt', uint8Array);
- // 后续调用 LoadFromFile('/input.txt') 即可
- };
复制代码
这样就不必把文件预先塞进 public 目录。唯一要守住的规则是:写进 VFS 的路径,必须和后面 LoadFromFile 使用的路径完全一致,否则会读不到文件。
七、实践中必须留意的几个限制
- 包体积与首次加载:WASM 模块通常较大,首次加载偏慢。建议做代码分割和懒加载,等用户真正点击转换时再加载模块;
- 内存管理:每次转换后及时 Dispose(),页面里频繁导出时内存占用会明显上升;
- 字体依赖:不提前把字体放进 VFS,文本渲染可能异常;中文字体文件大,对加载时间有直接影响;
- 样式限制:TXT 本身不携带样式,转换后走的是库的默认样式。要精细控制标题、段落、字体,必须在加载后用 API 手动设置格式;
- 部署路径:运行时文件必须放在可访问的静态路径下,动态导入的 URL 解析机制和打包器不同,子目录部署要重点验证。
八、和其他方案的取舍
- 后端生成(如 docx 类库):控制力强、样式还原度高,但需要服务器,并增加一次网络往返;
- 纯前端 React 组件:声明式 API,和 React 模式契合,但需要熟悉 JSX 到文档结构的映射关系,生态相对较小;
- 基于 WebAssembly 的文档库:纯前端、无需后端、文档处理能力强,代价是包体积大、字体要额外加载;
- 简单 Blob 拼接:实现极简,但生成的不是真正的 .docx,兼容性差。
选择逻辑其实很直接:只是简单文本导出、对样式要求不高,用 WebAssembly 方案就能省掉后端开发;需要精确控制文档结构和排版,后端库或声明式组件更合适。
九、小结
本文的核心链路是:加载 WASM 模块 → 注入字体与输入文件到 VFS → new Document → LoadFromFile → SaveToFile(Docx2016) → FS.readFile 读回并触发下载,同时用 FS.writeFile 打通了用户上传的分支。WebAssembly 让浏览器具备了处理复杂文档格式的能力,但包体积、字体加载和内存释放这三个细节决定了方案能否真正上生产。先把边界搞清楚,再做技术选型,比先写代码再补坑要稳得多。 |