技术文档、接口说明与发布记录越来越多地以 Markdown 的形式维护:它便于存入代码仓库、便于评审,也便于在版本之间比较差异。但对外交付时,对方往往需要一份 Word 或 PDF 文档。转到桌面软件中复制粘贴会丢失格式,表格与代码块需要重新排版;放到服务端转换,源文件又离开了用户的设备。
本文用 Spire.Doc for JavaScript 在浏览器中把 Markdown 转换为 Word 文档与 PDF 文档。该库以 WebAssembly 模块的形式在浏览器端运行,源文件通过虚拟文件系统(VFS)读入,转换结果同样写回 VFS 后再触发下载;整个过程既不需要安装 Office,也不依赖任何后端服务。
本文介绍三个功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.Doc for JavaScript。以下示例假定 Spire.Doc 已安装、WebAssembly 模块已初始化。示例使用的 Markdown 源文件与模板文档放在 public/data/ 目录下,字体文件放在 public/static/font/ 目录下。为便于直接运行,每个功能点的代码都是完整的单文件代码,整段替换 App.js 的内容即可执行。
将 Markdown 转换为 Word 文档
Spire.Doc for JavaScript 通过 Document.LoadFromFile() 载入源文件,第二个参数指定文件格式。加载 Markdown 时传入 FileFormat.Markdown,库会按 Markdown 的语法规则解析标题、段落、列表、表格、引用、代码块与图片,并把它们转换成对应的 Word 文档对象。
源文件中的各类写法与转换后套用的样式对应如下:
| Markdown 写法 | 转换后套用的样式 | 说明 |
|---|---|---|
# 至 ###### |
Heading 1 至 Heading 6 | 六级标题分别对应六个内置标题样式 |
| 普通段落 | Normal | 段落文字与粗体、斜体等行内格式一并保留 |
> 引用 |
Quote | 转为带缩进的引用段落 |
- 列表、1. 列表 |
列表段落 | 无序列表与有序列表转为自动编号的项目 |
| 管道表格 | Word 表格 | 首行作为表头,单元格带细线边框 |
| 反引号行内代码 | InlineCode | 字符级样式,只影响被标记的文字 |
| 围栏代码块 | FencedCode 加语言名 | 例如代码块标注 js 时套用 FencedCodejs |
| 缩进代码块 | FencedCode | 未标注语言的代码块 |
具体操作步骤如下:
- 获取 Spire.Doc 的 WASM 模块,确认已就绪。
- 把 Markdown 源文件读入虚拟文件系统。
- 创建
Document实例,用LoadFromFile()按FileFormat.Markdown加载源文件。 - 用
SaveToFile()把文档保存为 DOCX 文件。 - 从虚拟文件系统读出结果文件,触发浏览器下载。
将 Markdown 转换为 Word 文档的代码如下:
function App() {
const convertToWord = async () => {
// 获取 Spire.Doc WASM 模块
const docModule = window.wasmModule?.spiredoc;
// 确认 WASM 模块已完全加载
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 把 Markdown 源文件及其引用的图片读入虚拟文件系统
await window.spire.FetchFileToVFS('Sample.md', "", `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS('pipeline.png', "", `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS('msyh.ttc', "/Library/Fonts/", `${process.env.PUBLIC_URL}/static/font/`);
await window.spire.FetchFileToVFS('msyhbd.ttc', "/Library/Fonts/", `${process.env.PUBLIC_URL}/static/font/`);
// 创建 Document 实例,按 Markdown 格式加载源文件
const doc = new docModule.Document();
doc.LoadFromFile('Sample.md', docModule.FileFormat.Markdown);
// 保存为 Word 文档
const outputFileName = 'MarkdownToWord.docx';
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2016 });
doc.Dispose();
// 从虚拟文件系统读取生成的文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
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>将 Markdown 转换为 Word</h1>
<button onClick={convertToWord}>
转换为 Word
</button>
</div>
);
}
export default App;
转换得到的 Word 文档如下,标题、列表、引用、表格与代码块均已按源文件的结构还原:

