极简静态博客的 Mermaid 原生图表系统集成与禅意调优实践
在技术写作与系统设计复盘中,文字与代码往往只能呈现离散的逻辑点,而一张结构清晰的架构图、时序图或数据流向图,能够瞬间拉通读者的全局认知。
然而,在坚持 “纯净优先、零臃肿客户端、秒级静态构建” 的 Zen 架构静态博客中,引入现代图表系统面临着重重工程与美学挑战:
- 体积痛点:Mermaid 完整的渲染引擎压缩后超过 2MB,若无脑全站注入,会让原本轻若鸿毛的纯静态博客瞬间变成重型 SPA;
- 构建痛点:若在 Node.js 服务端通过 Headless Chrome(Puppeteer/JSDOM)进行 SSR 编译,CI 构建时间会暴增数十倍,且生成的静态 SVG 无法跟随前端主题进行实时动态重绘;
- 视觉痛点:Mermaid 默认的刺眼亮蓝、高饱和度配色与纯白背景,与本博客典雅的东方抹茶绿/暗夜墨绿排版体系格格不入。
本文将完整复盘如何在不妥协性能与美学的前提下,通过 “构建期 AST 拦截分流 + 客户端动态 ESM 按需加载 + MutationObserver 毫秒级双主题热切换 + 东方 Zen 调色盘深度定制”,为静态博客带来原生级图表支持。
1. 架构设计:构建期 AST 分流与零成本标记
1.1 代码块分流拦截与语义化封装
在 Markdown 解析管线([core/markdown.js](file:///Users/Carl/Documents/GitHubProjects/Blog/core/markdown.js))中,所有的代码块(```lang)默认由 Shiki 在服务端完成语法分析与双主题 Tokenizer 染色。
为了支持 Mermaid,我们需要在 markdown-it 的 fence 规则层面进行精准拦截:
// 自定义规则:Mermaid 图表拦截与语义化包装
const defaultFence = md.renderer.rules.fence || function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
md.renderer.rules.fence = function (tokens, idx, options, env, self) {
const token = tokens[idx];
const info = token.info ? token.info.trim() : '';
const lang = info ? info.split(/\s+/g)[0] : '';
if (lang === 'mermaid') {
const rawCode = token.content.trim();
const escapedCode = md.utils.escapeHtml(rawCode);
return `<figure class="figure-mermaid">
<div class="mermaid" data-source="${escapedCode}">${escapedCode}</div>
<button class="copy-code-button copy-mermaid-button" data-type="mermaid" title="Copy Mermaid">Copy</button>
</figure>\n`;
}
return defaultFence(tokens, idx, options, env, self);
};
这里有三个至关重要的设计细节:
- 语义化
<figure>容器:将图表包裹在标准的<figure class="figure-mermaid">语义结构中,与普通代码块容器.code-block-wrapper实现 DOM 物理隔离; - 避免
<pre>污染:容器内使用<div>而非<pre>,彻底防止 Cheerio 后置代码块处理逻辑(如$('pre').each)误伤图表容器; data-source原始代码无损挂载:通过属性挂载转义后的原始 Mermaid 语法,为后续双主题无损重绘与一键复制代码提供数据源。
1.2 hasMermaid 元数据联动与零成本加载
在 parseMarkdown 产出文章元数据时,自动检测页面是否包含 Mermaid 图表:
return {
...attributes,
id,
title: attributes.title || id,
hasMath: html.includes('katex'),
hasMermaid: html.includes('class="mermaid"') || html.includes('figure-mermaid'),
content: $.html(),
};
在文章模板 [theme/layouts/content/index.ejs](file:///Users/Carl/Documents/GitHubProjects/Blog/theme/layouts/content/index.ejs) 底部:
<% if (locals.hasMermaid) { %>
<script type="module" src="/js/mermaid-init.js"></script>
<% } %>
成效:全站仅含有图表的文章才会加载驱动脚本,普通随笔、读书笔记保持 0 额外 JS 载荷,守护博客的秒开性能。
2. 运行时设计:ESM 动态按需与 MutationObserver 即时热切换
2.1 现代化公共 CDN 动态导入
我们采用现代化 ES Module 规范,通过动态 import 从全球 CDN 拉取轻量稳定版 Mermaid 核心引擎,无需在 Git 仓库中堆积 vendor 体积:
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
2.2 响应式热切换:MutationObserver 毫秒级重绘
传统的主题切换监听往往依赖 Checkbox 的 change 或 click 事件。但在用户通过系统外观自动切换、多端同步或快捷键切换时,change 事件极易漏判,导致页面必须手动刷新才能换色。
我们引入原生的 MutationObserver 直接监听根节点 <html> 的 class 属性变化:
// 1. MutationObserver 监听 <html> 的 class 变化(即时响应 dark-theme 增删)
const themeObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.attributeName === 'class') {
const isDark = document.documentElement.classList.contains('dark-theme');
if (isDark !== lastThemeIsDark) {
renderAllDiagrams();
}
}
}
});
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
2.3 样式残留清除与代数 ID 隔离
Mermaid 在浏览器中渲染 SVG 时,会在 <head> 中生成局部的 <style id="mermaid-..."> 样式标签。如果不做清理,切换主题后旧的内联样式会覆盖新生成的 SVG,导致颜色出现脏数据。
我们在每次重绘前执行物理清理与代数 ID 递增:
let renderCount = 0;
let renderQueue = Promise.resolve();
async function renderAllDiagrams() {
renderQueue = renderQueue.then(async () => {
const isDark = document.documentElement.classList.contains('dark-theme');
// 1. 彻底清除上一次遗留在 DOM 中的旧样式标签
document.querySelectorAll('style[id^="mermaid-"], style[id^="dmermaid-"]').forEach(el => el.remove());
// 2. 重新初始化主题调色板
mermaid.initialize(getMermaidThemeConfig(isDark));
renderCount++;
const figures = document.querySelectorAll('.figure-mermaid');
for (let i = 0; i < figures.length; i++) {
const figure = figures[i];
const container = figure.querySelector('.mermaid');
if (!container) continue;
const rawCode = container.getAttribute('data-source') || container.textContent.trim();
const uniqueId = `mermaid-chart-v${renderCount}-${i}-${Date.now().toString(36)}`;
try {
const { svg } = await mermaid.render(uniqueId, rawCode);
container.innerHTML = svg;
} catch (err) {
container.innerHTML = `<pre class="mermaid-error">Failed to render:\n${err.message || err}</pre>`;
}
}
});
return renderQueue;
}
3. 视觉与排版系统:东方 Zen 调色盘与细节打磨
3.1 东方 Zen 专属双主题调色板
为了让图表完全融入博客的排版环境,我们为深浅色双模式量身定制了色彩映射:
| 元素分类 | 浅色模式(抹茶灰绿雅韵) | 深色模式(暗夜墨绿幽深) |
|---|---|---|
| 画布背景 | 透出博客底色 --background (#D5F5D9) | 透出博客底色 --background (#1A2421) |
| 节点底色 | 柔和浅绿 #E6F9E9 | 墨绿深灰 #24332F |
| 边框与主色 | 经典古金 #7A6626 | 晶莹翠绿 #8AE293 |
| 文字颜色 | 雅致黛黑 #2C3E50 | 柔和玉白 #E1E8E5 |
| 连线与箭头 | 竹青色 #507554 | 亮绿线 #8AE293 |
3.2 字体排版双轨制与防截断处理
在默认情况下,Mermaid 在计算文本边界盒(getBBox)时如果与页面字体设置冲突,极易导致长文字或中文被节点右边框截断。
我们进行了系统级调校:
- 字体与字号:强制继承系统默认无衬线字体(
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif),字号精确设定为13px(比正文16px小一阶,紧凑精致); - 防裁剪与内边距:配置
padding: 14与nodeSpacing: 45,在 CSS 中注入line-height: 1.4 !important,确保中英混排、标点与代码路径均有充足的呼吸感。
3.3 高反差饼图(Pie Chart)色盘定制
Mermaid 默认的饼图颜色为单一色系渐变,扇区之间极难区分。我们定制了 8 组高辨识度色彩:
- 浅色模式:深茶绿 (
#4A6B53)、琥珀赭黄 (#C28836)、靛湖蓝 (#3D6C85)、茜草红 (#A84E4E)、暮菖蒲紫 (#7E658E)、青瓷青 (#4E8075)、橄榄绿 (#7D8C51)、沉香褐 (#8C5E45); - 深色模式:明亮翡翠绿 (
#8AE293)、琥珀金 (#F0C674)、晴空蓝 (#68B6E8)、珊瑚红 (#F07178)、紫水晶 (#C792EA)、碧湖青 (#4FD6BE)、嫩草绿 (#B5D166)、暖橙 (#FF9E64)。
4. 全图表类型实战效果演示
以下是在本博客环境中原生渲染的 5 类典型 Mermaid 图表。你可以点击页面右上角的主题切换开关,实时体验图表的毫秒级换色动效。
示例 1:架构与数据流向流程图 (Flowchart TD)
示例 2:主题切换实时重绘时序图 (Sequence Diagram)
示例 3:Git 分支演进图 (GitGraph)
示例 4:资产分布高对比饼图 (Pie Chart)
示例 5:图表生命周期状态机图 (State Diagram)
5. 总结与 Zen 哲学思考
这一套方案落地之后,博客的图表支持达到了理想的工程状态:
- 零冗余:不包含图表的页面没有多引入 1 字节的额外 JS,纯净度与构建速度一如既往;
- 全受控:从 Markdown AST 解析、HTML 语义化包裹到客户端深浅色监听,每一行代码都处于绝对掌控之中;
- 排版融合:没有突兀的粗劣边框与花哨动画,图表与正文在字号、字体、边框阶差与配色上浑然一体。
技术的演进并非一味引入复杂的全家桶工具,而是在极简的物理约束下,用最优雅的代码实现恰到好处的表达。希望这篇实践能为同样追求纯粹静态与极致排版体验的开发者带来一些启发。