做技术写作这几年,我越来越觉得“图示”是文档里最容易被低估的部分。代码写得再清晰,架构图一塌糊涂,读者理解成本依然很高。我平时写博客、做技术分享、整理项目文档,最常用的就是 Mermaid,语法简单、渲染快,也不用装一堆绘图软件。但用久了就发现一个问题:Mermaid 默认样式实在太“素”了,配色、字体、间距都不太可控,生成出来的图放在博客里总觉得差了点质感。
于是就有了 diagram-design 这个小项目。简单说,它是一套基于 Mermaid 之上的图表设计工作流:先用 JSON 描述图表的结构和样式,再自动生成 Mermaid 语法,最后通过命令行工具批量渲染成统一风格的 SVG/PNG。核心目标是解决“Mermaid 能画图,但画不出符合个人审美的图”这个痛点。这篇文章我会把这个项目的设计思路、核心实现、渲染流水线到自动化集成全部拆开讲清楚,适合正在折腾 Mermaid、想搭建个人图表设计方案的开发者参考。
1. 项目整体设计与核心思路
1.1 为什么不做渲染引擎,而是包一层配置层
刚开始我其实纠结过要不要自己写一个基于 Canvas/SVG 的渲染引擎,后来仔细想了一下,这个方向太重了。Mermaid 已经解决了最难的部分:语法解析、布局计算、箭头路径生成,这些都是非常复杂的算法问题,自己从头造轮子,光是节点自动布局就够写几个月的。而我的真实需求只是“在保持 Mermaid 便利性的前提下,把样式和排版变得更可控”。
所以我确定了一条核心设计原则:不重写渲染引擎,做 Mermaid 之上的可视化配置层。项目的输入是一份 JSON 配置,输出是一段经过优化的 Mermaid 语法和最终的图片文件。JSON 配置负责描述图表类型、节点内容、分组关系、样式主题、颜色映射,Mermaid 只负责把这段语法变成 SVG。这样分工的好处很明显:语法解析和布局算法的复杂性问题全部交给 Mermaid 社区解决,我只需要专注于样式生成和自动化流程,把有限的精力放在真正影响出图质量的部分。
在设计配置层的接口时,我参考了 D3 的数据驱动思路:数据与样式分离。配置里用data字段描述图表的业务内容,用theme字段描述视觉外观。这样同一个数据,切换不同的主题就能生成不同风格的图,非常适合“一套架构图,白天版和夜间版”这样的场景。实际使用时只需要改一行配置,不用动任何一个数据节点,整个图中所有颜色都会自动切换。
1.2 技术栈选型的取舍过程
技术栈的选择我前后调整过三轮。最初想用纯 JavaScript 写,依赖少、上手快,但项目做到后面发现要维护的类型越来越多,节点配置、主题配置、图表产物都有复杂的结构约束,没有类型系统真的很痛苦。后来换成了 TypeScript,类型定义不仅是给编译器看的,更像是给配置接口做的“隐形式文档”,团队协作或者自己几个月后回来看代码,都能快速搞清楚某个字段到底能填什么值。
构建工具方面,我用的是 tsup,因为它的零配置体验确实好,默认就能同时输出 ESM 和 CJS,对 Node.js 双格式兼容非常友好。打包成 CLI 工具后,用npm link在你的全局环境中挂载一下,就能随时在任意目录执行diagram-design命令,不需要繁琐的路径配置。
渲染流程上我做了两级方案:本地场景用 Mermaid CLI 直接渲染 SVG,速度极快;需要高保真 PNG 或者复杂样式时用 Playwright 驱动浏览器无头渲染。为什么要两套方案而不是一套走到底?因为 Mermaid CLI 的 Puppeteer 依赖有时会因为网络原因安装失败(国内环境下载 Chromium 经常超时),而 Playwright 的浏览器管理更稳定,下载失败时可以自动重试。两个方案互补,能应对不同的使用场景。
2. 核心细节解析与关键技术实现
2.1 可视化配置 DSL 的设计
diagram-design 的入口是一个 JSON 配置文件,我用它替代直接手写 Mermaid 语法。原因是 Mermaid 语法虽然简单,但一旦图表复杂起来——节点多了、关系乱了、案例多了——文本形式的语法在维护上的劣势就暴露了:改了 A 节点忘了改 B 节点的关联,或者想批量把某个模块的颜色统一换掉,手写语法要一个个去改,非常容易漏。
配置结构我设计成了这样:
{ "type": "flowchart", "direction": "LR", "data": { "nodes": ["用户请求", "网关层", "服务发现", "配置中心"], "links": [ ["用户请求", "网关层"], ["网关层", "服务发现"] ] }, "theme": { "primary": "#4F46E5", "secondary": "#10B981", "background": "#FFFFFF", "fontFamily": "Inter, PingFang SC, Microsoft YaHei" } }type字段决定图表类型,data字段是纯粹的图表内容,theme字段是视觉样式。还有第五个字段options,用于控制一些特殊行为,比如流程图是否开启紧凑模式、时序图的消息字体大小等。这个设计的核心思路是把结构(structure)和样式(style)彻底分开,结构字段描述“画什么”,样式字段描述“画成什么样”,互不干扰。
JSON 配置生成之后,真正执行的流程是:配置读取 → 语法生成 → 语法校验 → 渲染。语法校验非常重要,我在这上面踩过很深的坑。Mermaid 的语法错误提示有时候相当隐晦,少一个分号,或者某个特殊字符没有转义,报错信息根本指向不到正确位置。所以我写了一个预校验层,在把语法交给 Mermaid 渲染之前,先自己检查一遍语法的基本结构:方括号、圆括号、分号是否匹配,节点 ID 是否合法,箭头符号是否被正确解析。
2.2 从 JSON 配置到 Mermaid 语法的生成逻辑
语法生成层是项目中最核心的模块。流程图为例,最简单的转换逻辑是:遍历data.links数组,把每个链接转成A --> B的形式。但真实场景比这个复杂得多,节点可能需要分组、需要添加自定义样式、需要设置不同的形状。
我的生成器支持了三种节点类型的映射:默认矩形节点(A["节点文本"])、圆角节点(A("节点文本"))和圆形节点(A(("节点文本")))。这个设计其实来自于我总结的实际需求:主流程节点用矩形,子流程入口用圆角,关键判断节点用圆形,视觉上就能区分不同语义,不用额外加注释。
样式的注入是另一层逻辑。Mermaid 的classDef机制允许你给一类节点定义一个样式类,然后在节点上引用。我的生成逻辑会扫描data.nodes中的节点分组信息,假设有两个分组:核心模块(橙色系)和基础设施(蓝色系),那么就会生成类似这样的代码:
classDef coreModule fill:#F59E0B,stroke:#B45309,color:#FFFFFF; classDef infraModule fill:#3B82F6,stroke:#1D4ED8,color:#FFFFFF; node1["用户请求"]:::coreModule;这里有个细节值得说一下:Mermaid 的 classDef 中填充色(fill)、边框色(stroke)、文字颜色(color)三个属性必须同时出现才能确保样式不被覆盖。我在实际测试中发现,Mermaid 的默认主题在某些情况下会覆盖掉只设置了部分属性的 classDef,特别是文字颜色,不显式指定的话,在深色背景下会变得几乎不可读。所以生成器在输出样式代码时,一定会强制补全这三维属性。
2.3 数据处理与高级语法特性
项目还实现了几个“手写 Mermaid 时非常痛苦”的高级特性。第一个是子图(subgraph)的自动归属。手写子图语法时,子图名不能有中文,但显示名可以用中文,这个靠subgraph sg1["核心服务"]这样的语法解决。我封装成配置字段后是这样的:
{ "subgraphs": { "sg1": { "label": "核心服务", "nodes": ["服务发现", "配置中心"] } } }生成器会先输出子图的声明,再把属于该子图的节点全部放进去,最后输出子图中的内部连线。这个逻辑听起来简单,但实现时要注意子图中的节点 ID 不能和其他子图重复,否则渲染时会莫名奇妙地合并子图,但不报任何错误。
第二个是自定义链接样式。流程图里经常需要强调某些关键链路,比如异常处理路径用红色虚线,正常路径用蓝色实线。Mermaid 的语法是A -.-> B表示虚线,A ==> B表示粗线。我的配置里可以直接指定某个链路的样式级别(normal、bold、dotted、thick),生成器根据级别自动映射到不同的 Mermaid 箭头符号。这个特性在我画灾难恢复流程图、降级链路图时特别有用,关键路径一眼就能看出来。
第三个是 HTML 标签的转义处理。节点文本里如果包含<、>、&这样的特殊字符,直接拼进 Mermaid 语法里会导致渲染异常。生成器会对这些字符做 HTML 实体转义,这算是从实际问题中逼出来的功能。第一次用 diagram-design 画一个包含“用户输入 < 1000ms 时正常响应”的节点时,整个图渲染失败,定位了半天才发现是小于号的问题。
3. 渲染流水线与图形输出的完整实现
这段是整个项目里“技术含量最密集”的部分——配置和语法生成只是做好菜,怎么把菜端上桌、摆盘好看,完全是另一套功夫。
3.1 渲染主流程解析
渲染模块的工作流程是:输入 Mermaid 语法文本 → 选择渲染引擎 → 输出目标格式文件 → 按配置做后处理。整个流程用流程图来理解就是:
JSON 配置 --> Mermaid 语法字符串 --> Mermaid CLI/Playwright 渲染 --> SVG 文件 --> Sharp 后处理 --> PNG 文件为什么中间产物一定要是 SVG,而不是直接渲染成 PNG?因为 SVG 是矢量格式,可以无损缩放到任意尺寸,方便后期嵌入到网页或文档里。而且 SVG 的结构可编程控制,我想在后续版本里做“自动给节点加编号”、“自动加背景网格”这些功能时,直接操作 SVG 的 DOM 结构就能实现。PNG 则适合放在没有矢量渲染能力的场景,比如微信聊天记录、某些内部 Wiki 系统。
3.2 Mermaid CLI 参数选择的实战经验
Mermaid CLI 的命令行参数,我踩过的坑比想象中多。首先是-b参数(背景色),默认是白色,但我在浅色模式下设置透明背景时发现一个问题:SVG 文件确实变成透明了,但打开 PNG 时还是会看到白色底。原因是 SVG 的background-color属性被显式设置为white,透明背景只对 SVG 自身有效。解决方案是渲染完成后用 Sharp 库把白色像素替换成透明,这一步在后处理模块里处理。
其次是-s(缩放倍数)。Mermaid CLI 的默认缩放是 1,但如果你要输出高清图片,建议设置为 2 或者 3。设置缩放倍数后生成的 SVG 尺寸会按倍数放大,但内部的字体、线条宽度也会等比放大。实际操作时我一般用-s 2,既能保证清晰度,又不会让文件体积膨胀得太离谱。如果追求极致清晰,可以输出 SVG 后自己用工具转高清 PNG,那个效果比单纯加大-s好得多。
还有一个值得注意的参数是-w(最大宽度)。这个参数的作用是控制输出图片的最大宽度,当图表比较宽的时候,Mermaid 会压缩整体宽度。但如果你已经用 JSON 配置控制了图表的节点数量和排版方向,一般不需要设置这个参数,让它按布局算法自然输出就行。
我用得最多的命令组合是这样的:
mmdc -i input.mmd -o output.svg -b transparent -s 2 -f -p puppeteer-config.json-f参数是启用“流程图安全模式”,它会禁用流程图中的某些潜在危险功能,比如 HTML 标签解析,但这会让某些自定义样式失效,所以我在项目里默认不开启。-p参数指定 Puppeteer 的配置文件,里面可以设置浏览器路径和启动参数。
3.3 高保真 PNG 渲染的方案:Playwright 无头浏览器
当 Mermaid CLI 解决不了问题时,我引入了 Playwright 作为第二种渲染方案。核心思路是用 Playwright 打开一个本地 HTML 页面,页面里加载 Mermaid 的 JavaScript 库,再把 Mermaid 语法传给mermaid.render()方法,渲染完成后通过document.querySelector('svg')拿到 SVG 字符串,最后用 Sharp 转成 PNG。
具体实现分四个步骤:第一步,启动无头浏览器实例;第二步,通过page.setContent()加载嵌入了 Mermaid 库的 HTML 页面;第三步,调用page.evaluate()在浏览器环境中执行渲染脚本,获取 SVG 代码;第四步,把 SVG 代码写入临时文件,再用 Sharp 处理成目标格式。
import { chromium } from 'playwright'; const renderSVGWithPlaywright = async (mermaidCode, theme = 'default') => { const browser = await chromium.launch(); const page = await browser.newPage(); await page.setContent(` <html> <head><script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script></head> <body><pre id="diagram">${mermaidCode}</pre></body> </html> `); await page.evaluate(async () => { await mermaid.initialize({ startOnLoad: false, theme: '${theme}' }); }); const result = await page.evaluate(async () => { const { svg } = await mermaid.render('diagram-svg', document.getElementById('diagram').textContent); return svg; }); await browser.close(); return result; };这个方案的优点是渲染质量和浏览器保持一致——Mermaid CLI 本质上也是用 Puppeteer 驱动浏览器渲染,但 Playwright 的多浏览器支持更好。缺点是需要额外安装 Playwright 依赖,配置较重。我对接 Dark 主题时用的是 Mermaid 内置的theme参数,比如theme: 'dark',但自定义主题就要通过themeVariables传入,这个后面讲样式时细说。
4. 样式美化、主题系统与多场景适配
有了能干活的渲染引擎,接下来就是“好不好看”的问题了。这一节聊我是怎么把默认样式处理成有设计感的图。
4.1 颜色系统的统一管理与生成逻辑
写样式代码容易陷入一个误区:直接写死颜色值。这样短期看很方便,但改主题时就要在几十个 classDef 里反复替换,效率极低。diagram-design 在项目里定义了一套颜色 token 系统——类似设计系统里的 Design Token。我们把颜色抽象成几个语义化变量:primary主色、secondary辅助色、accent强调色、bgDefault默认背景色、textDefault默认文字色。
但这里有一个很实际的问题:Mermaid 的 classDef 里需要的是具体颜色值,它不认识primary这种变量。所以生成器要做一次计算转换。我用了一个轻量级颜色处理库来处理亮度、对比度计算,比如根据主色自动计算 hover 态的加深色:
import Color from 'color'; const getDerivedColors = (primary) => { const base = Color(primary); return { primaryLight: base.lighten(0.2).hex(), primaryDark: base.darken(0.2).hex(), primaryAlpha: base.alpha(0.15).hex(), }; };lighten()方法是通过 HSL 空间的亮度计算生成的,比手动调 HEX 字符串靠谱得多。这样定义的好处是,你只需要给一个主色,其他相关的浅色、深色、半透明色都由程序生成,最大的优势是“一套代码,任何颜色都能用”,完全不需要手动配十几二十个氛围色。
4.2 字体渲染的坑与新方案
中文文档的项目最大的痛点是:Mermaid 默认字体是 Trebuchet MS、Verdana 这种西文字体,一旦节点里有中文,就会 fallback 到系统默认中文字体,在 SVG/PNG 渲染时经常出现中英文混排错位、文字被截断的问题。
解决字体问题的关键是配置fontFamily,而且要配置好 fallback 顺序。我最终用的是:
--font-family: "Inter", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;这个顺序的意思是:英文首先用 Inter(干净的几何风格字体),中文依次回退到 PingFang SC(macOS 系统字体)、Hiragino Sans GB(macOS 日语字体包含的中文)、微软雅黑(Windows 系统字体),最后兜底到系统默认 sans-serif。
另外一个容易忽略的点是:Mermaid CLI 在服务器端渲染时,服务器上未必安装了 PC 上那些字体。之前我在服务器跑渲染,出来的 PNG 中文全部变成“豆腐块”,排查半天才发现服务器没有中文字体,后来在 Docker 镜像里加装fonts-noto-cjk才解决。这个问题如果你在本地开发可能完全感知不到,但跑 CI/CD 流水线时非常致命。所以要么保证渲染环境有目标字体,要么先在 SVG 阶段把所有文字转为矢量路径。后者我用过一段时间的方案是每张图先生成 SVG 再调用字体转轮廓工具处理,效果最稳定,但速度慢一些。
后续我还发现了第二套可用方案:用 WeasyPrint 代替浏览器渲染。WeasyPrint 是一个 HTML/CSS 渲染引擎,对字体渲染的支持比 Chromium 好很多,在 Linux 环境下尤其稳,中文渲染效果清晰锐利。它没有浏览器那么重的依赖,只需要安装 Python 包和几个系统字体库就能用。我现在项目里对质量要求高的图,首选 WeasyPrint 方案。
4.3 暗黑模式与主题适配
dark mode 是一张图的“第二张脸”。Mermaid 默认支持的主题里有neutral、dark、forest,但实际效果比较粗糙,尤其是节点里的文字在暗色背景下容易和边框混淆,而且整个图的对比度偏低。
我的方案是先定义一套全局语义色板,然后按主题模式切换整套色板。比如:
{ "light": { "bg": "#FFFFFF", "text": "#1F2937", "nodeFill": "#F3F4F6", "line": "#6B7280" }, "dark": { "bg": "#111827", "text": "#F9FAFB", "nodeFill": "#1F2937", "line": "#9CA3AF" } }生成时根据theme.mode字段选择对应色板,动态生成 Mermaid 的themeVariables。其中比较关键的一个变量是lineColor,决定图表的连线颜色;另一个是nodeBorder,决定节点边框,暗黑模式下要降低亮度,让节点的轮廓在深色背景上还保持明显。
在做一个技术分享的暗黑版系统架构图时,我把主色调调成紫蓝色(#818CF8),配合深灰蓝背景,生成的图整体质感比 Mermaid 默认的 “dark” 主题好非常多。看图的人都不用我解释,就能感受到两版图的视觉高度差异。
5. 从流程图到复杂图表的全场景实践
这块是项目最具“纵深感”的部分——流程图只是最基础的一类图。真实文档里要画的图远不止这些,类图、时序图、饼图、甘特图,各有各的语法和样式控制方式。
5.1 类图的定制实现
类图是面向对象设计的常用表达方式。Mermaid 的类图语法中用class关键字声明类,+表示 public,-表示 private。但直接写语法中的一个痛点是,类名、属性名、方法名一旦多了,代码会非常难维护。diagram-design 里我把类图也抽象成 JSON 配置:
{ "type": "class", "classes": [ { "name": "UserService", "members": [ { "name": "findById", "type": "User", "visibility": "+", "static": true } ] } ] }生成器根据配置决定属性和方法的顺序,输出标准的 Mermaid 类图语法。样式方面,类图支持对背景色、边框色的自定义,我就把服务类的底色设成暖色系(表示业务逻辑),领域模型类设成冷色系(表示数据结构),读者一眼能看清分层。
5.2 时序图的脚本化生成
时序图在 Mermaid 里是最容易写乱的图。最麻烦的是参与者顺序和消息类型的管理。参与者每增加一个,后面所有消息的上下顺序都要跟着调整,非常容易出错。我封装成了配置形式后,只要定义参与者列表和消息列表,生成器会自动维护顺序和参与者声明:
{ "type": "sequence", "participants": ["客户端", "网关", "订单服务"], "messages": [ ["客户端", "网关", "创建订单", "solid"], ["网关", "订单服务", "生成订单号", "dotted"] ] }生成器会按消息顺序动态确定哪些参与者需要提前声明,避免 “actor 未定义” 的渲染报错。时序图的样式定制点主要是消息字体大小和激活条颜色,激活条是 Mermaid 时序图中表示方法调用占用的色条,颜色选得合适能显著提升图的层级感。
5.3 饼图与甘特图
项目里还实现了饼图和甘特图的支持。甘特图其实是个硬件挑战:项目里有个内部周报需求,需要每周生成一张“本周任务进度甘特图”,任务有十几项,每项的起止时间每周都在变。手动调 Mermaid 甘特图语法最崩溃的是日期计算,某个任务要延期三天,就得手动改dateFormat和-或+的偏移量,改错一次图就全乱了。
我封装了数据接口之后,只要在配置里按 ISO 格式给出任务的startDate和endDate字段,程序会自动计算工期并转成 Mermaid 甘特图的内置语法。如果某个任务延期,只改endDate一个字段,重新生成就完事了。这个功能极大解放了手动绘图的工作量,也是后来项目被团队里其他人“借用”最多的一个能力。
6. 自动化流水线与 CI/CD 集成
图做出来了,但真正提升效率的是把整套流程自动化。我期望的效果是:以后写文档时只需要维护 JSON 配置,提交代码后 GitHub Actions 自动跑渲染,把生成的图片回传到仓库。这样人只需要关注内容和结构,不可能再去纠结图片的样式细节。
6.1 本地脚本化的便捷一键生成
最先实现的是本地 CLI。项目的scripts/generate.mjs是打包后的入口,只要传入配置文件路径,就自动执行“解析 → 生成 → 渲染 → 输出”流程。核心脚本逻辑:
#!/usr/bin/env node import { generateDiagram } from '../src/index.js'; const configPath = process.argv[2]; if (!configPath) { console.error('用法: diagram-design <config.json>'); process.exit(1); } generateDiagram(configPath);这个脚本里我特意加了一个目录检查逻辑:目标输出目录不存在时,自动创建并提示。这个细节看上去微不足道,但实际用起来很关键。因为配置文件和输出目录如果不在同一个目录,遇到“output/images 不存在”的情况,之前每次都要手动mkdir -p,很烦。
6.2 GitHub Actions 自动渲染
CI 流水线的价值体现在:团队协作时,任何成员提交新的 JSON 配置,流水线自动渲染成最新图片,确保文档的配图永远和代码同步。我的 Actions 工作流用了一个简单可靠的结构:
name: generate-diagrams on: push: paths: - 'diagrams/**' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' - name: Install dependencies run: npm ci - name: Generate diagrams run: node scripts/generate-all.mjs - name: Commit changes run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add docs/img git commit -m "docs: regenerate diagrams" git push这里有一个我在试跑时反复踩的坑:如果渲染环境的PUPPETEER_SKIP_DOWNLOAD环境变量被设置,Puppeteer 就不会下载 Chromium 内核,导致渲染直接失败。在 GitHub Actions 上,有些基础镜像默认包含这个变量,所以要显式地在工作流中删掉或覆盖它:
env: PUPPETEER_SKIP_DOWNLOAD: 'false'还有一个细节:由于自动 commit 会触发新的 workflow 运行,需要在提交信息上加上[skip ci]或者配置 Actions 不在github-actions[bot]提交时触发匹配的路径。我两种都加了,避免无限循环。
6.3 性能优化与缓存策略
渲染的瓶颈通常出现在批量生成多张图的时候。项目里generate-all.mjs会遍历diagrams/目录下所有 JSON 配置文件,挨个渲染。最直接的问题是:每张图都要启动一次浏览器,链路开销很大。我做的优化是让 Playwright 的浏览器实例在整个批量任务中只启动一次,渲染完所有图再关闭,成本从“每张图启动一次”降为“整个流程只启动一次”。大批量出图的场景下,这个优化能缩短接近一倍的耗时。
7. 常见问题与实战排查
实战过程中积攒了一批“Mermaid/命令行工具会隐瞒你”的问题,这一块专门做个梳理,方便大家遇到类似报错时有地方可查。
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
渲染时报Syntax error in text | 节点文本含特殊字符未转义 | 生成器强制对</>/&做 HTML 实体转义 |
| 图片中文全部变“豆腐块” | 渲染环境缺中文字体 | 安装fonts-noto-cjk,或 SVG 阶段转轮廓 |
| 背景设了透明仍出现白底 | SVG 自带background-color=white | 用 Sharp 做白色像素替换为透明 |
DNS 解析失败导致mmdc装不上 | Puppeteer 下载 Chromium 超时 | 设置镜像源或改用 Playwright 管理浏览器 |
| 自动提交触发无限 Actions | commit 行为又匹配了同一触发路径 | 提交信息加[skip ci] |
| 某条链路样式没生效 | classDef 未定义到具体节点 | 检查引用部分是否漏掉了:::语法 |
| GitHub Actions 里渲染直接退出 | PUPPETEER_SKIP_DOWNLOAD 误设 | 工作流中显式设回false |
7.1 语法生成层最常见的坑
我遇到频次最高的一个问题是:节点 ID 含有中文。Mermaid 对节点 ID 的规则是“允许中文 ID”,但后续引用时容易出现问题,尤其是在子图内部引入外部节点时。后来我强制要求“节点 ID 必须是英文/数字/下划线”,显示名称用["中文文本"]来注入。这个习惯坚持下来之后,基本再也没遇到过中文 ID 导致的诡异渲染失败。
另一个高频坑是 Mermaid 新老版本的语法差异。Mermaid 10 以后,有一些老版本里的写法被标记弃用了,比如graph TD在新版本中只是兼容保留,推荐使用更明确的写法。如果你的环境装的是 v9,有些配置在 v10 里渲染结果可能完全不同。项目里我会在配置文件里加一个mermaidVersion字段,锁定渲染用的 Mermaid 版本,避免升级带来的不确定性。
7.2 渲染引擎选择的经验总结
到目前这个阶段,我的经验判断是:图表简单、追求速度时,优先用 Mermaid CLI;图表复杂、需要自定义样式精细调整时,优先用 Playwright 控制浏览器渲染;服务器环境或对中文排版有极致要求时,用 WeasyPrint。三者之间不是替代关系,而是互相补充。项目里我把渲染引擎抽象成了一个可配置项,命令行加--engine cli/playwright/weasyprint就能切换,十分灵活。
8. 后续扩展与最终心得
diagram-design 这个项目的核心价值不在于它重新发明了图表渲染技术,而在于它把“画图”这个行为从“零散的手工操作”变成了“统一的工程流程”。通过一个配置层,把图表的内容、样式、输出格式、自动化流程全部标准化。现在写文档遇到需要插图的需求,我不再面临开启画图软件、手动对齐节点、调整样式的漫长流程,而是通过修改 JSON 和重新运行命令交付一套风格统一的图。
这个项目后续的扩展方向,我在实践中也想到不少。第一是引入预制模板库,把不同的配色方案、排版风格封装成可直接引用的 npm 包,使用者在配置里直接用"template": "corp-tech"就能应用一整套风格体系。第二是增加“导出 React/Vue 组件”的支持,把渲染结果封装成前端组件,配合mermaidnpm 库直接在前端项目里动态渲染。第三是增加“语法检查”的在线服务,让配置在提交前就能在浏览器里实时预览效果,而不是等 CI 跑了半天才发现语法有问题,降低试错的成本。
最后分享一个实际使用心得。这个项目开发的初衷是解决“我自己画图丑”的问题,但开发完成后意外收获是,它把我从前种种对图表设计的模糊偏好转变成了明确的工程规范。比如:什么样的图用圆角节点,什么时候需要给节点分组,重点链路如何突出表示。现在团队里其他人画图,也会拿我的 JSON 配置当模板改一改就用了。好的工具不一定需要找到一个足够复杂的值得去解决的技术问题,能把一件每天都在做的小事变得顺手又省心,就已经很有价值了。