在技术写作和文档协作场景中,Word 和 Markdown 各有其位置。Word 适合需要精细排版的正式文档,Markdown 则以纯文本的简洁性见长。当两者需要在同一个系统中共存时,双向转换就成为一项实际需求------用户可能希望把一份 Word 报告导出为 Markdown 来纳入版本管理,也可能需要把 Markdown 编写的技术文档转换成 Word 格式提交给不熟悉 Markdown 的协作者。
本文记录在 React 应用中实现 Word 与 Markdown 双向转换的过程,涉及环境配置、模块初始化、转换逻辑,以及两种输入方式的选择依据。
为什么在客户端完成转换
传统的文档转换通常依赖后端服务:文件上传到服务器,服务端调用相应的库进行处理,再把结果返回给前端。这种模式成熟,但文件需要离开本地环境,转换过程受网络状况影响,后端也需要部署和维护额外的文档处理服务。
WebAssembly 让浏览器具备了运行完整文档处理库的能力。转换逻辑可以在前端完成,文件数据不需要上传,转换结果即时可用。代价是浏览器端需要加载 WASM 模块和相关资源,首次使用的等待时间会比调用后端接口更长,具体取决于模块体积和网络条件。
需要说明的是,客户端方案并非在所有场景下都优于服务端。文档体积较大、格式特别复杂,或对转换稳定性要求很高时,服务端方案的可控性通常更好。客户端方案更适合文档规模适中、对数据本地化有要求的场景。
环境准备与模块初始化
安装通过 npm:
bash
npm i spire.office
安装完成后,需要把 spire.doc.js、Spire.Doc.Wasm.zip 及相关运行时文件复制到 React 项目的 public 目录。WebAssembly 模块是异步加载的,在组件里用 useEffect 在挂载时初始化:
javascript
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 spire.doc.js WASM module:', error);
}
})();
}, []);
}
/* webpackIgnore: true */ 阻止 Webpack 对动态导入路径做打包处理,让运行时从 public 目录按原路径加载脚本。locateFile 回调确保 .wasm 文件从同一目录取。wasmModule 这个 state 用于在加载完成后才启用转换按钮。
把模块挂到 window 上是一种可行做法。更严格的项目可以把实例保存在 ref 或 context 中,避免污染全局作用域。
字体加载与虚拟文件系统
文档处理库通过虚拟文件系统(VFS)管理输入输出文件。转换前需要先把字体加载到 VFS,否则生成的文档可能出现排版问题或字符缺失。
FetchFileToVFS 接收三个参数:文件名、VFS 中的目标路径、源文件所在的 URL 前缀:
javascript
await window.spire.FetchFileToVFS('CALIBRI.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
字体选哪个取决于文档中实际使用的字体。如果内容含中文,只加载西文字体是不够的。 CALIBRI.ttf 不包含中文字形,只加载它,中文会变成方块、问号或空白。需要额外加载一款中文字体,比如宋体:
javascript
// 西文字体,用于数字和英文
await window.spire.FetchFileToVFS('CALIBRI.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
// 中文字体
await window.spire.FetchFileToVFS('SimSun.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
字体文件可以从系统字体目录(Windows 下是 C:\Windows\Fonts)复制,商业分发前需确认授权许可。中文字体通常几 MB 到十几 MB,比西文字体大得多,会明显增加加载时间。只用到少量汉字时,可以对字体做子集化处理。
将 Word 文档转换为 Markdown
Word 转 Markdown 的核心流程分为四步:加载字体、把 Word 文件载入 VFS、创建文档对象并加载、保存为 Markdown 格式。
javascript
const ConvertWordToMD = async () => {
const wasmModule = window.wasmModule.spiredoc;
if (wasmModule) {
await window.spire.FetchFileToVFS('CALIBRI.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
const inputFileName = 'sample.docx';
const outputFileName = 'WordToMarkdown.md';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);
const doc = new wasmModule.Document();
doc.LoadFromFile(inputFileName);
doc.SaveToFile({ fileName: outputFileName, fileFormat: wasmModule.FileFormat.Markdown });
doc.Dispose();
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { type: 'text/plain' });
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);
}
};
SaveToFile 的 fileFormat 参数指定为 Markdown。Word 中的段落、标题、列表、粗体斜体等格式会映射到对应的 Markdown 语法。表格和图片的处理相对复杂,Word 中的复杂表格在转换后可能变成 Markdown 的管道表格(pipe table),如果单元格内含有多行内容或合并单元格,转换结果的保真度会下降。
将 Markdown 转换为 Word
Markdown 转 Word 的流程与反向转换类似,区别在于输入环节的处理方式,以及输出格式的指定。下面的示例使用中文内容,因此需要配合前面加载的宋体。
javascript
const ConvertMDToWord = async () => {
const wasmModule = window.wasmModule.spiredoc;
if (wasmModule) {
// 加载西文字体
await window.spire.FetchFileToVFS('CALIBRI.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
// 加载中文字体
await window.spire.FetchFileToVFS('SimSun.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
const doc = new wasmModule.Document();
const outputFileName = 'MarkdownToWord.docx';
const markdownString = `# 项目概述
这是一个示例文档。
## 功能列表
- 实时数据处理
- 自定义场景配置
- 历史数据对比分析
## 命令示例
| 命令 | 说明 |
|------|------|
| region=asia | 运行区域模拟 |
| year=2050 | 对比不同年份场景 |`;
// 将 Markdown 字符串写入 VFS
await window.dotnetRuntime.Module.FS.writeFile('Markdown.md', markdownString, {encoding: 'utf8'})
doc.LoadFromFile({ fileName: 'Markdown.md', fileFormat: wasmModule.FileFormat.Markdown });
doc.SaveToFile({fileName: outputFileName, fileFormat: wasmModule.FileFormat.Docx2019});
doc.Dispose();
const outputWordFile = await window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([outputWordFile], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
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);
}
};
加载 Markdown 文件时需要显式指定 fileFormat 为 Markdown,否则解析器可能无法正确识别格式。输出格式这里用的是 Docx2019,也可以选择更早的 Word 版本格式。Markdown 中的标题层级、列表、代码块会映射到 Word 的对应样式,代码块通常以等宽字体呈现。
Markdown 字符串中的列表格式有一个需要注意的地方:段落之间需要留空行,否则列表缩进可能出现异常。上面的示例在标题、段落、列表、表格之间都保留了空行,符合规范。表格和链接图片的转换在较新版本中已有所改善,但复杂结构(如合并单元格)仍可能丢失部分格式。
输入环节的两种处理方式
官网的 Markdown 转 Word 示例采用的是字符串写入方式,而不是从静态目录加载文件。这两种处理方式值得区分。
通过 FetchFileToVFS 加载文件 ,适合内容已经以文件形式存在的场景:用户上传的 .md 文件、构建时已经放进 public 目录的文档、从后端下载后暂存的临时文件。
javascript
await window.spire.FetchFileToVFS('MarkdownExample.md', '', `${process.env.PUBLIC_URL}/static/data/`);
doc.LoadFromFile({fileName: 'MarkdownExample.md', fileFormat: wasmModule.FileFormat.Markdown});
通过 FS.writeFile 写入字符串,适合内容由代码动态生成的场景:用户在线编辑器里的内容、从接口获取的 Markdown 文本、模板拼接出的文档。
javascript
await window.dotnetRuntime.Module.FS.writeFile('Markdown.md', markdownString, {encoding: 'utf8'})
doc.LoadFromFile({fileName: 'Markdown.md', fileFormat: wasmModule.FileFormat.Markdown});
window.dotnetRuntime.Module.FS 是 Emscripten 运行时暴露的虚拟文件系统接口,writeFile 是它的标准方法之一。写入后,VFS 中就有了对应的文件,后续 LoadFromFile 从 VFS 读取,与从静态目录加载的文件在行为上没有区别。
两种方式最终都汇入同一个 LoadFromFile 调用,之后的保存和下载逻辑完全相同。选哪种,取决于输入内容从哪来,而不是转换本身的要求。
实际使用中的观察
格式保真度存在方向性差异。 Word 到 Markdown 的转换,由于 Markdown 的表达能力有限,复杂的 Word 格式(如多级列表的精确缩进、文本框、页眉页脚)会被简化或丢失。Markdown 到 Word 的转换相对可控,因为 Markdown 的语法规则清晰且有限。
字体加载是影响体验的主要因素。 中文字体的体积使得首次转换的等待时间可能在数秒到十几秒之间,具体取决于字体文件和网络速度。如果应用会频繁使用转换功能,可以在初始化阶段预加载字体,或对字体做子集化处理。
文件命名和并发需要留意。 FetchFileToVFS、FS.writeFile 和 SaveToFile 都操作 VFS 中的文件,如果同名文件被并发写入,后一次操作可能覆盖前一次的结果。频繁转换的场景需要自行做串行化处理。
错误处理不可省略。 WASM 模块加载、文件读取、格式解析都可能因网络、字体或文件内容问题而失败。把转换逻辑包在 try-catch 中并给用户明确的错误提示,能减少排查成本。
总结
本文覆盖了 React 中 Word 与 Markdown 双向转换的完整实现:模块初始化、字体加载、文件与字符串两种输入方式,以及两个方向的转换逻辑。核心 API 集中在 LoadFromFile 与 SaveToFile 两个方法上,差异主要体现在加载时是否需要显式声明源格式。
从验证结果看,方案能否落地主要取决于两个条件:字体是否完备,以及文档结构是否落在 Markdown 的表达能力之内。前者决定转换是否成功,后者决定输出是否可用。这两个条件由需求端决定,实现层面无法绕开。
因此,这套方案更适合文档结构简单、对数据本地化有要求的场景;格式复杂或排版精度要求较高时,服务端方案的成熟度仍是更稳妥的选择。