用 Bun WebView 给博客搭了一个 Mermaid 渲染引擎
写博客的人都有一个共同经历:想在文章里画一张架构图,要么打开 Figma 拖半小时,要么截图贴上去丑得不行。
Mermaid 解决了「文本即图表」的问题,你写几行 DSL,它给你渲染成图。但它有个尴尬的地方——运行时渲染需要加载 Mermaid 的 JS 库,而构建时渲染又需要一个浏览器环境。
Sunil Pai 给了一个漂亮的答案。在他的博客never waste a token那篇文章里,他附带了一个工具:用 @tldraw/mermaid 把 Mermaid 代码块转换成 tldraw 形状,然后通过 Playwright 在构建时导出 SVG。结果就是那些漂亮的手绘风格流程图,light/dark 模式都自动生成好。
我看完的第二天,决定自己也搭一套。但又不想装一个 Chromium。
Bun 内置了 WebView API。macOS 上它用系统自带的 WKWebView,Linux 和 Windows 上驱动 Chrome 或 Edge。关键是不需要额外安装浏览器——系统自带的就够了。
第一版很简单:写一个 React 组件,加载 tldraw editor 和 @tldraw/mermaid,在 Harness 脚本里通过 Bun.WebView 创建窗口、加载页面、注入 Mermaid 源码、调用 createMermaidDiagram 渲染、用 getSvgString 导出 SVG。十分钟就跑通了。
但工具和原型之间差了两个「嗯?」。
第一个问题是字体。tldraw 有自己的手绘字体 tldraw_draw,内嵌在 SVG 里当 base64 woff2 占了将近 200KB。 对于博客用图来说,手绘风格不是必须的。直接去掉 @font-face 块,字体 fallback 到系统 sans-serif,中文还能正常显示。
第二个问题是 SVG 太大。每个 foreignObject 里的 div 都带了一个包含了所有 CSS 属性的 style 属性。这些是浏览器默认值,在 SVG foreignObject 里没有任何意义。解析 style 字符串,只保留 font-family、color、text-align 等必要属性,其余全扔掉。
两步下来,313KB 的 SVG 变成了 24KB。
第三个问题是布局。@tldraw/mermaid 在渲染 TD 方向的 flowchart 时,不会真的把节点纵向排列。我试了 graph TD、flowchart TD、甚至手动指定 direction TB,tldraw 的渲染器都忽略了。节点永远从左到右排开,宽度轻易突破 2000px。
最后的选择是:接受 LR 布局,简化节点文字,让 24KB 的 SVG 在博客的 max-width: 640px 列宽里自适应缩放。实际效果够用。
第四个是不起眼但值得说的:SecurityError。想通过 canvas toBlob 把 SVG 转成 WebP,SVG 里的 foreignObject 会污染 canvas,浏览器直接抛异常。换成 WebView screenshot 又发现 macOS 的 WKWebView 不支持 webp 格式。所以最终保留 SVG 输出。
工具做完了,三种输入方式:
bun run mermaid arch.mermaid # 从文件
cat arch.mermaid | bun run mermaid # 从管道
bun run mermaid -- 'flowchart TD\nA --> B' # 从参数
输出一对 SVG,light 和 dark 各一个,直接嵌入博客文章。
回头看看,这个工具的技术栈有点意思:Bun.WebView 做 headless 渲染,bun build 打包前端 bundle,tldraw 做图形引擎,没有 npm 包需要额外安装浏览器。整个流程只用了一个运行时。下图就是这个工具的完整工作流。
Sunil 的原版用 Playwright 加 Vite 加 Chromium。Bun 的版本把这四个简化为一个。
当然也有代价:macOS 上 WebView 会弹一个窗口再关掉,Linux 才能真 headless。而且 tldraw 本身是个全功能画布编辑器,只为渲染 Mermaid 就打包 10MB 的 bundle 确实有点重。但 bundle 可以缓存,只要不升级 tldraw 就只用打一次。
最后想说,不要因为「这个功能太小不值得写工具」就用手工画图。自动化工具的好处不是省那几分钟,是你可以持续产出风格一致的图,每一篇都用同一套规范。
写一次工具,后面所有文章都跟着享受。
相关文章: