在技术文档和博客写作中,图表是表达复杂概念和流程的重要工具。传统的做法是使用专业绘图软件制作图片,但这种方式存在维护困难、版本控制不友好等问题。Mermaid 作为一种基于文本的图表描述语言,让我们可以用代码的方式创建流程图、时序图、类图等各种图表。
在我的 Astro 博客项目中,我希望能够在 MDX 文件中直接写 Mermaid 代码,然后自动渲染成美观的图表。这个需求看似简单,但实际实现涉及到 Markdown 处理管道、remark 插件系统、客户端渲染等多个技术环节。
经过研究和实践,我实现了一套完整的 Mermaid 渲染方案:
- 在 MDX 文件中直接写 ```mermaid 代码块
- 构建时通过 remark 插件自动转换为 HTML
- 客户端动态加载 Mermaid 库并渲染图表
- 支持亮色/暗色主题自动切换
- 响应式设计,移动端友好
这套方案的核心是理解 remark 在 Astro 构建流程中的作用,以及如何编写自定义插件来扩展 Markdown 处理能力。
演示#
你可以在我其他的博客文章中看到 Mermaid 图表的实际渲染效果,包括流程图、时序图等多种类型的图表。
功能点详解#
remark 的作用和地位#
在深入实现细节之前,我们需要先理解 remark 在整个 Astro 构建流程中的作用。
什么是 remark? remark 是一个基于 unified 生态系统的 Markdown 处理器。它的核心理念是将 Markdown 文本转换为抽象语法树(AST),然后通过插件对 AST 进行各种转换,最后输出为目标格式。
remark 在 Astro 中的工作流程:
remark 插件的工作原理:
- AST 遍历:插件通过
visit
函数遍历语法树的每个节点 - 节点识别:根据节点类型和属性识别需要处理的内容
- 节点转换:修改、替换或删除节点
- 新节点创建:根据需要创建新的 AST 节点
Mermaid 渲染的技术挑战#
实现 Mermaid 渲染面临几个技术挑战:
1. 构建时 vs 运行时渲染
- 构建时渲染:需要在 Node.js 环境中运行 Mermaid,但 Mermaid 依赖浏览器 API
- 运行时渲染:需要在客户端动态加载和渲染,影响首屏性能
2. 主题适配
- Mermaid 图表需要适配网站的亮色/暗色主题
- 主题切换时需要重新渲染图表
3. 响应式设计
- 图表需要在不同屏幕尺寸下正常显示
- 需要处理图表过宽的情况
4. 错误处理
- Mermaid 语法错误时需要优雅降级
- 网络加载失败时的备选方案
核心实现策略#
基于以上挑战,我选择了以下实现策略:
1. 混合渲染方案
- 构建时:remark 插件转换代码块为 HTML 容器
- 运行时:客户端动态渲染 Mermaid 图表
2. 延迟加载
- 使用 CDN 动态加载 Mermaid 库
- 避免增加主包体积
3. 主题响应式
- 监听 DOM 变化检测主题切换
- 自动重新渲染图表
4. 优雅降级
- 渲染失败时显示原始代码
- 提供错误提示信息
架构图解#
整体处理流程#
remark 插件处理流程#
客户端渲染时序图#
主题切换处理流程#
代码实现#
Astro 配置中注册插件#
// astro.config.ts
import { remarkMermaid } from './src/plugins/remark-mermaid.ts'
export default defineConfig({
markdown: {
remarkPlugins: [remarkMath, remarkMermaid],
rehypePlugins: [
[rehypeKatex, {}],
rehypeHeadingIds,
[rehypeAutolinkHeadings, { /* ... */ }]
],
// ...其他配置
}
})
typescript配置要点:
- remarkMermaid 插件需要在 remarkPlugins 数组中注册
- 插件执行顺序很重要,Mermaid 插件应该在其他文本处理插件之前
- 可以与其他 remark/rehype 插件配合使用
remark 插件核心实现#
// src/plugins/remark-mermaid.ts
import type { Root } from 'mdast'
import type { Plugin } from 'unified'
import { visit } from 'unist-util-visit'
export const remarkMermaid: Plugin<[], Root> = function () {
return function (tree) {
// 遍历 AST 中的所有节点
visit(tree, 'code', (node, index, parent) => {
// 只处理 mermaid 语言的代码块
if (node.lang !== 'mermaid') {
return
}
if (!parent || index === undefined) {
return
}
// 生成唯一 ID 避免冲突
const id = `mermaid-${Math.random().toString(36).substring(2, 11)}`
// 创建包含脚本的 HTML 节点
const htmlNode = {
type: 'html',
value: `
<div class="mermaid-container">
<div id="${id}" class="mermaid-diagram" data-code="${node.value.replace(/"/g, '"')}">${node.value}</div>
</div>
<script type="module">
// 动态加载和渲染逻辑
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
// ... 渲染脚本,即客户端渲染核心逻辑
</script>
`
}
// 替换原始代码块节点
parent.children[index] = htmlNode as any
})
}
}
typescript实现要点:
- 使用
visit
函数遍历 AST,只处理type: 'code'
且lang: 'mermaid'
的节点 - 生成唯一 ID 避免多个图表之间的冲突
- 将 Mermaid 代码保存在
data-code
属性中,便于客户端获取 - 创建
type: 'html'
节点替换原始代码块
客户端渲染核心逻辑#
客户端渲染代码在 remark 插件中生成,并已嵌入在 HTML 中。
// 嵌入在 HTML 中的渲染脚本
async function renderMermaidDiagrams() {
const currentTheme = getCurrentTheme()
configureMermaid(currentTheme)
const diagrams = document.querySelectorAll('.mermaid-diagram')
for (const diagram of diagrams) {
const element = diagram
// 获取原始代码
let code = element.dataset.code || element.textContent?.trim()
if (!code || element.dataset.rendered === 'true') continue
try {
// 清除之前的内容
element.innerHTML = ''
// 渲染新的图表
const { svg } = await mermaid.render(element.id + '-svg', code)
element.innerHTML = svg
element.dataset.rendered = 'true'
// 添加响应式样式
const svgElement = element.querySelector('svg')
if (svgElement) {
svgElement.style.maxWidth = '100%'
svgElement.style.height = 'auto'
svgElement.style.maxHeight = '720px'
}
} catch (error) {
console.error('Mermaid rendering error:', error)
// 优雅降级:显示原始代码
element.innerHTML = `
<pre class="mermaid-error"><code>${code}</code></pre>
<p class="mermaid-error-message">Failed to render Mermaid diagram</p>
`
}
}
}
javascript实现要点:
- 使用
dataset.rendered
标记避免重复渲染 - 异步渲染支持大型图表
- 错误处理:渲染失败时显示原始代码
- 响应式处理:限制图表最大尺寸
主题适配实现#
// 主题检测和配置
function getCurrentTheme() {
const isDark = document.documentElement.classList.contains('dark') ||
document.body.classList.contains('dark') ||
document.documentElement.getAttribute('data-theme') === 'dark'
return isDark ? 'dark' : 'light'
}
function configureMermaid(theme) {
const config = {
startOnLoad: false,
theme: theme === 'dark' ? 'dark' : 'default',
themeVariables: {
primaryColor: theme === 'dark' ? '#3b82f6' : '#2563eb',
primaryTextColor: theme === 'dark' ? '#f8fafc' : '#1e293b',
primaryBorderColor: theme === 'dark' ? '#475569' : '#cbd5e1',
lineColor: theme === 'dark' ? '#64748b' : '#475569',
// ... 更多主题变量
},
fontFamily: 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto',
fontSize: 14
}
mermaid.initialize(config)
}
// 监听主题变化
function watchThemeChanges() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' &&
(mutation.attributeName === 'class' || mutation.attributeName === 'data-theme')) {
// 重置渲染状态并重新渲染
document.querySelectorAll('.mermaid-diagram').forEach(el => {
el.dataset.rendered = 'false'
})
setTimeout(renderMermaidDiagrams, 100)
}
})
})
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class', 'data-theme']
})
}
javascript实现要点:
- 支持多种主题检测方式,兼容不同的主题切换实现
- 自定义主题变量确保图表与网站风格一致
- 使用 MutationObserver 监听 DOM 变化
- 延迟重新渲染避免频繁触发
AST 节点结构理解#
// Markdown AST 中的代码块节点结构
interface CodeNode {
type: 'code'
lang: string | null // 语言标识,如 'mermaid'
meta: string | null // 元信息
value: string // 代码内容
}
// 转换后的 HTML 节点结构
interface HtmlNode {
type: 'html'
value: string // HTML 字符串
}
typescript节点转换过程:
- remark 解析器将 ```mermaid 代码块解析为 CodeNode
- remarkMermaid 插件识别
lang: 'mermaid'
的节点 - 提取
value
中的 Mermaid 代码 - 生成包含容器和脚本的 HTML 字符串
- 创建 HtmlNode 替换原始 CodeNode
简单总结#
通过深入理解 remark 插件系统和 Astro 构建流程,我成功实现了 MDX 文件中 Mermaid 图表的自动渲染。这个方案的核心价值:
技术优势:
- 构建时转换:利用 remark 插件在构建时处理,避免运行时解析开销
- 按需加载:动态加载 Mermaid 库,不影响主包体积
- 主题一致性:自动适配网站主题,保持视觉统一
- 错误容错:优雅降级处理,提升用户体验
开发体验:
- 语法简单:直接在 MDX 中写 ```mermaid 代码块
- 实时预览:开发时可以实时看到图表效果
- 版本控制友好:图表以文本形式存储,便于 diff 和协作
- 维护简单:修改图表只需编辑文本,无需专业工具
架构设计亮点:
- 插件化设计:通过 remark 插件扩展 Markdown 处理能力
- 关注点分离:构建时转换和运行时渲染各司其职
- 响应式适配:支持多种屏幕尺寸和主题模式
- 性能优化:避免重复渲染,支持大型图表
后续改进方向:
- 服务端渲染:探索在构建时预渲染图表,提升首屏性能
- 图表缓存:实现图表渲染结果缓存,避免重复计算
- 交互增强:添加图表缩放、导出等交互功能
- 类型安全:为 Mermaid 代码添加语法检查和类型提示
- 性能监控:添加渲染性能监控,优化大型图表处理
- 插件扩展:支持更多图表类型和自定义样式
这套 Mermaid 渲染方案不仅解决了技术文档中图表展示的问题,更重要的是展示了如何通过理解和扩展构建工具来提升开发体验。remark 插件系统的强大之处在于它让我们可以用编程的方式扩展 Markdown 的能力,这为技术写作和文档工程化提供了无限可能。