diagram-design:把项目图表当代码治理,实现版本化与CI校验
2026/9/8 16:24:05 网站建设 项目流程

diagram-design:把项目里的每一张图,都当成代码来治理

做技术这么多年,我越来越觉得“画图”这件事,在大多数团队里都是被严重低估的。需求评审要画流程图,架构评审要画部署图,接口设计要画时序图,数据库设计要画ER图,甚至运维排查事故也要靠拓扑图还原现场。但你有没有发现,几乎所有团队的图都是“一次性”的——画完就扔进文档里,下次要改的时候找不到源文件,或者Git里躺着一堆draw.io/figma导出后的PNG,版本一换代,谁也说不清当前这张图到底是不是最新的。

我参与过一个很有意思的项目,代号就叫“diagram-design”。这个项目的定位很明确:把项目里所有的图示资产,从“设计图”变成“可维护的工程产物”。说人话就是,我们用一套统一的代码化方案,把架构图、流程图、时序图、部署图全部管起来,让图表能像代码一样被review、被diff、被版本化、被自动校验。这篇文章我就把这个项目从0到1的设计思路、工具选型、踩坑记录全部拆开讲讲,希望对那些正在为“团队图表混乱”头疼的同行有点帮助。

1. 内容整体设计与思路拆解

1.1 为什么要把“图表”当成一个独立的设计项目来治理

先说结论:如果你所在的团队遇到下面任意一条,那“diagram-design”这套思路大概率能救你。

第一,文档里的图和代码严重脱节。代码改了三次,架构图还停留在三个月前。第二,图中的文字没人规范,命名全靠灵感,同一层级的模块一会儿叫“订单中心”,一会儿叫“OrderService”。第三,图的格式五花八门,有线上白板导出的,有画图软件截图的,有Visio的,粒度参差不齐。第四,评审的时候看不懂图。画的太复杂,箭头乱飞,颜色花哨,观感上像“彩虹糖现场”。

之所以要把图表提升到“独立设计项目”的层次,核心逻辑是:图本身不是目的,图和代码之间的一致性才是目的。代码有规范、有review、有CI在保证质量,图呢?大多数团队的图,连一个“最低可维护标准”都没有。

所以“diagram-design”一开始定的原则就三条:

  1. 一切图表必须文本化。只允许用Mermaid、D2这类基于DSL的描述性语言来定义图,不允许直接产出静态图片当唯一档案。
  2. 一切图表必须版本化。图的定义和相关文档一起纳入Git管理,每次改动都能看到diff。
  3. 一切图表必须可校验。支持命令行渲染、自动导出为SVG/PNG,并将生成结果纳入构建流程,在CI阶段校验所有图都能正常渲染。

这个思路其实是把软件开发里的优秀实践移植到“图示设计”上,起源可以追溯到“Diagrams as Code”这个理念。它不是某一家公司的专利,而是一种社区共识。你只需要理解一句话:图就是代码,代码就要有治理

1.2 需求拆解:一张“好图”到底需要满足什么

在动手搭建之前,我把需求拆成了三块来看:

第一层:业务需求。团队里的角色分成三类——架构师、开发者和项目经理。架构师要画大粒度的系统蓝图,开发者要看局部流程和类关系,项目经理更关心状态迁移和部署拓扑。这三类角色对图表的关注点完全不同,一套方案必须同时满足。

第二层:工具需求。工具最好是开源的、可脚本化的、能融入现有CI/CD流程的。不能是那种只能在网站里在线编辑、无法自动化导出的。同时输出的格式要通用,至少支持SVG和PNG两种。

第三层:规范需求。要有一份可以强制执行的图示规范模板,包括颜色、线型、字体、图标、命名规则。否则每个人画的图都是“自由发挥”,即便工具统一了,风格依然五花八门。

在正式选型之前,我也看过一些现成的解决方案,比如直接让团队统一用draw.io,把文件存到git的某个目录下。思路对了一半,但draw.io的源文件是XML,虽然可以放进git做版本管理,但diff起来简直灾难,一行代码级的改动可能产生几百行XML差异,review成本极高。所以“diagram-design”从一开始就把核心工具锁定在“文本化图表DSL”这个方向上,从源头上保证diff是干净的人类可读文本。

2. 工具链选型与核心方案解析

2.1 Mermaid、D2、Graphviz、Excalidraw到底怎么选

当前主流的文本化图表工具有这么几类,我各做了几轮实测:

工具语法难度内置图形丰富度时序图支持实时预览导出质量生态成熟度
Mermaid低,十分钟上手较高优秀丰富,VS Code插件很稳一般,需调整缩放高,GitHub原生支持
D2低-中基础够用较丰富优秀,默认排版惊艳中,社区发展极快
Graphviz高,需要理解dot语法一般高,适合自动布局高,老牌稳定
PlantUML低-中优秀丰富一般

实测下来的感受是:

  • 如果你需要大量画时序图、状态图和甘特图,Mermaid是目前综合成本最低的,因为GitHub和GitLab对Mermaid有原生渲染支持,PR里的图直接就能看,不用额外搭建服务。
  • 如果你对“美观”有比较高的要求,D2的默认排版我认为是这几款里最好的,它对“箭头重叠”“标签遮挡”的处理非常聪明,省了很多手工调整的功夫。
  • Graphviz适合复杂的节点自动布局,但写起来很痛苦,举个例子:一个简单的流程图,Mermaid写十行,Graphviz可能要写三十行,而且还不一定能一次写对。

我这里给的选型结论是:“diagram-design”项目采用Mermaid作为主渲染引擎,D2作为备选方案,Graphviz降级为特殊场景专用。为什么主选Mermaid?因为它在现代代码托管平台上的原生支持太香了,团队成员把.md文件往仓库里一推,浏览页面上直接就能看到渲染好的图,零学习成本、零维护成本,这种体验是其他工具很难比的。

2.2 围绕Mermaid搭一套“可维护的文本图体系”

工具有了,但光有工具成不了体系。真正让“diagram-design”落地的是下面这三板斧:

第一板斧:统一目录结构。在仓库里专门开辟一个diagrams目录,按图类型分子目录:flowcharts(业务流程图)、sequence(时序图)、state(状态图)、architecture(系统架构图)、deployment(部署拓扑图)。每张图的源文件命名规则统一为“数字前缀-模块名-图名.mmd”,比如:010-order-module-state.mmd。数字前缀用于控制文档里展示顺序,模块名用于关联代码模块,图名用于人眼识别。

第二板斧:写一个统一渲染脚本。团队里不可能每个人都装Mermaid的命令行工具,所以项目里提供了一个一键渲染脚本,内部调用mermaid-cli,把所有的.mmd文件批量渲染成SVG和PNG,输出到dist目录。不依赖任何在线服务,离线环境下也能用。

第三板斧:CI流程接入。在GitHub Actions或GitLab CI里加一个job,每次代码push后自动渲染所有.mmd文件,如果渲染失败,直接把pipeline标红。这样就能从机制上保证:所有进入主干分支的图,都是可以正常渲染的。

这三板斧的核心并不是技术含量多高,而是“把图当成代码治理”的心智。一旦图和代码一样被置于同样的质量门禁下,团队成员自然会开始在意图的规范性和可维护性。

3. 实操过程与核心环节实现

3.1 第一步:从0搭建diagram-design的目录骨架

整个实操过程分成五个阶段,我建议你按顺序来,可以少走很多弯路。

先看目录骨架。我最终的目录结构长这样:

