React 中实现 Word 与 Markdown 双向转换的实践记录

在技术写作和文档协作场景中,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 的表达能力之内。前者决定转换是否成功,后者决定输出是否可用。这两个条件由需求端决定,实现层面无法绕开。

因此,这套方案更适合文档结构简单、对数据本地化有要求的场景;格式复杂或排版精度要求较高时,服务端方案的成熟度仍是更稳妥的选择。

相关推荐
OpsEye3 小时前
大模型会话里的手机号、身份证,怎么自动脱敏?
javascript·ai编程
爱喝水的小周3 小时前
实验十 Vue‑Router 王者荣耀战绩查询 APP 开发实践
前端·javascript·vue.js
yexianglunbai3 小时前
Vue 3 全面详解:从核心特性到工程化实践
前端·javascript·vue.js
孟陬4 小时前
JavaScript 反引号(` backtick)和双引号的不完全等价 ≠ 场景
javascript·node.js·bun
JavaPub-rodert4 小时前
开源一套 Go + React 后台管理系统,支持 RBAC 权限管理、Docker 一键部署!
react.js·golang·开源
谢亮_vipxieliang5 小时前
深入理解 JavaScript 数据类型与类型转换
开发语言·javascript·ecmascript
程序员Sunday14 小时前
JavaScript 事件循环面试题,宏任务与微任务怎么执行|Sunday面试指南
开发语言·javascript·面试·校招·事件循环·程序员sunday
默_笙15 小时前
🌊 向量库和 ES 都查不出"关系",我只好给奶茶建了一张人脉网
前端·javascript
思无邪6616 小时前
用 AI 做 JS 逆向:从抓包到复现的完整方法论
开发语言·javascript·人工智能