ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

像写代码一样画图:文本绘图与文档即代码实战

像写代码一样画图:文本绘图与文档即代码实战

1. 项目定位:为什么“画图”这件事值得用代码重做一遍

先说个扎心的事实:大多数开发团队里的架构图、流程图、时序图,画完第一版之后基本就进了坟墓。需求一变,文档没人更新,图还是三个月前那张老图。而“diagram-design”这个项目的核心思路,就是反过来——不直接“画”图,而是用文本/DSL(领域专用语言)去“描述”图,然后再通过渲染引擎把它变成可视化的图形。说得更直白一点:像写代码一样画图,像review代码一样review图。

这套思路其实不是什么新发明。类似的做法早就在基础设施和开源圈子里流行,比如用PlantUML画UML、用Graphviz画依赖图、用Mermaid画流程图和时序图。但真正值得认真落地为一个独立项目的原因,在于它解决了几件非常实际的问题:

  • 图的可版本化。文本进Git,每次更新有diff,能追溯,能回滚,不再是一张png孤零零躺在网盘里。
  • 图的可协作。多人改一个文件,合并冲突时Git能告诉你哪里改过,而不是大家各自导出图片然后再手工拼。
  • 图的可再生。一个源文件可以生成不同风格的输出,比如白底版、深色版,甚至像官网那样按主题自动换肤。

所以,如果你想搭建一套靠谱的“文档即代码”工作流,或者你正在思考如何让团队里的架构图、流程图真正活起来,那么这篇文章就是围绕“diagram-design”这个项目做的一次完整拆解,从选型、语法、流程到踩坑,一次讲透。

我尽量不写那些“官网文档翻译”式的教程,而是把实际项目里用得到的东西、以及那些文档里不会告诉你的细节,揉碎了讲出来。你会看到完整的代码示例、配置决策过程,以及我在真实项目里撞见过的问题。

2. 选型阶段:Mermaid、PlantUML还是Graphviz,怎么选才不后悔

正式开始写图之前,最绕不开的一步是选渲染方言。很多新手会一上来就纠结“哪个工具最好”,但我建议先明确一个判断逻辑:语法表达的舒适度、渲染输出的可控程度、以及生态接入成本,三者取平衡,而不是单纯比某个功能。

2.1 三类主流方言的核心差异

拿Mermaid、PlantUML和Graphviz做对比,你会发现它们的侧重点完全不同:

工具语法风格擅长场景渲染质量社区活跃度上手难度
Mermaid极简、类Markdown流程图、时序图、状态图、饼图中等,偏清爽非常高低
PlantUML类Java风格UML全系类图、时序图、用例图中高,偏工程风高中
Graphviz声明式属性强依赖图、树形结构、自动布局高,偏学术论文风高较高

主观感受是:Mermaid看起来最接近“自然语言”,写起来几乎不需要心理负担,比如A->>B这种箭头写法非常直觉。PlantUML则更适合UML重度用户,尤其需要严格表达类之间的关系、接口实现、多重继承这类建模语义时,PlantUML的语法更完善。Graphviz则适合节点多、连接关系复杂的场景,它的自动排布算法在整体布局均匀度上是三者中最好的,因为它本质是专业级图布局引擎。

2.2 为什么我在项目主体里以Mermaid为默认参考

这次项目里我选Mermaid做主力,理由其实很朴素:

  • 团队里大多数人不愿意学一套复杂的建模语言,但他们能秒懂Markdown风格的写法。
  • Mermaid对中文支持相对友好,GitHub仓库、各类Markdown编辑器、很多文档平台都原生渲染。站在“让图真正被大家使用”的角度,这个生态优势无价。
  • 它有基础的样式调整能力,配合主题和CSS,足够撑起日常80%的需求。

当然这不代表PlantUML和Graphviz没有用。实际情况是:后续如果做更严谨的领域建模,我会把PlantUML单独作为补充方案;如果涉及复杂网络拓扑、分层依赖关系自动化生成,Graphviz反而更有优势。工具不是越多越好,而是要在合适场景用合适方言。