diagram-design/ ├── README.md # 项目说明、使用指引、规范速查 ├── package.json # 脚本管理,依赖声明 ├── .github/ │ └── workflows/ │ └── render-diagrams.yml # CI渲染流程 ├── diagrams/ │ ├── flowcharts/ # 业务流程图 │ ├── sequence/ # 时序图 │ ├── state/ # 状态图 │ ├── architecture/ # 系统架构图 │ └── deployment/ # 部署拓扑图 ├── scripts/ │ ├── render.mjs # 批量渲染脚本 │ └── lint.mjs # 图文件规范性检查(命名、引用等) └── assets/ ├── fonts/ # 统一字体文件 └── templates/ # 各类图的标准模板

看起来东西不少,但核心真正发挥作用的只有scripts/render.mjs这一个入口脚本,以及CI流程里对它的调用。mermaid-cli本质上封装了Puppeteer,用无头浏览器渲染图表,所以你本机必须能正常安装Puppeteer,否则渲染脚本跑不起来。这里有个坑我后文会单独讲。

3.2 第二步:定义图表规范和样式体系

工具能帮你渲染图,但渲染出来的图好不好看、规不规范,完全由“样式定义”决定。Mermaid支持通过init指令来全局配置主题、颜色、字体,我们把统一规范固化在项目里的mermaid.config.json里。

{ "theme": "base", "themeVariables": { "primaryColor": "#4A90D9", "primaryTextColor": "#ffffff", "primaryBorderColor": "#2C5F8A", "lineColor": "#5B7B9A", "fontFamily": "PingFang SC, Microsoft YaHei, Noto Sans CJK SC, sans-serif", "fontSize": "16px" }, "flowchart": { "curve": "basis", "nodeSpacing": 50, "rankSpacing": 50, "useMaxWidth": true }, "sequence": { "mirrorActors": false, "actorMargin": 80, "messageMargin": 50 } }

这份配置的核心思想是:颜色和字体是统一的,间距是经过调优的,各个图类型的参数是预设好的,这样谁画出来的图风格都不会差太远。

有几个细节值得单独强调:

  • 字体一定要显式声明中文字体。否则默认的字体栈在Linux CI环境上渲染出来的中文全是“豆腐块”,导出SVG后文字丢失、错位,惨不忍睹。
  • 节点间距和层级间距不要太大也不要太小。我试过rankSpacing: 100,结果四个节点的流程图在PNG里能占满一屏,拆开后完全没法看。
  • sequence的mirrorActors建议设成false,这样参与者只在顶部出现一次,图更紧凑,尤其适合流程较长、参与者较多的场景。

3.3 第三步:编写可复用的图表模板

规范定义好了,每个团队成员画图时从零开始写也会出现风格漂移。所以项目里准备了几个典型模板,大家直接复制模板改成自己的内容就行。

以架构图模板为例,我们统一用flowchart的TB(Top to Bottom)方向,配合子图(subgraph)来分组模块。模板长这样:

flowchart TB subgraph Client["客户端层"] Web["Web端"] App["App端"] MiniProgram["小程序"] end subgraph Gateway["接入层"] Nginx["Nginx网关"] Auth["统一认证"] end subgraph Service["业务服务层"] OrderService["订单服务"] PayService["支付服务"] UserService["用户服务"] end subgraph Storage["存储层"] MySQL[("MySQL主库")] Redis[("Redis缓存")] Kafka[("Kafka消息队列")] end Web --> Nginx App --> Nginx MiniProgram --> Nginx Nginx --> Auth Auth --> OrderService Auth --> PayService Auth --> UserService OrderService --> MySQL OrderService --> Redis OrderService --> Kafka

注意这里子图的命名我用了双引号包裹中文别名,语法上其实是subgraph节点ID["显示用名称"]的写法,这样图里显示的是中文,ID是稳定的英文标识,后面如果要做自动化分析,可以直接拿ID去关联代码模块。这是一个很实用的小技巧,能兼顾“人眼可读”和“机器可解析”。

3.4 第四步:渲染脚本的完整实现

