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

20 阅读6分钟

Markdown 作为一种轻量级标记语言,广泛应用于技术文档、README 文件以及各类内容管理系统中。在 Web 前端开发中,将 Markdown 内容转换为 HTML 以便在页面上渲染或导出,是一个常见的功能需求。目前社区已有 marked、markdown-it 等专注于语法解析的轻量库,它们在前端场景中表现稳定。

然而,当项目需求超出单纯的语法渲染——例如需要将 Markdown 导出为可独立分发的 HTML 文件,或后续需要进一步转换为 PDF、Word 等格式时,引入一个具备文档对象模型处理能力的方案可能更为合适。本文将介绍一种基于 WebAssembly 的浏览器端转换思路,以 Spire.Doc for JavaScript 为例,展示在 React 项目中完成 Markdown 到 HTML 转换的完整流程。

环境配置与模块加载

该方案的核心依赖是一个编译为 WebAssembly 的文档处理引擎。在 React 项目中,首先需要通过 NPM 安装运行时包,并将必要的 WASM 资源文件复制到 public 目录。典型的资源文件包括 spire.doc.js、Spire.Doc.Wasm.zip 以及 _framework 目录等。

在 React 组件中,WASM 模块需要在组件挂载后异步加载。以下是最小化的初始化代码:

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

        setWasmModule(window.wasmModule);
      } catch (error) {
        console.error('WASM 模块加载失败:', error);
      }
    })();
  }, []);
}

webpackIgnore: true 注释的作用是阻止 Webpack 对动态导入路径进行重写,确保运行时能按实际路径加载资源。模块加载完成后存入 state,用于控制转换按钮的可用状态。

字体文件的处理

文档转换中,字体的可用性直接影响输出质量。该方案通过虚拟文件系统(VFS)管理字体资源,转换前需要将字体文件加载到 VFS 的指定路径。对于纯英文内容,加载一款西文字体即可:

await window.spire.FetchFileToVFS( 'CALIBRI.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/` );

当 Markdown 内容包含中文时,情况有所不同。中文字符需要相应的中文字体支持,否则生成的 HTML 中可能出现乱码或空白方框。因此,中文场景下需要额外加载中文字体文件,例如 SimSun 或 SimHei。字体文件通常体积较大,建议根据实际内容按需加载,而非在页面初始化时全部载入。

转换 Markdown 文件为 HTML

当 Markdown 以 .md 文件形式存在时,核心流程分为三步:将文件加载到 VFS、通过 Document 实例解析、导出为 HTML。以下为文件转换的主要代码:

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

  // 加载字体
  await window.spire.FetchFileToVFS( 'CALIBRI.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/` );

  // 将 Markdown 文件加载到 VFS
  const inputFileName = 'MarkdownExample.md';
  await window.spire.FetchFileToVFS( inputFileName, '', `${process.env.PUBLIC_URL}/static/data/` );

  const doc = new wasmModule.Document();

  try {
    // 显式指定以 Markdown 格式加载
    doc.LoadFromFile({ fileName: inputFileName, fileFormat: wasmModule.FileFormat.Markdown });

    // 配置 HTML 导出选项
    doc.HtmlExportOptions.CssStyleSheetType = wasmModule.CssStyleSheetType.Internal;
    doc.HtmlExportOptions.ImageEmbedded = true;

    // 保存为 HTML
    const outputFileName = 'MarkdownToHtml.html';
    doc.SaveToFile({ fileName: outputFileName, fileFormat: wasmModule.FileFormat.Html });

    // 从 VFS 读取生成的文件并触发下载
    const htmlBytes = window.dotnetRuntime.Module.FS.readFile( outputFileName );
    const blob = new Blob( [htmlBytes], { type: 'text/html;charset=utf-8' } );
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = outputFileName;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    URL.revokeObjectURL(url);
  } finally {
    doc.Dispose();
  }
};

几个关键点值得注意:LoadFromFile 通过 fileFormat 参数显式声明输入格式为 Markdown;CssStyleSheetType.Internal 将样式内联到 HTML 中,生成的单文件便于分发;ImageEmbedded = true 将图片以 Data URI 形式嵌入,避免外部资源依赖。Dispose() 放在 finally 中确保资源释放。

转换 Markdown 字符串为 HTML

实际项目中,Markdown 内容常以字符串形式来自用户输入或 API 响应,而非文件。这时可以利用 VFS 的 writeFile 方法将字符串写入虚拟文件系统,后续流程与文件转换完全一致:

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

  // 中文内容需加载中文字体
  await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);

  const markdownString = `# 项目说明文档

本项目提供一个基于浏览器的文档转换工具。

## 主要功能

- 将 Markdown 转换为 HTML
- 在浏览器端完成内容处理

## 支持的内容

| 功能 | 是否支持 |
|------|----------|
| 标题 | 是 |
| 列表 | 是 |
| 表格 | 是 |

访问 [示例网站](https://example.com) 了解更多信息。
`;

  const inputFileName = 'MarkdownString.md';
  const outputFileName = 'MarkdownStringToHtml.html';

  // 将字符串以 UTF-8 编码写入 VFS
  window.dotnetRuntime.Module.FS.writeFile( inputFileName, markdownString, { encoding: 'utf8' } );

  const doc = new wasmModule.Document();

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

    doc.HtmlExportOptions.CssStyleSheetType = wasmModule.CssStyleSheetType.Internal;
    doc.HtmlExportOptions.ImageEmbedded = true;

    doc.SaveToFile({ fileName: outputFileName, fileFormat: wasmModule.FileFormat.Html });

    const htmlBytes = window.dotnetRuntime.Module.FS.readFile( outputFileName );
    const blob = new Blob( [htmlBytes], { type: 'text/html;charset=utf-8' } );
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = outputFileName;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    URL.revokeObjectURL(url);
  } finally {
    doc.Dispose();
  }
};

字符串转换与文件转换的差异仅在于输入环节:前者通过 FS.writeFile 写入,后者通过 FetchFileToVFS 拉取。后续的解析、导出、下载逻辑可以抽取为公共函数复用。

中文内容处理的注意事项

当 Markdown 内容包含中文时,有三个环节需要关注:

字体的必要性。 若不加载中文字体,生成的 HTML 中汉字可能无法正确渲染。字体需通过 FetchFileToVFS 加载到 /Library/Fonts/ 路径下,引擎在渲染时会从该路径查找可用字体。

编码一致性。 FS.writeFile 写入时指定 encoding: 'utf8',生成 HTML 的 Blob 类型声明为 text/html;charset=utf-8,两端保持一致可避免乱码。

字体体积与加载策略。 中文字体文件通常为数 MB 到十几 MB,在页面初始化时全部加载会拖慢首屏。建议在用户触发转换操作时再加载字体,或根据内容语言动态决定加载哪些字体。

在 React 中预览转换结果

上述示例将生成的 HTML 作为文件下载。如果需要在页面上直接预览,可以将读取到的字节解码为字符串后渲染:

const htmlBytes = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const htmlContent = new TextDecoder('utf-8').decode(htmlBytes);
setPreviewHtml(htmlContent);

使用 dangerouslySetInnerHTML 渲染时需注意,若 Markdown 内容来自不可信来源,建议先经过 DOMPurify 等库净化,以防范 XSS 风险。

方案对比与适用场景

从技术选型的客观角度,纯 Markdown 语法解析场景下,marked 或 markdown-it 等库在体积、解析速度和依赖复杂度上更具优势,且无需处理 WASM 初始化和字体加载的开销。Spire.Doc for JavaScript 的合理定位在于以下场景:

  • 一体化文档处理:同一个库既能完成 Markdown 到 HTML 的预览级转换,也支持后续导出为 PDF、图片等格式,减少技术栈碎片化。
  • 浏览器端独立运行:所有处理在客户端完成,不依赖后端服务,适合对数据隐私敏感或希望降低服务器负载的场景。
  • 格式保真度需求:对于包含复杂格式的文档,基于文档对象模型的转换方案在输出质量上通常优于纯文本解析。

小结

在 React 项目中实现 Markdown 到 HTML 的浏览器端转换,核心步骤包括:异步加载 WASM 模块、将字体和源内容载入虚拟文件系统、通过文档对象模型完成格式转换、读取结果并下载或渲染。文件与字符串两种输入形式的差异仅在数据写入环节,后续逻辑可统一处理。当中文内容参与转换时,中文字体的加载与 UTF-8 编码的保持一致是保证输出质量的关键。这一方案适合那些对文档导出能力有额外需求的应用场景;若仅需基础语法渲染,轻量级解析库仍是更简洁的选择。