注意:如果你所在团队已有某种方言的沉淀,比如代码仓库里全是PlantUML文件,那就不建议强行切换。迁移成本永远比“更好的语法”更贵。

2.3 文本绘图核心三步走

不管用哪个方言,整个工作流的逻辑是一样的:

  1. 编写源文件:所有图形描述放在一个目录下,命名清晰,按模块或系统组织。
  2. 渲染出图:通过命行工具或脚本将源文件转为PNG、SVG、PDF等格式。
  3. 集成发布:嵌入到文档站、Wiki、README或CI产物中,作为文档体系的一部分。

这套流程在“diagram-design”里的具体落地,我会在第4节里用一个完整示例展开。

3. 核心语法细节:节点、连线与子图,以及那些文档没讲透的坑

很多人学图方言,只看到“A->B”这种箭头,然后觉得很简单。但真正进阶的部分在:怎么组织节点语义、怎么分块布局、怎么处理复杂关系,以及怎么使用图元属性控制最终渲染结果。单独把语法过一遍没意义,我把常用的核心概念拆出来,结合踩坑经验讲。

3.1 节点的定义与语义命名

Mermaid里定义一个节点非常容易,比如:

flowchart LR A[登录请求] --> B[校验] B --> C{是否合法}

但这里有个数据实践层面的建议:不要直接用“A”“B”“C”这种抽象字母,而是使用语义化ID。比如:

flowchart LR LoginRequest[登录请求] --> AuthCheck[凭证校验] AuthCheck --> Decision{是否合法}

为什么?因为源文件本身就是文档,它不只是一张图的“底稿”,而是要被人读的。如果你把ID写成login_request,那么别人在阅读源文件时,即使不看渲染结果,也能大概猜到流程内容。更重要的是,在Git diff的时候,语义化ID能让你清楚地看出来是哪个环节发生了变更,而不是“A改成了B,B改成了C”这种废话式变更。

3.2 连线类型不是摆设

Mermaid支持多种箭头类型,例如:

  • -->实线箭头:表示数据流/流程方向
  • ---实线无箭头:表示关联
  • -.->虚线箭头:表示异步或间接依赖
  • <-->双向箭头:表示双向交互
  • ==>粗线箭头:表示重点路径或主流程

在真实项目中明确区分连线类型,会让图的表达力提升一个档次。例如:

  • 支付请求走==>强调主链路,风险相对高;
  • 回调通知走-.->标识异步事件;
  • 读取配置表走---表示非关键关联。

这样辅助图和文档的优势就出来了:读图的人不仅看“谁指向谁”,还能看出依赖的强度与类型。

3.3 子图(Subgraph)的布局控制与分组

当图越来越复杂,大量节点扁平堆在一层,布局会乱到你怀疑人生。这时就要引入子图,把语义上靠近的节点归拢在一起:

flowchart TB subgraph 网关层 A[API网关] --> B[鉴权服务] end subgraph 业务层 C[订单服务] --> D[库存服务] end B --> C

这里有几个值得注意的点:

第一,子图会影响连线层次。如果只把B和C放在子图里,但A放在外面,连线跨层时布局引擎处理效果往往不理想。一般来说,让子图内部的连线保持在内层,子图之间只留少量跨层连线,布局会干净得多。

第二,子图的顺序由声明顺序决定。在Mermaid中,子图的渲染顺序大体上跟着定义顺序走,所以如果你想让“网关层”出现在上方,就先定义它。

第三,子图内也可以继续嵌套子图,但层级不要超过3层。超过三层,渲染出来基本就是一团乱麻。如果你发现需要用嵌套子图表达系统复杂度,那大概率不是图的问题,而是架构本身需要拆分文档了。

3.4 用配置项和主题统一视觉风格

“diagram-design”里最容易被人忽略的是主题配置。默认的Mermaid配色其实还算顺眼,但放在实际文档体系里会有强烈的“工具默认感”。好在Mermaid支持初始化配置,可以高度自定义:

%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#d2eaff", "lineColor": "#666", "textColor": "#333"}}}%% flowchart LR A[节点一] --> B[节点二]

这种初始化注释支持写在源文件顶部。主题变量可以控制节点填充色、边框、连线颜色和字体色。建议团队把统一主题变量抽成一个公共配置文件,渲染时通过命令行参数注入,而不是每个源文件都写一遍。这样后续改色,只需要改一处。

另外值得提醒的是:不要过度调样式。图的核心是信息结构清晰,不是视觉炫技。大多数情况下,保持统一配色、让节点内容规整,比在每个小节里换五种颜色更有用。

3.5 关于中文乱码和字体

这是绝大多数中文用户都会碰到的第一个坑。

在浏览器预览或者某些在线编辑器里,中文显示可能没问题;但一旦用命令行工具导出PNG,很容易出现豆腐块或字体缺失。原因在于渲染引擎的字体配置没有正确覆盖中文系统字体。

解决方案通常是在导出命令中增加字体配置参数,或在源文件中指定字体族。以常见的Mermaid CLI为例,puppeteer渲染模式下需要确保系统安装了相应中文字体(如思源黑体、Noto Sans CJK SC),并且在配置里指定fontFamily,否则就会退化为不可用字体。这点我在第5节会继续展开。

4. 完整实操:从零搭建一个可复用的图文档体系

这一节我想以一个模拟的“某跨平台系统”数据架构为例,带你完整走一遍从零搭建图文档体系的过程。这个系统的核心流程包括:客户端上报数据、网关接收、消息队列缓冲、数据处理管道、最终入库。整个过程既有流程结构,又有组件依赖,非常适合用来演示。

4.1 建立目录结构与命名约定

第一步,先为所有图表源文件建立统一目录:

docs/ ├── diagrams/ │ ├── system/ │ │ ├──>flowchart LR subgraph 客户端 Mobile[移动端] --> Reporter[数据采集模块] Web[Web端] --> Reporter end subgraph 接入层 Reporter -->|HTTPS上报| Gateway[API网关] Gateway --> RateLimiter[限流组件] RateLimiter --> Validator[参数校验] end subgraph 数据处理层 Validator --> MQ[(消息队列)] MQ --> Consumer[数据消费服务] Consumer --> Transform[清洗与转换] end subgraph 存储层 Transform --> DB[(主数据库)] Transform --> Warehouse[(分析仓库)] end DB --> Metrics[监控大盘] Warehouse --> Metrics

写这一段时,我有几个操作心德:

第一,不要把环节拆得过细。我当时随手写的时候,一开始把“负载均衡”“DNS解析”“TLS终止”都拆成了独立节点,结果图变得很长很难看。后来果断把网络层那些细节合并为“API网关”的一部分,用连线标签“HTTPS上报”表达要点,结构立刻清爽。图的表达能力不在于“节点多”,而在于“边界清晰”。

第二,子图里节点命名尽量简洁。子图标题可以写完整业务名,但节点文本保持在4到8个字以内,过长时渲染效果会明显变差。

第三,跨子图连线是注意力焦点。图上所有连接不同子图的线,都是架构上的边界交互。像上面示例中的 “Gateway -> RateLimiter”和“Validator -> MQ”,才是最重要的关键链路,值得在正文里单独用文字解释。

4.3 为关键业务流程补充时序图

架构图表达的是全局,但很多需求细节是时序图才讲得清的。同一个系统里,我会为高风险的交互过程单独画时序图。例如一次下单操作的完整时序:

sequenceDiagram participant U as 用户 participant P as 订单页面 participant O as 订单服务 participant S as 库存服务 participant D as 支付网关 U->>P: 点击下单 P->>O: 创建订单请求 O->>O: 校验商品状态 O->>S: 预占库存 alt 库存余量足够 S-->>O: 预占成功 O->>D: 发起支付 D-->>U: 收银台跳转 else 库存不足 S-->>O: 预占失败 O-->>P: 返回库存不足提示 end

时序图的表达逻辑非常接近代码执行顺序,写起来几乎等同于“用文字描述一次函数调用链”。它的好处是能清晰暴露异常分支,避免“只画了happy path”的通病。

我特别推荐把“鉴权”“支付”“退款”这类涉及多方协作的关键链路画成时序图,因为在代码Review的时候,时序图比一堆代码注释更能帮助同事快速理解交互逻辑。

4.4 本地渲染与CI集成

源文件写好了,接下来就是渲染。

本地命令行手动渲染,尤其是调试配色或字体时,很依赖CLI工具,一般操作是:

npx @mermaid-js/mermaid-cli -i>npx @mermaid-js/mermaid-cli -i>render: mkdir -p dist for file in docs/diagrams/**/*.md; do \ npx @mermaid-js/mermaid-cli -i "$$file" -o "dist/$$(basename "$$file" .md).svg" -t base -w 1200; \ done

虽然这个脚本很简单,但它做了一件事:把复杂的命令封装成一行统一入口,避免团队里三个人用三种参数,产出的图风格不一致。

更进一步,可以把这个渲染步骤挂到CI里。在每次合并到主分支时,让拉取请求自动生成最新版图片,加到一个构建产物里。这样文档和代码的更新永远保持同步,而不是“代码改了一周,文档还在旧分支”。

注意:CI环境里需要安装对应字体,否则中文会乱码。常见的做法是在构建镜像里安装fonts-noto-cjk这类字体包,或在runner启动脚本中提前执行字体安装。

4.5 转换成其他图类型:不只是流程图

“diagram-design”这个项目里,我还会把同一套数据模型转成不同视角的图。比如,把系统模块间的强依赖关系用ER形态表达,或者把榜单排名用简单的饼图/柱状图表达。这不只是为了炫技,而是为了在“不同场合有不同表达方式”:

  • 给架构评审人员看的数据流转图,核心是子图边界和跨边界连线;
  • 给研发排查问题时的时序图,强调消息顺序和返回路径;
  • 给管理层汇报用的统计示意图,强调关键数字和简单对比。

同一个底层数据对象,通过不同DSL表达成不同体裁,这种灵活性是手绘工具很难具备的。

5. 高频问题与排查方法

这个部分是我整理的真实项目中的高频问题,每一个都是我或者团队同事实际操作中撞过的墙,按优先级列出来。

5.1 中文乱码问题

现象:在本地预览正常,用CLI导出PNG后中文变成方框或乱掉。

原因:渲染进程(通常是无头浏览器)没有找到可用的中文字体。

排查步骤:

  1. 在终端输入fc-list | grep -i "cjk",看看系统是否有CJK字体。如果没有,安装一个(如fonts-noto-cjk)。
  2. 如果字体已安装还乱码,检查Mermaid配置中的fontFamily是否设置了可用的中文字族。可以在初始化配置里写上:
%%{init: {"theme": "base", "themeVariables": {"fontFamily": "Noto Sans CJK SC"}}}%%
  1. 注意浏览器里的字体回退逻辑和命令行里的回退逻辑不一定一样,所以“本地预览没问题”并不能说明渲染环境没问题。

5.2 布局乱跑,子图不按预期排布

现象:同样一张图,本地渲染和团队同事渲染出来的布局不一样,或者稍微加一个新节点,整张图的形状就变得很怪。

原因:Mermaid的自动布局算法对节点顺序、连线方向、子图嵌套层数都很敏感。某些节点虽然逻辑上可以指向上游,但从布局引擎角度看,它可能会被强制拉平或移动。

排查方法:

  • 优先统一渲染引擎版本。团队里有人用旧版本,有人用新版本,布局一定不一致。把CLI版本号写进构建脚本里,锁死。
  • 如果某条线的方向严重干扰布局,考虑把它从子图内部挪到子图之间,让边界的跨度变大,反而更容易布局。
  • 如果确实需要非常精确的坐标控制(比如像素级对齐),不要企图靠Mermaid塞坐标实现,那说明你进入了Graphviz的领域,或者直接用绘图软件更适合。