这是整个项目里最关键的产物。render.mjs脚本做的事情很简单:遍历diagrams目录下所有的.mmd文件,调用mermaid-cli,一个接一个地渲染出声。

import { execa } from 'execa'; import { glob } from 'glob'; import path from 'node:path'; import fs from 'node:fs'; const ROOT_DIR = process.cwd(); const DIAGRAMS_DIR = path.join(ROOT_DIR, 'diagrams'); const OUTPUT_DIR = path.join(ROOT_DIR, 'dist'); async function renderAll() { if (!fs.existsSync(OUTPUT_DIR)) { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); } const files = await glob('**/*.mmd', { cwd: DIAGRAMS_DIR }); const results = []; for (const file of files) { const inputPath = path.join(DIAGRAMS_DIR, file); const outputBase = path.join(OUTPUT_DIR, path.dirname(file)); const fileBase = path.basename(file, '.mmd'); fs.mkdirSync(outputBase, { recursive: true }); try { const svgPath = path.join(outputBase, `${fileBase}.svg`); const pngPath = path.join(outputBase, `${fileBase}.png`); // 渲染SVG,再转PNG await execa('mmdc', [ '-i', inputPath, '-o', svgPath, '-c', path.join(ROOT_DIR, 'mermaid.config.json'), '-b', 'white', ]); await execa('npx', [ 'sharp-cli', '-i', svgPath, '-o', pngPath, 'resize', '1600' ]); results.push(`[OK] ${file}`); } catch (err) { results.push(`[FAIL] ${file}: ${err.stderr || err.message}`); } } console.log(results.join('\n')); const failed = results.filter(r => r.startsWith('[FAIL]')); if (failed.length > 0) { process.exit(1); } } renderAll();

这段脚本有几个设计思路想展开说说:

  • 用glob按**/*.mmd递归匹配,而不是手动列文件列表,是为了让新增文件时不需要改脚本。
  • SVG渲染出来后保留一份原始版本,这个很有用。SVG可以被网页直接引用,也可以被矢量软件再编辑,未来如果想通过程序提取节点信息做自动化分析,SVG是标准格式。
  • PNG用sharp-cli做了resize,统一到1600像素宽度。这个宽度兼顾“放大后能看清”和“文档导入后体积可控”两个要求。实测一张复杂的时序图,1600宽PNG大概在100~300KB之间,可接受。

3.5 第五步:CI校验和文档集成

CI环节我用的是GitHub Actions,完整流程文件如下。核心目标只有一个:让图表的渲染校验成为代码合入的前置条件。

name: render-diagrams on: push: branches: [main] pull_request: jobs: render: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: Render diagrams run: node scripts/render.mjs

这段YAML非常朴素,没有花哨的并行配置和缓存优化,但胜在简洁可靠。只要仓库里有任何.mmd文件写坏了,pipeline就会标红,问题会被拦截在code review阶段之前。

还有一个落地细节:主要文档里引用图的方式,建议统一用相对路径的SVG,而不是PNG。比如在项目的README.md里:

系统整体架构如[图1]所示。 图1 系统整体架构 ![系统架构](./diagrams/architecture/010-trade-system-arch.svg)

为什么用SVG?因为SVG是矢量格式,在网页上缩放不糊,而且可以用CSS控制样式,文档观感会好很多。PNG只留给那些需要插入Word、PPT等办公文档的场景。

4. 常见问题与排查技巧实录

4.1 中文乱码与字体缺失问题

这大概是mermaid-cli渲染时最常遇到的问题,而且没有之一。我一开始在本地Mac上渲染一切正常,推到CI上跑就崩了。排查后发现CI的容器里没有中文字体,渲染出来的结果里所有中文都显示为方块。

解决办法有两个方向:

一是给脚本单独装上中文字体包。在CI的YAML里增加一步安装字体的命令:

- name: Install CJK fonts run: | sudo apt-get update sudo apt-get install -y fonts-noto-cjk

二是利用Puppeteer的chromium启动参数,加载项目自带的字体文件到内存中。这个方案相对麻烦,但胜在可控,适合内网环境。

实操中我建议直接选第一个方案,简单粗暴有效。

4.2 SVG渲染不出的高阶语法错误

Mermaid的语法更新速度很快,偶尔会出现本地渲染正常、CI上渲染失败的情况。这多半是因为CI拉取的mermaid-cli版本与本地不一致。mermaid-cli内部依赖的mermaid核心版本,决定了它能解析哪些新语法。

处理办法是package.json里锁定精确版本号,而不是用^或~前缀。比如:

{ "devDependencies": { "@mermaid-js/mermaid-cli": "10.9.1" } }

版本号写死之后,我在CI里再也没遇到过“本地能出图、线上不能出图”的诡异问题。这是一个很细但极其重要的经验。

4.3 渲染出来的图太“宽”怎么办

Mermaid的useMaxWidth默认值是true,意思是渲染时自动撑满可用宽度,但PNG导出时如果图很大,宽高比会非常夸张,插到文档里字小得看不清。

我的处理方式是:脚本里对PNG做resize,并且把useMaxWidth设成false,强制让图的内容区不要无限拉伸。如果图实在太大,优先考虑把它拆分成多个子图,再画一张总览图串起来。拆图这个动作本身也能强迫你把系统边界想得更清楚,一举两得。

4.4 流程类图与状态类图混用导致信息过载

有一个比较隐蔽的“体验问题”:很多团队不管画什么图,一律用flowchart。就我观察,这是信息过载的头号来源。一个订单状态流转,画出来全是箭头和判断分支,看半天看不出几种状态;而真正的状态图只需要几个圆角矩形加状态名,一目了然。

“diagram-design”项目里我明确做了分类:业务操作流程、审批流程用flowchart;对象生命周期、状态迁移用stateDiagram;跨服务调用链用sequenceDiagram;部署关系用flowchart LR加子图。分类不是限制自由,而是降低读图成本。图的价值是让读者三秒钟内看懂信息,而不是让他们对着箭头绕迷宫。

工具适配场景这个观点,是我在这套项目里踩过几次坑之后才总结出来的,也是我认为这个项目最核心的知识沉淀之一。

4.5 团队协作中的命名规范原子性

最后一个常见问题比较“软性”:即便有了目录和命名规则,实际协作中仍然会有人在文档里随意引用不存在的图文件,或者图文件名改了,但文档里的引用没同步更新,导致渲染出来的文档一堆死链。

这块我建议引入一个轻量级全量检查脚本,扫描所有.md文件里引用的.svg/.png路径,对照dist目录里生成的实际文件,发现引用不存在的就报错。脚本本身不复杂,半小时就能写完,但它能让“文档、图片、代码”三者的一致性从自觉行为变成工程约束,效果立竿见影。

关于最终呈现的一点体会

回到“diagram-design”这个项目的本质,它真正解决的问题,其实不是“怎么画出一张漂亮的图”,而是“怎么让图在团队里持续地保持有效”。

我用这个方案接管了项目的可视化文档之后,最大的感受是:图和代码之间的缝隙被填上了。代码改动引发的结构变化,会自然地反馈到图的更新需求上;图更新之后,评审的时候大家讨论的是真实的设计,而不是对着过期的架构图争论早已不存在的模块。这种“图随代码走”的节奏,让设计文档不再是一个被遗忘的摆设。

另外还有一点很值得说的,就是这套方案几乎不挑团队规模。哪怕你是一个人在维护一个开源项目,只要在仓库里加一个diagrams目录和一份渲染脚本,你的所有架构演进记录就可以用一张张变化可追溯的图来表达,这本身就是一种极好的工程习惯。每次迭代回顾的时候,翻一翻历史图表的diff,系统是怎么一步步变成今天这个样子的,一目了然。这种体验,用传统画图软件是永远体会不到的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询