将 Markdown 转换为 PDF 文档
Markdown 载入后得到的是一个完整的 Word 文档对象,因此同一份对象既可以保存为 DOCX,也可以直接导出为 PDF,不需要重新读取源文件。导出时把 SaveToFile() 的 fileFormat 换成 FileFormat.PDF 即可,页面尺寸与页边距沿用文档的节(Section)设置,分页由导出时的排版计算决定。
与 DOCX 不同,PDF 需要把文字真正栅格化,因此导出前应把文档用到的字体读入虚拟文件系统的字体目录,示例把字体放在 /Library/Fonts/。
具体操作步骤如下:
- 获取 Spire.Doc 的 WASM 模块,确认已就绪。
- 把 Markdown 源文件与字体文件读入虚拟文件系统。
- 创建
Document实例,用LoadFromFile()按FileFormat.Markdown加载源文件。 - 用
SaveToFile()把文档导出为 PDF 文件。 - 从虚拟文件系统读出结果文件,触发浏览器下载。
将 Markdown 转换为 PDF 文档的代码如下:
function App() {
const convertToPdf = async () => {
// 获取 Spire.Doc WASM 模块
const docModule = window.wasmModule?.spiredoc;
// 确认 WASM 模块已完全加载
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 把 Markdown 源文件与字体读入虚拟文件系统
await window.spire.FetchFileToVFS('Sample.md', "", `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS('pipeline.png', "", `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS('msyh.ttc', "/Library/Fonts/", `${process.env.PUBLIC_URL}/static/font/`);
await window.spire.FetchFileToVFS('msyhbd.ttc', "/Library/Fonts/", `${process.env.PUBLIC_URL}/static/font/`);
// 创建 Document 实例,按 Markdown 格式加载源文件
const doc = new docModule.Document();
doc.LoadFromFile('Sample.md', docModule.FileFormat.Markdown);
// 导出为 PDF 文档
const outputFileName = 'MarkdownToPdf.pdf';
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.PDF });
doc.Dispose();
// 从虚拟文件系统读取生成的文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
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>将 Markdown 转换为 PDF</h1>
<button onClick={convertToPdf}>
转换为 PDF
</button>
</div>
);
}
export default App;
由同一份 Markdown 导出的 PDF 文档如下:

使用模板样式转换 Markdown 为 Word 文档
按默认规则转换得到的文档使用的是内置样式,字体、配色与行距都是通用外观。实际交付时,文档往往需要符合公司或部门的版式要求:标题用什么字号、正文用什么行距、标题下方有没有分隔线。这些要求通常已经整理成一份 Word 模板,只需把模板中的样式复制到转换结果上即可。
当前 JavaScript 版本没有提供一次复制全部模板样式的方法,示例按下述方式处理:先按名称在目标文档中找到同名样式,再把模板样式的字符格式与段落格式逐项复制过去。因此模板中的样式名要与转换出的样式名一致,常用的对应关系如下:
| 模板样式名 | 影响的内容 | 示例模板中的设置 |
|---|---|---|
| Normal | 正文段落 | 微软雅黑 10.5 磅、灰色文字、1.35 倍行距 |
| Heading 1 | # 一级标题 |
18 磅加粗、深蓝、下方带分隔线 |
| Heading 2 | ## 二级标题 |
14 磅加粗、墨绿 |
| Heading 3 | ### 三级标题 |
12 磅加粗、深蓝 |
| Quote | > 引用段落 |
10 磅斜体、墨绿、左缩进 |
具体操作步骤如下:
- 获取 Spire.Doc 的 WASM 模块,确认已就绪。
- 把 Markdown 源文件与模板文档读入虚拟文件系统。
- 创建
Document实例,用LoadFromFile()按FileFormat.Markdown加载源文件。 - 另建一个
Document实例加载模板文档,按名称取出其中的样式。 - 用
copyStyle()辅助函数把模板样式的字符格式与段落格式复制到目标文档的同名样式。 - 用
SaveToFile()保存结果文件,再从虚拟文件系统读出并下载。
使用模板样式转换 Markdown 的代码如下:
// 需要从模板复制的样式名,须与 Markdown 转换后套用的样式名一致
const TEMPLATE_STYLES = ['Normal', 'Heading 1', 'Heading 2', 'Heading 3', 'Quote'];
// 把模板文档中的一条样式复制到目标文档的同名样式
function copyStyle(src, dst) {
// 字符格式:字体、字号、加粗、倾斜、文字颜色
const sf = src.CharacterFormat;
const df = dst.CharacterFormat;
df.FontName = sf.FontName;
df.FontSize = sf.FontSize;
df.Bold = sf.Bold;
df.Italic = sf.Italic;
df.TextColor = sf.TextColor;
// 段落格式:段前段后、行距、缩进、对齐与分页控制
const sp = src.ParagraphFormat;
const dp = dst.ParagraphFormat;
dp.BeforeSpacing = sp.BeforeSpacing;
dp.AfterSpacing = sp.AfterSpacing;
dp.LineSpacing = sp.LineSpacing;
dp.LineSpacingRule = sp.LineSpacingRule;
dp.LeftIndent = sp.LeftIndent;
dp.FirstLineIndent = sp.FirstLineIndent;
dp.HorizontalAlignment = sp.HorizontalAlignment;
dp.KeepFollow = sp.KeepFollow;
// 段落边框:标题下方的分隔线
const sb = sp.Borders.Bottom;
const db = dp.Borders.Bottom;
db.BorderType = sb.BorderType;
db.LineWidth = sb.LineWidth;
db.Color = sb.Color;
db.Space = sb.Space;
}
function App() {
const convertWithTemplate = async () => {
// 获取 Spire.Doc WASM 模块
const docModule = window.wasmModule?.spiredoc;
// 确认 WASM 模块已完全加载
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 把 Markdown 源文件与模板文档读入虚拟文件系统
await window.spire.FetchFileToVFS('Sample.md', "", `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS('Template.docx', "", `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS('msyh.ttc', "/Library/Fonts/", `${process.env.PUBLIC_URL}/static/font/`);
await window.spire.FetchFileToVFS('msyhbd.ttc', "/Library/Fonts/", `${process.env.PUBLIC_URL}/static/font/`);
// 按 Markdown 格式加载源文件
const doc = new docModule.Document();
doc.LoadFromFile('Sample.md', docModule.FileFormat.Markdown);
// 加载模板文档,只取其中的样式
const template = new docModule.Document();
template.LoadFromFile('Template.docx');
// 逐条把模板样式复制到目标文档的同名样式
TEMPLATE_STYLES.forEach((name) => {
const src = template.Styles.FindByName(name);
const dst = doc.Styles.FindByName(name);
if (src && dst) {
copyStyle(src, dst);
}
});
template.Dispose();
// 保存为 Word 文档
const outputFileName = 'MarkdownToWordWithTemplate.docx';
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2016 });
doc.Dispose();
// 从虚拟文件系统读取生成的文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
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>使用模板样式转换 Markdown</h1>
<button onClick={convertWithTemplate}>
转换并套用模板样式
</button>
</div>
);
}
export default App;
套用模板样式后的 Word 文档首页如下,标题、正文、引用与表格均沿用模板的字体、配色与行距:

示例 Markdown 文件
三个功能点共用同一份 Sample.md,文件放在 public/data/ 目录下。示例内容是一份产品发布说明,覆盖了一级至三级标题、无序列表、引用、管道表格、围栏代码块、行内代码、图片以及粗体与斜体,便于对照观察各类写法在 Word 中的转换结果。
图片以相对路径引用,转换前需要把图片文件一并读入虚拟文件系统,路径与 Markdown 中写的一致。
示例 Markdown 文件的内容如下:
# 北辰平台 2026.4 版本发布说明
## 概述
北辰 2026.4 聚焦**浏览器内的文档处理**。本版本把模板渲染、格式转换与批量输出整合到同一条 WebAssembly 处理链路中,源文件无需离开用户的设备。
本版本有三个目标:
- 把生成一份文档的时间从 6 分钟缩短到 10 秒以内。
- 让合同与薪酬类文件不再经过服务器转换。
- 保证输出文档的页数与屏幕预览一致。
## 更新内容
### 浏览器端转换
现在所有转换工作都在浏览器中通过 WebAssembly 模块完成。字体与模板按需从虚拟文件系统载入,结果在下载前写回同一文件系统。
> 文件在本地处理,平台不会在服务器上保存源文档。
### 支持的输入格式
| 格式 | 读取 | 写入 | 说明 |
|---|---|---|---|
| Markdown | 支持 | 支持 | 标题、列表、表格、图片 |
| Word (.docx) | 支持 | 支持 | 保留样式与域 |
| HTML | 支持 | 不支持 | 忽略外部 CSS |
| PDF | 支持 | 支持 | 页面设置沿用节属性 |
### Markdown 处理流程
Markdown 载入器把源文档结构映射为 Word 的文档构件,再由同一个文档对象导出结果:

- 标题映射为内置的 Heading 1 至 Heading 6 样式。
- 无序列表与有序列表保留嵌套层级。
- 管道表格转换为带表头的 Word 表格。
```js
const doc = new Document();
doc.LoadFromFile('Sample.md', FileFormat.Markdown);
doc.SaveToFile('Result.docx', FileFormat.Docx2016);
```
## 升级说明
载入文档时会*保留原有样式*,本版本有两处行为变化:
- Markdown 标题改用内置标题样式,不再使用直接格式。
- 表格边框默认采用单线、细线、浅灰色。
## 时间线
| 里程碑 | 日期 | 状态 |
|---|---|---|
| 设计评审 | 2026-02-12 | 已完成 |
| 核心开发 | 2026-03-01 | 已完成 |
| 集成测试 | 2026-07-20 | 进行中 |
| 验收 | 2026-10-15 | 计划中 |
模板文档 Template.docx 只提供样式,正文内容不参与转换。模板中的样式名要与转换出的样式名对应(Normal、Heading 1、Heading 2、Heading 3、Quote),名称不一致的样式不会被复制。
常见问题
转换后的文档第一段为什么是一行红色的英文提示
原因:这一行是 Spire.Doc for JavaScript 在保存文件时插入的评估版提示,内容为 Evaluation Warning: The document was created with Spire.Doc for JavaScript.,与源文件的内容无关。
解决:提示只在评估版中存在,应用许可证后保存的文件不再包含该段落。
转换出来的标题和正文为什么不是我期望的字体和行距
原因:LoadFromFile() 按内置样式转换 Markdown,标题套用 Heading 1 至 Heading 6,正文套用 Normal,这些样式本身没有定义公司文档常用的字体与配色。
解决:把版式要求整理成一份 Word 模板,按使用模板样式转换 Markdown 为 Word 文档一节的写法复制样式;也可以在加载完成后直接修改 doc.Styles 中对应样式的 CharacterFormat 与 ParagraphFormat。
Markdown 里有多个条目的有序列表,为什么转换后第 2 条以后挤进了第一段
原因:解析器按段落识别有序列表,条目之间没有空行时,第 2 条起的文字会被当成第 1 条的续行,编号也以纯文本的形式保留在段落里。
解决:在条目之间留一个空行,每条都能转换成独立编号的列表项;无序列表不受这一限制,连续书写即可:
1. 第一步。
2. 第二步。
3. 第三步。
Markdown 中的图片为什么没有显示,或者显示得特别大
原因:图片按 Markdown 中的路径从虚拟文件系统读取,文件没有读入 VFS 时无法嵌入;图片尺寸按原始像素、以 96 dpi 换算成页面尺寸,像素宽度较大的图片在 A4 页面上可能超出正文宽度。
解决:先把图片读入 VFS,路径与 Markdown 中写的保持一致:
await window.spire.FetchFileToVFS('pipeline.png', "", `${process.env.PUBLIC_URL}/data/`);
需要指定宽度时,把图片写成 HTML 的 img 标签,width 属性会按像素写入文档:
<img src="pipeline.png" width="360">
导出的 PDF 中文字体不对,或者出现空白、方框怎么办
原因:DOCX 只记录字体名称,打开时由 Word 解析;PDF 需要把文字渲染成图形,字体文件没有加载时,导出过程只能使用替代字体或留空。
解决:导出 PDF 之前,把文档中使用的字体读入虚拟文件系统的字体目录,示例把字体放在 /Library/Fonts/;中文字体较多时,建议同时加载常规体与粗体:
await window.spire.FetchFileToVFS('msyh.ttc', "/Library/Fonts/", `${process.env.PUBLIC_URL}/static/font/`);
await window.spire.FetchFileToVFS('msyhbd.ttc', "/Library/Fonts/", `${process.env.PUBLIC_URL}/static/font/`);
获取免费许可证
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。