5.3 图太复杂,阅读成本急剧上升

现象:节点超过20个,连线超过30条,图完全失去表达力。

判断原则:一张图如果需要在盯着屏幕超过30秒才能理解主干流程,那它就该拆了。

解法:

  • 把大图按领域拆成多个小图,每个图表达一个职责域;
  • 用子图把复杂度包住,让读者先看边界再看边界之间的连线;
  • 对于“当前关键路径”,在正文里用文字或高亮颜色强调,而不是试图让图把所有信息都装下。

我个人经验:架构图“节点数小于15”是可读性边际线,超过这个数,维护成本和理解成本都会陡增。一个普通读者扫一眼只能记住三四个重点,因此最好每张图只承载一个明确的问题视角。

5.4 多人协作时的版本冲突与解耦

现象:两个人同时改同一个图文件,Git冲突无解。

原因:图的源文件虽然是文本,但表达的是全局结构,大家很容易在同一处区域里增删节点。

缓解手段:

  1. 尽量一个文件只承载一个主题,并列关系用多个文件承接,降低多人同时修改同一个文件的可能性。
  2. 冲突产生时,优先沟通而不是直接“用我的版本覆盖”。因为图的语义冲突和人之间的理解冲突高度相关,没有哪个自动合并工具能替你们做架构决定。
  3. 把图的负责人机制写进目录README里,明确“这张图谁负责维护”,减少无主文件的混乱状态。

5.5 输出图片模糊、尺寸不统一

现象:README里嵌入的图,有些清晰有些模糊,大小还参差不齐。

原因:多半是因为输出的位图分辨率不够,或者宽度参数没有统一。

解法:

  • 一律输出SVG,不输出PNG。
  • 如果目标平台强制PNG,那么在导出时把-w参数调大,比如1600或2000,不要依赖800默认值。
  • 在Markdown中建议限制图片展示宽度,而不是缩放原图,例如使用![alt](path.svg){width=800}的方式,既清晰又统一。

6. 常用参数与配置速查表

这是我在实际项目中沉淀的一份配置速查,你可以直接抄作业。因为有些参数不是每次都记得住,我干脆都在这个表里统一管理。

项目配置或命令备注
主题定制%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#fff"}}}%%自定义变量时优先基于base
背景颜色themeVariables: {"background": "#ffffff"}深色模式下另配
边距控制themeVariables: {"nodeMargin": "8"}数值越大节点间距越大
主节点填充色themeVariables: {"primaryColor": "#f0f4ff"}用十六进制色值
文字颜色themeVariables: {"textColor": "#333"}与背景色做好对比度
字体themeVariables: {"fontFamily": "Noto Sans CJK SC"}中文场景必配
输出SVG-o 输出.svg推荐默认
输出PNG-o 输出.png -w 1600目标平台不支持SVG时用
指定主题-t base可用base/neutral/dark/default
锁定布局方向flowchart LR/TB/RL/BT在flowchart后定义
启用Markdown渲染markdownScripts: true在部分CLI版本中支持

补充两个细节。第一个,配置初始化块写在文件首行时,如果同一文件有多个%%{}%%,后面的会覆盖前面的配置,不要写多个混淆。第二个,修改主题颜色的时候,注意primaryBorderColor和lineColor对整体观感影响很大,边框颜色太深会让图显得很脏。建议色值饱和度降低一点,视觉上更耐看。

快速调试表示例,前面出现过的数据流图的深色模式版本:

%%{init: {"theme": "base", "themeVariables": {"background": "#1e1e1e", "primaryColor": "#2d2d2d", "primaryBorderColor": "#555", "lineColor": "#999", "textColor": "#ddd", "fontFamily": "Noto Sans CJK SC"}}}%% flowchart LR A[数据上报] --> B[网关] B --> C[(消息队列)]
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进