在 React 中使用 JavaScript 将 Markdown 转换为 PDF

Markdown 凭借简洁的语法,已成为技术文档与内容创作的主流格式之一。然而在实际交付或归档场景中,PDF 因其版式固定、跨平台一致性好的特点,仍是更稳妥的分发选择。在纯前端环境中完成这一转换,可以避免服务端渲染的资源开销,也让数据留在浏览器本地。本文以 React 为框架,介绍如何借助一个基于 WebAssembly 的文档处理库实现 Markdown 到 PDF 的转换,并给出可运行的代码示例与流程要点分析。

技术方案概述

本文使用的库并非纯粹的 Markdown 语法解析器,而是一个文档对象模型处理引擎的 WebAssembly 移植版本。它支持 Word 文档与多种格式互转,其中包括 Markdown 与 PDF。在 React 项目中,该库通过虚拟文件系统(VFS)管理输入输出文件,利用 WebAssembly 在浏览器端完成解析与渲染,不依赖后端服务或本地 Office 软件。

如果仅需在页面上渲染 Markdown,使用 marked 或 markdown-it 是更轻量的选择;若目标是生成可供下载的 PDF 文件,则文档处理库提供了一条更直接的路径。

环境准备与资源部署

首先通过 NPM 安装运行时包:

bash 复制代码
npm install spire.doc

安装完成后,需要将 WebAssembly 运行时资源从 node_modules 复制到项目的 public 目录。具体需要复制的文件和目录取决于所安装包的版本与结构,建议以官方集成文档列出的资源清单为准。字体文件和示例 Markdown 文件则放在 public/static/font/、public/static/data/ 这类静态目录下,供 FetchFileToVFS 按路径读取。

bash 复制代码
# 字体与数据文件的约定位置
# public/static/font/CALIBRI.ttf    (西文)
# public/static/font/SimSun.ttf     (中文,按需添加)
# public/static/data/MarkdownExample.md

React 组件实现

下面是基于官方示例整理的核心组件。初始化与转换逻辑分离,按钮在模块就绪前保持禁用。

jsx 复制代码
import React, { useEffect, useState } from 'react';

function App() {
  const [wasmModule, setWasmModule] = useState(null);

  // 初始化 WebAssembly 模块
  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: (path) => path.endsWith('.wasm') ? `${publicUrl}/${path}` : path })
            : rawModule;

        setWasmModule(window.wasmModule);
      } catch (error) {
        console.error('Failed to load the WASM module:', error);
      }
    })();
  }, []);

  // 从 WASM 虚拟文件系统下载生成的文件
  const downloadVfsFile = (fileName, mimeType) => {
    const fileData = window.dotnetRuntime.Module.FS.readFile(fileName);
    const blob = new Blob([fileData], { type: mimeType });
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');

    link.href = url;
    link.download = fileName;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    URL.revokeObjectURL(url);
  };

  const convertMarkdownToPdf = async () => {
    const docModule = window.wasmModule?.spiredoc;
    if (!docModule) return;

    const publicUrl = process.env.PUBLIC_URL || '';

    // 加载西文字体到 VFS
    await window.spire.FetchFileToVFS( 'CALIBRI.ttf', '/Library/Fonts/', `${publicUrl}/static/font/` );

    // 若 Markdown 含中文,再加载一款中文字体
    await window.spire.FetchFileToVFS('SimSun.ttf', '/Library/Fonts/', `${publicUrl}/static/font/`);

    const inputFileName = 'MarkdownExample.md';
    const outputFileName = 'MarkdownToPDF.pdf';

    // 将 Markdown 文件载入 VFS
    await window.spire.FetchFileToVFS( inputFileName, '', `${publicUrl}/static/data/` );

    const doc = new docModule.Document();

    try {
      doc.LoadFromFile({ fileName: inputFileName, fileFormat: docModule.FileFormat.Markdown });

      doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.PDF });

      downloadVfsFile( outputFileName, 'application/pdf' );
    } finally {
      doc.Dispose();
    }
  };

  return (
    <div style={{ textAlign: 'center', padding: '40px' }}>
      <h1>Convert Markdown to PDF</h1>
      <button onClick={convertMarkdownToPdf} disabled={!wasmModule}>
        Convert and Download PDF
      </button>
    </div>
  );
}

export default App;

如果 Markdown 内容不是来自文件,而是直接来自 React 中的字符串,可以改为先写入 VFS 再加载。写法上把 FS.writeFile 的编码显式声明为 utf8,其余流程一致:

jsx 复制代码
const convertMarkdownStringToPdf = async () => {
  const docModule = window.wasmModule?.spiredoc;
  if (!docModule) return;

  const publicUrl = process.env.PUBLIC_URL || '';

  await window.spire.FetchFileToVFS( 'CALIBRI.ttf', '/Library/Fonts/', `${publicUrl}/static/font/` );
  // 中文内容需追加中文字体加载,见前文说明

  const markdownString = `# 项目记录

本文档由 **存储在 React 字符串中的 Markdown** 生成。

## 任务

- 审阅初稿
- 导出终稿
- 分享 PDF
`;

  const inputFileName = 'MarkdownInput.md';
  const outputFileName = 'MarkdownStringToPDF.pdf';

  window.dotnetRuntime.Module.FS.writeFile( inputFileName, markdownString, { encoding: 'utf8' } );

  const doc = new docModule.Document();

  try {
    doc.LoadFromFile({ fileName: inputFileName, fileFormat: docModule.FileFormat.Markdown });

    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.PDF });

    downloadVfsFile(outputFileName, 'application/pdf');
  } finally {
    doc.Dispose();
  }
};

转换流程的关键环节

结合代码,可以把流程归纳为三个关键环节。

第一步:字体与源文件载入 VFS。 在调用转换方法前,先通过 FetchFileToVFS 把字体文件写入 /Library/Fonts/,再把 Markdown 文件或字符串写入虚拟文件系统。字体是渲染正确识别字符的前提;源文件与字符串两条路径的差别只在于前者用 FetchFileToVFS、后者用 FS.writeFile。

第二步:文档对象的创建与加载。 通过 new docModule.Document() 创建实例后,调用 LoadFromFile 并传入包含 fileName 与 fileFormat 的对象,其中 fileFormat 取 docModule.FileFormat.Markdown,告知引擎按 Markdown 语法解析。FileFormat 是模块导出的枚举,而非字符串常量。

第三步:导出为 PDF 并触发下载。 调用 SaveToFile 并指定 docModule.FileFormat.PDF,将文档保存到 VFS;随后通过 FS.readFile 读取字节数据,封装为 Blob 并模拟点击下载。try/finally 中的 doc.Dispose() 用于释放文档对象,避免反复转换造成资源累积,这一处理方式在官方示例中被保留,可以沿用到自己的项目中。

实践中的注意事项

中文字体需要单独加载。 CALIBRI.ttf 字体不含中文字形。如果 Markdown 内容包含中文而只加载了西文字体,生成的 PDF 中中文可能显示为方块或乱码。若文档含中文,应再调用一次 FetchFileToVFS 加载中文字体(如 SimSun.ttf)。此外,字体是否被正确匹配,取决于文件内部的 family name 而非文件名本身;若声明的字体名与 VFS 中加载字体的内部名称不一致,渲染仍可能回退。

复杂 Markdown 语法的支持程度。 该库以 Word 文档处理为核心,其 Markdown 解析侧重将语法映射为文档对象,而非追求与 CommonMark 规范完全一致。标准标题、段落、列表、代码块、表格等常见语法转换结果通常可用;但嵌套极深的列表或非标准扩展语法,输出效果可能需要额外验证。对边界情况做测试,是引入任何文档库处理 Markdown 时都不可省略的一步。

包体积与加载策略。 WebAssembly 模块及运行时资源体积较大,首屏同步引入会明显影响性能。官方示例采用动态导入并在 useEffect 中异步初始化,按钮在 wasmModule 就绪前保持禁用;字体文件也应避免一次性全量加载,可根据文档内容的实际语言分布按需请求。

试用版本的功能限制。 该库提供社区版(免费)与商业版。社区版在转换 PDF 时存在页数限制(仅前 3 页),且不提供技术支持。如果项目需要完整输出,需评估是否申请临时许可证或采购商业授权。在技术选型阶段,建议先用实际文档验证输出页数与内容完整性是否满足需求。

适用场景与边界

这一方案适合以下场景:项目已经在使用同一文档处理库完成其他任务(如生成 Word 报表),需要复用已有基础设施完成 Markdown 导出;或对 PDF 版式控制有较高要求,希望借助文档引擎的排版能力而非浏览器打印功能来生成输出。

反之,如果需求仅限于将 Markdown 内容以 PDF 形式呈现,且对样式没有超出浏览器默认打印的期望,那么利用 window.print() 配合打印样式表,或使用 jsPDF 配合 html2canvas 等纯前端方案,在依赖体积和引入成本上可能更具优势。将文档处理库用于单一的 Markdown 转 PDF 任务时,需要根据项目对输出质量、包体积和授权成本的综合考量来做取舍。

结语

在 React 中借助基于 WebAssembly 的文档处理库将 Markdown 转换为 PDF,技术上是一条可行的路径。实现过程中,模块初始化方式、字体管理(尤其是中文字体的单独加载)、VFS 的读写顺序以及 Dispose 的调用,是影响集成是否顺畅的几个因素。开发者在选型时,建议先以项目中的实际 Markdown 样本做一轮输出质量与性能验证,再决定是否将其纳入技术栈。

相关推荐
Hilaku1 小时前
为什么我在团队里禁止使用 axios?
前端·javascript·程序员
一只游鱼1 小时前
我用原生 JS 做了一个「会呼吸」的博客:Canvas 水境 + 合成音效 + 会破裂的留言气泡(vibe coding)
javascript·agent·canvas·github pages
stereohomology2 小时前
外挂可能是用AI工具自定义solomd插件和主题适合的方式
人工智能·markdown·薅羊毛·solomd·外挂新特性
只睡四小时2 小时前
Local-First 数据契约:一张表装下 4 类数据
前端·javascript·架构设计·local-first·数据契约
Java后端的Ai之路2 小时前
19-ReactJS最佳实践
前端·react.js·前端框架
云浪13 小时前
Go 并发实战:从零实现一个高并发安全的任务调度器
javascript·后端·go
OpsEye21 小时前
大模型账单对账难:不同厂商 Token 统计口径差异怎么处理
javascript·ai编程
全栈项目管理程序猿21 小时前
ArcGIS JS 基础教程(32):体元系列 - VoxelSlice 体元切片
前端·javascript
结局无敌1 天前
React Native for OpenHarmony 实战:三方库 react-native-modal-dialog 的鸿蒙化适配指南
深度学习·算法·react native·react.js·harmonyos