1. 为什么我放弃了拖拽画图改用代码写图表你有没有过这种经历熬夜赶完一套系统设计把架构图画得漂漂亮亮结果第二天需求一改参数对不上、服务拆分变了那张图就成了废纸。团队里没人愿意更新它最后它变成墙上的一张装饰品新同事照着上面过时的依赖关系去排查问题白白浪费一下午。我就是在被这么坑过两三次之后开始认真审视画图这件事。最初用Visio后来换draw.io再后来用ProcessOn工具换了一大堆问题却始终没解决——图是给人看的但人不想维护它。直到我把图表从画变成写这个问题才算真正被治住。所谓写图表业界叫法很多Diagram as Code、代码化图表、图表即代码。核心思路很简单用一段结构化的文本描述节点和连线再交给工具去渲染成图片。这不是什么新概念Graphviz在1991年就干了这件事PlantUML也已经活跃了十几年。但真正让这个思路普及开来的还是Mermaid这类语法足够简单的工具把门槛降到了任何一个会写Markdown的人都能上手的地步。用代码写图表带来的变化是本质性的图不再是一个孤立的图片文件而是一段可以进Git、可以做diff、可以被CI/CD自动处理的文本。改代码的时候顺手把依赖图里的节点更新一下提交PR时评审人看着diff就能发现你改了支付服务的调用关系但是图里没同步。这种约束力是任何拖拽工具都给不了的。简单总结代码化图表最大的价值有三条可版本化图的每一次修改都有历史记录谁改的、改了什么、为什么改全部有迹可循。可评审图表源码参与代码评审架构变更不再是评审会上临时画个图解释一下而是变成文档的一部分被持续审视。可自动化源码可以被脚本批量更新可以在CI流程里自动渲染还能直接嵌入到Markdown文档、Wiki、API文档里。当然这并不是说所有场景都该用代码化图表。后面我会专门说清楚它的边界在哪里。2. 五款主流图表工具的真实选型对比聊选型之前先说清楚一件事市面上工具很多的本质是没有银弹。每一款工具的定位完全不同搞清楚自己要画什么比纠结哪款工具最流行更重要。先把我实际用过的五款工具放在一起对比工具语法类型擅长的图核心痛点适合人群Mermaid类Markdown声明式流程图、时序图、状态图、甘特图、架构分层图复杂布局控制力偏弱节点位置不可精确指定绝大多数研发场景PlantUML类代码描述式UML全系类图、用例图、部署图、时序图需要Java环境UML之外的自由绘图能力一般需要对UML规范有严格要求的团队GraphvizDOT语言结构化关系图、依赖关系、树状结构语法硬核布局算法有时过于智能数据流、依赖分析场景D2简洁声明式架构图、流程图、层叠图生态还在成长期集成方案少想尝鲜且对现代语法有好感的团队Excalidraw手绘风格白板、头脑风暴图不是代码化工具但也值得知道快速沟通、临时讨论这里只挑重点说避免写成工具清单。关于Mermaid我的建议是无脑优先选它。理由不是它的功能最全而是生态最成熟。GitHub原生支持仓库里的Markdown渲染MermaidGitLab也支持VS Code装个插件就能实时预览Notion、Obsidian、Hexo、VuePress全都有集成方案。这意味着你写出来的图表源码可以几乎零成本地流动在团队日常使用的每一个工具里。语法上虽然需要记一些关键字但掌握常见几种图流程图、时序图、状态图基本半小时就够。PlantUML是UML场景下绕不开的存在。如果团队在画类图、用例图时对UML语法规范有硬性要求比如类之间的泛化、实现、依赖、聚合关系必须严格符合UML语义那PlantUML比Mermaid要严谨得多。代价是它基于Java运行本机环境配置对新人略麻烦。Mermaid虽然也支持classDiagram但支持深度和规范程度确实不如PlantUML。Graphviz值得专门提一句因为它解决的是Mermaid搞不定的问题。Mermaid这类声明式工具会根据语法自动排布节点和连线。大多数场景下这个自动是优点但当你需要表达一个包含几十个节点、关系复杂到像蜘蛛网一样的依赖图时Mermaid的自动布局会乱到没法看。Graphviz的DOT语言则允许你通过设置rank、group等属性精细控制节点的排列顺序和层级关系最终产出的图往往更清爽。再补两个选型时容易忽略的判断标准升级频率Mermaid过去两年从v9升级到v10再到v11语法有破坏性变更。选型时要想清楚你的图表源码是不是长期资产如果是就要固定版本或者从一开始就规避易变语法。离线可用性有些在线工具确实方便但代码库里的图表源码不应该依赖外部服务才能渲染。这也是为什么我始终建议用支持本地渲染的工具链。3. 从零开始用Mermaid写一套完整的电商系统架构图光说概念容易飘我拿一个实际项目举例完整走一遍从一个想法到一张能进文档的架构图的过程。这个例子是我最近在做的电商中台系统的部分架构图技术栈很常规前端有Web和App后端按领域拆了商品、订单、用户、支付四个服务数据层用MySQL存主业务数据、Redis做缓存、Elasticsearch做商品检索。3.1 先想清楚这张图给谁看、表达什么动笔写代码之前最重要的一件事是搞清楚这张图的阅读对象。同一个系统给架构评审委员会看的、给新人做培训用的、给运维排查故障用的画出来的图完全是三张图。我这张图的定位是系统全貌速览阅读对象是刚加入团队的后端开发。所以我不需要把每个服务内部的类、方法都画出来只需要把流量从哪进来、经过哪些层、依赖哪些存储讲清楚。目标定了后面的所有设计决策都围绕这个目标展开。3.2 完整示例从用户端到数据层的分层架构以下是我实际提交到项目docs目录里的Mermaid源码为匹配更通用的渲染环境我做了一些语法标准化处理flowchart TD subgraph client[客户端层] web[Web端] app[App端] end subgraph gateway[接入层] nginx[Nginx] api[API网关] end subgraph service[应用服务层] product[商品服务] order[订单服务] user[用户服务] pay[支付服务] end subgraph storage[数据层] mysql[(MySQLbr/主从集群)] redis[(Redisbr/缓存集群)] es[(Elasticsearchbr/检索集群)] end web -- nginx app -- nginx nginx -- api api -- product api -- order api -- user api -- pay product -- mysql product -- redis product -- es order -- mysql order -- redis user -- mysql user -- redis pay -- mysql这段代码渲染出来的效果是五层结构最上层两个客户端节点往下经过Nginx和API网关再到四个业务服务最后落到三个存储组件。整体阅读方向从上到下符合大多数人对系统架构的直觉。3.3 细节打磨子图分组、关系线语义、样式高亮上面的例子只是第一版能看但不够好。我实际提交的版本还做了三处细节优化。第一处是子图命名。subgraph后面跟着的client、gateway这些名字是内部标识不会直接显示在渲染结果里。真正显示的是方括号里的客户端层、接入层这些名字。很多新手在这里会把内部标识写得很有含义比如subgraph client_layer这样并不会让图更清晰反而增加输入量。我建议内部标识用简短英文单词显示名用中文。这里有一个Mermaid语法的特殊点子图声明时用subgraph id[显示名]这种带引号的标准写法在跨平台渲染时兼容性更好。第二处是连线语义。Mermaid支持实线和虚线两种基本连接我在这张图里全部用了实线每个箭头都代表一次同步HTTP调用。如果你的系统里存在异步消息依赖比如订单服务发MQ给商品服务做库存扣减那这条关系就应该用虚线文字标注否则看图的人会误以为是一次同步调用排查问题时就会走弯路。给连线加说明文字的语法是order -. 发送MQ .- product虽然渲染效果略有不同但语义表达能力强很多。第三处是重点链路高亮。我手上有另一张图是专门画核心下单链路的只保留了用户服务、订单服务、商品服务、支付服务这几个节点但给关键路径上了颜色。Mermaid里设置节点颜色的标准做法是通过classDef和class关键字组合这里要注意不同渲染器对classDef语法的兼容性差异后面我会单独讲这个坑。3.4 布局方向的选择逻辑Mermaid里flowchart后面跟上TD或LR指定图的排布方向TD是自上而下LR是自左向右。这个看起来只是个人偏好实际影响很大。架构分层图我建议优先用TD。因为大多数人对用户在上、数据在下有天然直觉流量从上往下流阅读阻力最小。反过来时序图、状态机这类注重步骤推进的图用LR或者TD看具体内容时序图更适合LR因为时间的左右延伸更符合阅读习惯。这个决策不需要什么高深理论核心逻辑就一条看图的人能不能第一眼就找到入口和出口。如果一张图画完看的人要花五秒才能找到第一个节点在哪那就是失败的布局。4. 踩坑实录换行失效、中文字体、节点ID冲突我全遇到过工具用多了最大的收获不是会用而是知道它会怎么坑你。这一节写的都是我在真实项目里踩过的坑每个都附上排查思路而不是直接给你答案。4.1 中文字体在不同渲染器下的表现差异我最早的架构图里很多处用了中文标签在本地VS Code预览一切正常推到GitHub上显示也正常但同事用GitLab看同一个仓库渲染出来中文字体发虚严重的地方直接变成方块。排查链路是这样的先在本地用不同渲染器逐一测试发现同一个.mmd文件用Mermaid CLI渲染和用GitLab内置渲染器渲染出来的字体完全不同。再进一步查发现GitLab内置的Mermaid渲染在Docker镜像里没有安装完整的中文字体包回退到了系统默认字体。解决方案是在GitLab服务器的Docker镜像里装font-noto-cjk包或者更省事的办法图表里尽量用中英混排纯中文只出现在子图标题和节点显示名里。这个坑的核心启示是Mermaid语法是标准化的但渲染器是各个平台自己实现的底层的字体、布局引擎甚至版本都可能不一样。在本地预览正常不代表线上没问题重要文档最好在目标平台做一次渲染验证。4.2 一个编译错误引发的ID排查链路有一次我在一个比较复杂的依赖图里加节点渲染时Mermaid直接报错提示信息很笼统Syntax error in text。一开始以为是标点符号问题检查了一遍没发现异常。后来把报错段落删掉一部分再试缩小范围之后发现是两个节点ID重名了。很多人会忽略ID的唯一性要求觉得只要显示文本不同就行。实际上Mermaid的节点ID是渲染器的内部索引ID相同就算显示名不同也会导致连线指向错误。更隐蔽的情况是ID里的空格和下划线比如node A和node_A在部分解析器里会被当作同一个ID处理。我现在的习惯是节点ID一律用小写字母加下划线严禁空格和连字符。4.3 同一份源码GitHub和GitLab渲染结果不一样这个坑让我一度怀疑是自己写错了代码后来才发现是平台差异。GitHub对Mermaid的支持有两种方式一种是在Markdown文件里直接写fenced代码块GitHub会自动识别并渲染另一种是.mmd独立文件GitHub不会默认渲染只能当文本看。GitLab则是两种都支持但对Markdown里Mermaid代码块的语法认定比GitHub更严格——GitLab要求fenced代码块必须明确指出语言是mermaid而GitHub可以通过内容自动识别但如果在GitHub的代码块里也标注了mermaid同样没问题。所以在代码块标注语言这一件事上我的标准做法是永远显式标注mermaid语言标签让两个平台都能识别。4.4 版本升级带来的语法兼容问题Mermaid v10到v11之间classDef的语法就发生过变化。老版本里classDef default fill:#f9f9f9这种写法还能用新版本里部分配置项被调整成了更加严格的schema。如果你维护着大量历史图表版本升级之后可能出现局部渲染异常但不会有明显的报错。这个坑的规避办法是在仓库里锁定Mermaid CLI的版本号不要随手npm install -g mermaid-js/mermaid-cli而是用package.json或lockfile固定到一个Stable版本。团队协作时如果多人同时维护图表统一版本能省去一大半莫名其妙的在我电脑上是好的问题。5. 把图表写进研发流程而不是画完就扔工具和语法都是表面功夫真正让代码化图表发挥价值的是流程建设。我见过不少团队确实用了Mermaid但图还是躺在某个人的本地文件里提交到仓库之后没人管。这跟用Visio画完扔共享文件夹本质上没有区别。5.1 在Git仓库里维护图表源文件第一步很基础但很重要约定图表源文件的存放位置。我建议在仓库里专门建一个docs/diagrams目录所有图表源文件以.mmd或.md的形式放在里面和代码一起做版本管理。为什么要单独设目录因为如果图表散落在各个文档里你很难追踪这张图属于哪个模块、上次更新是什么时候。集中存放之后配合一个简单的INDEX.md索引页就能做到打开目录就知道系统里有哪些图、各自描述什么。5.2 用CI/CD在PR里自动渲染架构图光有源文件还不够渲染这一步也应该自动化。GitHub Actions和GitLab CI都有现成的Mermaid渲染插件可以在代码提交后自动把.mmd文件转成PNG或SVG发布到Wiki或者作为构建产物。我实践下来最有价值的效果是PR提交的时候CI自动把改动涉及的图表渲染出来作为PR描述的一部分贴上去。评审人打开PR就能看到这张架构图随本次代码变更被更新了不需要手动下载渲染工具也不需要打开某个在线编辑器。这一步把图表的保鲜成本降到了最低。5.3 团队协作的三个约定自动化解决的是渲染问题但真正难的是人的问题——怎么让团队养成更新图表的习惯。我这里分享三个在实践中有效的约定PR模板里加一个选项本次变更是否涉及系统架构/依赖关系是/否。如果选是必须附上更新后的图表链接。这个看似死板实际是让更新图表从可选项变成强制项。代码评审里把图表diff作为审查点看到有人改了服务调用的代码但架构图没动直接打回去。新同学入职文档指向最新图表新人根据架构图了解系统如果图是旧的新人的第一反应是这份文档不可信以后就不会再看了。一份没人信任的文档比没有文档更糟。5.4 维护成本才是关键我的一次返工教训我最早在团队里推代码化图表时热情很高一周内画了一堆图时序图、用例图、部署图恨不得把整个系统事无巨细都画出来。结果三个月后这套图大部分已经过时因为画的时候太细细到内部类名和临时表名都画进去了而这些东西恰恰是变动最快的。那次返工后我给自己定了一个原则架构图只画三个月内不太可能变的东西。模块边界、服务依赖、数据流向这些属于稳定信息值得画某个定时任务的cron表达式、某个服务内部用了哪个框架这些不该进架构图写了就是给自己找维护负担。现在我的做法是每张图在文件头部用注释写上最后更新时间和维护人每次更新代码时顺手看一眼前面提到的PR模板选项需要动图就动不需要就保持原样。这个流程跑了大半年团队里的图表活跃度比我预想的高很多。说到底diagram-design这件事的核心不是工具不是语法而是你愿不愿意把图表当成代码一样对待——有版本、有评审、有生命周期。能做到这一点的团队不多但做到了之后图的寿命和可信度会完全上一个台阶。