外观
一文搞懂 VitePress 自定义容器与 Mermaid 渲染
在搭建技术文档站时,纯文字与代码块往往不足以承载架构图、流程图与状态说明。VitePress 基于 markdown-it 提供了强大的 Markdown 扩展能力:一方面内置了自定义容器(custom containers),另一方面允许我们通过插件链注入 Mermaid 图表渲染。本文从问题背景出发,对比几种落地方案,并给出可直接运行的配置与避坑总结。
问题背景:为什么需要 Markdown 增强
默认的 Markdown 语法在表达「提示、警告、危险操作」等语义化信息时缺乏专门结构,开发者只能用引用块凑合,样式与含义都不清晰;而架构图、时序图如果依赖外部绘图工具导出 PNG,又会带来版本不同步、无法随代码 Review、修改成本高等问题。
VitePress 的增强思路是:
- 自定义容器:用
::: info这类围栏语法,编译期转成带 class 的 HTML,再由主题样式呈现; - Mermaid 渲染:把
```mermaid代码块拦截,交给 Mermaid 在浏览器端解析为 SVG,实现「图表即文本」。
二者结合后,文档既能表达醒目的语义提示,又能内联维护各类图表。
方案对比:Mermaid 的三种接入路径
| 方案 | 渲染时机 | SSR 兼容 | 打包体积 | 适用场景 |
|---|---|---|---|---|
| 构建期预渲染(@mermaid-js/mermaid-cli) | 构建时生成 SVG | 好 | 小 | 图表数量多、追求极致性能 |
| 运行时全局指令渲染 | 浏览器端 | 需处理窗口判断 | 较大 | 图表分散、需动态更新 |
| Vue 组件 + markdown-it 插件(推荐) | 组件挂载后 | 好,客户端水合后渲染 | 按需 | 大多数文档站 |
本文采用第三种:通过自定义 markdown-it 插件把 mermaid 代码块替换成一个 <MermaidChart> 组件占位,再由组件在 onMounted 中调用 Mermaid。这样既避开 SSR 阶段访问 document 的报错,又能利用 Vue 的生命周期保证 DOM 就绪。
下图展示整体渲染链路:
内置自定义容器:零配置即可用
VitePress 默认支持 info、tip、warning、danger、details 五种容器,语法如下:
markdown
::: info 标题可自定义
这是一条普通信息,容器内仍支持 **Markdown** 与代码。
:::
::: tip
善用 tip 容器标注最佳实践。
:::
::: warning
warning 用于强调容易出错的步骤。
:::
::: danger
danger 用于不可逆操作,例如清空数据库。
:::
::: details 点击展开查看完整配置
这里的内容默认折叠,适合放置冗长输出。
:::容器在编译后会生成 .custom-block.info 等结构,可在主题中覆盖样式。若想新增 success 这类语义类型,需要同时扩展 markdown-it 渲染规则与 CSS:
typescript
// .vitepress/config.ts
import { defineConfig } from 'vitepress'
export default defineConfig({
markdown: {
config(md) {
// 让 ::: success 也能被容器规则识别
const defaultRender = md.renderer.rules.fence
// 如需自定义块类型,可借助 markdown-it-container 注册
}
}
})更常见的做法是安装 markdown-it-container 显式注册,下文 Mermaid 方案也会用到同一套插件机制。
实操步骤一:编写 Mermaid Vue 组件
新建 .vitepress/components/MermaidChart.vue,核心要点是:在 onMounted 内动态导入 Mermaid,避免 SSR 环境报错;用自增 ID 防止多图冲突;渲染失败时展示原始代码便于排查。
vue
<script setup lang="ts">
import { onMounted, ref, useId } from 'vue'
const props = defineProps<{
code: string
}>()
const svg = ref('')
const error = ref('')
// 利用 Vue 内置的稳定唯一 id,避免同页多图 id 冲突
const uid = useId()
const chartId = `mermaid-${uid.replace(/[^a-zA-Z0-9]/g, '')}`
onMounted(async () => {
try {
// 动态导入,mermaid 不会进入 SSR 执行路径
const mermaid = (await import('mermaid')).default
mermaid.initialize({
startOnLoad: false,
theme: 'default',
securityLevel: 'loose',
fontFamily: 'inherit'
})
const { svg: rendered } = await mermaid.render(chartId, props.code)
svg.value = rendered
} catch (e) {
error.value = e instanceof Error ? e.message : String(e)
}
})
</script>
<template>
<div class="mermaid-wrapper">
<div v-if=
<!-- 内容间隙推广位:正文之后内联渲染,不遮挡阅读;变现开关关闭时零渲染零占位 -->
<MonetizeSlot position="inline" domain="frontend" />
<!-- 免责声明:文字与 data/site.config.yaml 的 disclaimer 保持完全一致,不得删改 -->
::: warning 免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。
:::