☰
Mermaid vs PlantUML:文本化绘图选型与工程实践
2026/10/28 6:04:30 网站建设 项目流程

上周帮一个团队收拾技术文档,翻到一份三个月前的架构说明,里面的图和当前系统对不上的地方有七处。问起来才知道,图是当初有人用拖拽工具画的,导出成图片塞进文档,后来模块拆了、接口改了,没人愿意回去重新拖一遍。这事儿我见得太多了。从代码到图表的转变,本质上不是换个软件的问题,而是把图从"一次性消耗品"变成"可维护的产出物"。Mermaid 和 PlantUML 就是这条路上被比较最多的两个工具,一个轻得像便利贴,一个重得像整套工具柜。这篇内容我打算把两者的语法骨架、渲染模型、选型边界,以及我在真实协作里踩过的坑摊开讲,适合已经受够了手工拖框、想让图跟着代码一起迭代的开发者,也适合要给团队定文档规范的技术负责人。看完你应该能判断自己手头这个项目该用哪一个,以及怎么把它接进现有的写作和发布流程里。

1. 图为什么会失控:三个反复出现的现场

1.1 拖拽式绘图在迭代里失灵的三种典型

第一种是改一次错三处。系统里有十来个模块,你调整了其中一个的位置,结果箭头穿过了别的框,字体大小跟着变,导出的图糊成一团,只好重新排一遍。第二种是与代码脱钩。架构图躺在文档里,代码里的依赖关系天天变,两边没有任何约束关系,谁也不会主动去同步。第三种是评审时无法精确表达意见。同事在群里发一句"这个箭头好像不对",你打开源文件一看,那是一条画在画布上的线,没有名字、没有标签、没有 ID,根本对不上他指的是哪一条。

这三种现场的共同点是:图的成本集中在"绘制动作"上,而迭代恰恰反复发生在这个动作里。一次画完不再改的场景,拖拽式工具效率很高;但凡这个东西要活过两个版本,手工维护的成本就会指数级上涨。我见过一个项目,架构图一共十七个版本,最后没人知道哪张是对的,干脆全都留着,文档变成考古现场。

1.2 文本化绘图到底把成本挪到了哪里

文本化绘图的思路是把"画"这个动作换成"写"。你描述节点和关系,布局交给渲染器算。听上去多了一道学习成本,但成本结构变了:改动从"重新排版"变成"改一行字",评审从"看图片"变成"看 diff",复用从"复制粘贴像素"变成"复制一段文本换个名字"。

举个具体对比。要在流程图里插入一个新环节,拖拽式工具的操作是:拖一个框、输入文字、调整框大小、把原先的箭头删掉、重新连两根箭头、微调箭头折点、检查有没有压到别的元素。文本方式的操作是:在源文件里加一行B --> C,重跑渲染。前者耗时两分钟且容易出错,后者耗时三秒且不会破坏别的东西。这个差距在单次操作上不算什么,在一个季度五十次改动上就是几十个小时。

收益的另一半在可评审性。文本文件进版本库之后,每次修改都留下精确记录:谁在什么时候把哪个依赖关系删了,diff 里一行A --> B变成A --> C,清清楚楚。这个能力对需要过架构评审或者做合规留痕的团队尤其值钱,因为你能拿出"图在这一版是这样的"这种可举证的材料,而不是一堆命名混乱的 PNG。

1.3 哪些图适合写成代码,哪些别硬上

这里必须把话说清楚,文本化绘图不是万能的。适合的场景很明确:流程图、时序图、状态机、类图、实体关系图、部署图、甘特图、思维导图。它们的共同特征是结构比样式重要,节点和连线的关系是信息主体,位置和配色只是辅助。

不适合的场景同样明确。第一类是精细视觉设计,比如产品宣传页里的示意图、需要精确到像素的界面草图,这类东西的价值就在视觉细节上,渲染器给不了。第二类是布局本身就承载信息的图,比如一张手工绘制的时间轴海报,元素的位置排布是表达的一部分,交给自动布局反而糟。第三类是超大图,两百个节点的全景架构图,无论哪个渲染器都会给你一坨意大利面,这时候该做的是拆图,而不是换工具。

判断标准很简单:如果你能接受"图长什么样由工具决定,我只管内容对不对",那就用文本;如果你需要"每一个像素都必须由我决定",那就老老实实开设计工具。

2. Mermaid 的语法骨架:轻量带来的边界感

2.1 从一张最小流程图看懂它的设计取舍

Mermaid 的入口极其短。一段声明加几行连线就能出图,不需要装环境,绝大多数 Markdown 编辑器原生就能渲染:

flowchart TD A["用户提交表单"] --> B{"字段校验"} B -->|通过| C["写入数据库"] B -->|不通过| D["返回错误提示"] C --> E["触发通知"]

这段里几个语法点值得记住。TD是方向,可选TB、BT、LR、RL,画纵向流程用TD,画横向链路用LR,非常长的流程用LR能少占很多纵向空间。节点形状由括号决定:A[文本]是矩形,A(文本)是圆角矩形,A{文本}是菱形判断框,A[(文本)]是数据库桶形,A((文本))是圆形。连线上的文字写在管道符里,-->|通过|是一种写法,-- 通过 -->是另一种,前者对含空格的文本更友好。

最容易踩的是节点文本里有特殊字符。括号、冒号、引号、斜杠、<、>,一旦出现就必须用双引号把整段文本包起来,写成A["计算 (a+b)/2"]。我见过太多人卡在这里,报错信息只有一行 parse error,看不出问题在哪。养成习惯:只要标签里不是纯中文或纯英文单词,一律加引号,省事。节点 ID 也别用中文,虽然部分版本支持,但换个渲染器就翻车,用A、B、N1这类短 ID,标签放在括号里。

2.2 时序图、状态图、类图:语法差异比想象中大

Mermaid 支持十几种图,但它们的语法不是一套风格,切换时要重新适应。时序图用sequenceDiagram开头,参与者先声明再使用,消息箭头区分得很细:->>是实线带箭头,-->>是虚线,-x是带叉的终止消息,-)是异步。控制结构有alt/else/end、loop/end、opt/end、par/and/end,还有autonumber自动编号、Note over A,B加备注。

sequenceDiagram autonumber participant U as 客户端 participant S as 服务端 U->>S: 提交请求 activate S S->>S: 参数校验 alt 校验通过 S-->>U: 返回结果 else 校验失败 S-->>U: 返回错误码 end deactivate S

状态图用stateDiagram-v2,起始和结束是[*],迁移写成A --> B: 事件名,复合状态用state 名字 { ... }。类图用classDiagram,成员写在花括号里,+公有、-私有、#保护,关系符号区分继承<|--、组合*--、聚合o--、关联-->、依赖..>。这三个符号很容易记混,我的记法是:组合是实心菱形,聚合是空心菱形,菱形那块填不填色,对应"能不能独立存在"。

差异带来的实际问题是一份文档里混用好几种图时,作者容易把语法写串。建议做法是每类图单独建一个文件,文件名带类型前缀,写的时候参考现成模板,别凭记忆写。

2.3 渲染环境决定了它能走多远

Mermaid 的渲染分两条路径。一条是浏览器端渲染,页面里加载脚本,读取页面中的图定义块,在客户端转成 SVG。大多数笔记软件和 Markdown 预览走的就是这条路。另一条是构建期渲染,用命令行工具把源文件批量转成 SVG 或 PNG,塞进静态站点。

命令行工具需要 Node 环境,安装后用mmdc执行转换:

npm install -g @mermaid-js/mermaid-cli mmdc -i flow.mmd -o flow.svg -w 1200 -b transparent mmdc -i flow.mmd -o flow.png -w 2000 -b white -t dark

几个参数值得留意。-w控制输出宽度,SVG 其实用不上,但 PNG 必须给,否则默认尺寸小得看不清字。-b设背景色,导 PNG 时强烈建议显式给白色或透明,默认背景在某些场景下会出现灰色底。-t切主题,可选default、dark、forest、neutral。批量转换时用-i 目录 -o 目录,会按文件名一一对应。

主题定制还有一条更细的路:在源文件顶部插入初始化指令,写法是%%{init: {'theme':'forest','themeVariables':{'fontSize':'16px'}}}%%。这个是 Mermaid 里少见的"样式外露"设计,能用但不够好用,变量名散落在文档里,改一次要查半天。我的做法是接受默认主题,需要统一视觉就在外层容器上用 CSS 覆盖 SVG 里的font-family和font-size,比改主题变量稳。

一个实际经验:Mermaid 的自动布局在节点超过十五个之后质量下降明显,尤其是环状依赖和双向箭头。真需要画大图,用subgraph手工分组,能把布局质量拉回来一大截。

3. PlantUML 的另一条路:表达力换依赖

3.1 服务端渲染模型与本地环境怎么搭

PlantUML 的架构和 Mermaid 完全不同。它自带一套解析器和布局引擎(内部用 Graphviz 的 dot 做部分布局),本质是个 Java 程序,需要一台机器执行渲染,输出图片。这就是它被称作"服务端渲染"的原因——浏览器不认识这套语法,必须有东西把文本转成图。

本地搭建的门槛不算高但确实是门槛。装好 Java 运行时,下载 jar 包,用命令行调用:

java -Dfile.encoding=UTF-8 -jar plantuml.jar -charset UTF-8 -tpng ./diagrams/*.puml java -Dfile.encoding=UTF-8 -jar plantuml.jar -tsvg -charset UTF-8 diagram.puml

-t后面跟输出格式,常用png、svg、utxt(ASCII 图)、pdf。-charset UTF-8和-Dfile.encoding=UTF-8这两个参数在国内环境下几乎是必加项,不加就会出现中文变方块或者直接乱码的问题,这也是新手最常问的一类故障。

还有一个更省事的模式:PlantUML 自带一个简易 HTTP 服务,启动后可以用浏览器实时预览源文件,改动即刷新。在需要反复调图样式的阶段,这个模式比反复敲命令行舒服得多。另外不少编辑器插件内置了本地渲染,装完插件指定 jar 路径就行,写的时候能直接看到结果,这是长期使用 PlantUML 最推荐的姿势。

依赖带来一个副作用需要提前想清楚:如果文档要在没有 Java 环境的机器上预览,或者要在 CI 里生成图片,就得保证流水线镜像里有这套东西。这一点和 Mermaid 的"零依赖"形成直接对比,选型时不能忽略。

3.2 组件图、时序图、C4 模型的实战写法

PlantUML 支持的图类型比 Mermaid 多,表达力也更强。组件图的写法简洁到近乎裸文本:

@startuml skinparam componentStyle rectangle package "接入层" { [网关] as GW [鉴权服务] as AUTH } package "业务层" { [订单服务] as ORDER database "主库" as DB } GW --> AUTH GW --> ORDER ORDER --> DB @enduml

[名字]就是组件,package划组,database、queue、actor、cloud这些关键字直接给出图形语义,不用记形状映射表。这一点比 Mermaid 舒服:Mermaid 要记[(...)]是数据库、((...))是圆,PlantUML 直接写单词。

时序图的可读性也高一档,activate和deactivate可以用++和--简写,写成分号形式A -> B ++ : 请求,返回时B --> A -- : 结果,控制流有alt、group、loop、break,还支持...表示延迟、==表示分隔线。

真正拉开差距的是 C4 模型。这是一套描述软件架构的分层方法,PlantUML 通过扩展库直接支持,需要在文件头引入:

@startuml !include <C4/C4_Container> Person(user, "终端用户", "使用产品的人") System_Boundary(sys, "业务系统") { Container(web, "Web 前端", "浏览器", "负责交互") Container(api, "接口服务", "应用服务", "处理业务逻辑") ContainerDb(db, "数据存储", "关系型数据库", "存放业务数据") } Rel(user, web, "访问") Rel(web, api, "调用") Rel(api, db, "读写") @enduml

Person、System_Boundary、Container、ContainerDb、Rel这些宏自动处理图例、颜色、标注格式。Mermaid 想做同样的事得手搓subgraph加样式,做不到这么规范。如果你的团队在做架构文档沉淀,这一项的分量很重。

3.3 skinparam 与主题:样式控制的深浅两条路

PlantUML 的样式系统分三层。最浅的一层是主题,一行!theme plain或者!theme cerulean,内置主题列表有几十个,改一个词整个图风格就换掉。中间一层是skinparam,控制具体元素属性:

@startuml skinparam shadowing false skinparam defaultFontName "Microsoft YaHei" skinparam defaultFontSize 14 skinparam ArrowColor #4a6fa5 skinparam component { BackgroundColor #f5f7fa BorderColor #4a6fa5 BorderThickness 1 } @enduml

最深的一层是<style>块,支持类似 CSS 的选择器语法,能精细控制到某个 stereotype 的某个属性。三层的关系是从粗到细,绝大多数场景用前两层就够,<style>留给需要统一品牌视觉的团队。

这里有个务实的建议:样式别过度调。我见过一份文档,作者花了半天调箭头颜色和圆角半径,结果内容改了两版之后,样式配置和内容混在一起,谁也不想动。样式应该收敛到一个公共文件里,用!include引入,让图文件只关心结构。这跟前端把样式和结构分离是一个道理。

4. 正面比较:从语法到生态的选型依据

4.1 三个维度上的实际差距

把两者放在一张表里对比,比反复看文档快得多:

维度MermaidPlantUML
运行依赖无,浏览器或 Node 即可需要 Java 运行时和 jar 包
上手成本半小时能画第一张图半天到一天,含环境搭建
支持图类型流程、时序、状态、类、ER、甘特、饼图、思维导图等十余种上表全部,另加组件、部署、用例、C4、网络图、ASCII 图等
样式控制主题变量为主,细粒度弱skinparam 加 style 块,粒度很细
自动布局质量中小图良好,大图容易糊中小图更好,能用隐藏线引导布局
编辑器原生支持极广,多数 Markdown 工具直出需插件或服务,覆盖窄
版本库友好度极高,纯文本单文件高,但常配公共 include 文件
中文处理标签需加引号,字体依赖渲染环境需显式设字体和编码参数

表格之外补两个表格说不清的点。第一是错误提示质量。Mermaid 的解析错误信息经常只有一行位置提示,定位困难;PlantUML 会给出更具体的原因,比如"找不到这个别名",调试体验更好。第二是语法稳定性。Mermaid 的版本迭代比较快,新语法和老语法并存,跨版本渲染结果偶有差异;PlantUML 语法相对稳定,同一份文件隔两年再渲染,出来的图基本一致。

4.2 按场景做选择的判断路径

我把选型压缩成几个问题,按顺序问下去基本能得出结论。

第一个问题:这张图会不会出现在 Markdown 文档或者代码仓库的 README 里?如果是,选 Mermaid。理由很硬:渲染端不需要额外依赖,读者打开就能看,这个体验优势没法用其他能力弥补。

第二个问题:团队里有没有统一的文档生成流水线,且能控制镜像内容?如果有,PlantUML 的能力可以放开用;如果没有,引入 PlantUML 就等于给每个新人加一道环境配置作业,长期看维护成本会反噬。

第三个问题:图的类型是不是超出了流程图、时序图、类图这几个常见种类?需要画 C4 架构图、网络拓扑、部署图,PlantUML 是更合适的选择,Mermaid 在这些场景下要么不支持,要么需要大量手工模拟。

第四个问题:样式是否需要严格统一?要做对外交付的技术白皮书、需要精确控制视觉,PlantUML 的样式系统支撑得住;内部文档随便看看,Mermaid 的默认主题完全够用。

我的默认建议是:个人和小团队优先 Mermaid,先享受"零摩擦"的好处;等文档体系真的长出对架构图和样式统一的需求,再引入 PlantUML,并且只用在它明显更强的图上。

4.3 混用策略与团队规范怎么定

混用完全可行,而且我认为是多数团队的最终形态。关键是把边界划清楚,别让同一类图出现两种写法,那样维护起来更乱。

一个可落地的分工方案:所有流程类、时序类、状态类图用 Mermaid,嵌在 Markdown 里随文走;架构类图、部署图、对外交付的图用 PlantUML,源文件放独立目录,构建期导出 SVG,文档里引用图片路径。这样日常改动的图零依赖,重型的图集中管理。

配套要定三条规矩。第一,图源文件必须进版本库,导出的图片可以作为产物但绝不能是唯一副本。第二,文件命名统一风格,比如flow-order-submit.mmd、arch-deploy-prod.puml,前半段是类型,后半段是业务含义,谁都能猜出内容。第三,样式配置抽到公共文件,PlantUML 用!include,Mermaid 用统一的外层容器样式,禁止每个文件各调一套。这三条写进团队文档,新人的图就不会长出第二种风格。

5. 把图当代码管:容易被忽略的工程细节

5.1 目录结构与文件命名约定

图少的时候放哪都行,图超过二十张之后,目录结构直接决定你还能不能找到东西。我的习惯是跟文档结构对齐,而不是跟图的类型对齐。比如文档按业务域分目录,图就跟着放在对应业务域的diagrams子目录下:

docs/ order/ README.md diagrams/ flow-submit.mmd seq-payment.mmd infra/ README.md diagrams/ arch-deploy-prod.puml arch-network.puml

这样做的理由是查找路径最短。看订单文档的人要找订单的图,不会去一个全局的diagrams目录里翻。类型前缀的作用是在同一目录里多个文件时能快速区分,也方便写脚本按前缀批量渲染。

还有一条经验:文件名用英文短横线连接的小写词,别用中文、别用空格。不是为了好看,是为了让命令行批量处理和 URL 引用不出问题。中文文件名在部分渲染工具和静态站点生成器里会变成一串百分号编码,链接难看还容易断。

5.2 版本管理、diff 与评审的实际收益

文本图进版本库之后,收益比想象中大。最直接的是 diff 可读。一次修改把API --> 缓存改成API --> 消息队列,评审时一眼就能看到依赖关系变了,而不需要对比两张 PNG 找差异。

更进一步可以做图的一致性检查。写个脚本扫一遍源文件,抽出所有节点和关系,和代码里的模块清单比对,能发现"架构图里有一个已经不存在的服务"这类问题。这个做法不算普遍,但在模块多、变动快的项目里非常实用,成本也就一个几十行的脚本。

评审流程上也要调整。以前评审图是"看截图提意见",现在是"在 PR 里对某一行评论"。这个变化的实际影响是意见变具体了:以前是"这块感觉不太对",现在是"这行A --> B的建议方向反了,应该是 B 依赖 A"。沟通成本能降下来一半。

提醒一句:如果团队用 Git LFS 管大文件,注意别把.mmd、.puml也塞进去,它们是纯文本,走普通版本管理才能用 diff。这个坑我见过一次,配错了之后所有图的历史都变成二进制记录,diff 全废。

5.3 导出与嵌入:尺寸、字体、路径三个坑

导出环节的问题集中在三处。

尺寸是第一处。SVG 天然自适应,优先选它;必须用 PNG 时一定要显式指定宽度,Mermaid 用-w,PlantUML 用-scale或者在 skinparam 里调dpi。不指定的后果是文字糊、放大就失真,放文档里很难看。

字体是第二处。两台机器渲染同一份源文件,输出可能完全不同,因为字体不一样。中文图尤其明显,有的环境回退到衬线字体,字重和间距全变。解决办法是在源文件里显式指定字体名,PlantUML 用skinparam defaultFontName,Mermaid 靠外层容器 CSS 兜底,同时保证渲染机器上装了对应字体。

路径是第三处。文档里引用图片要用相对路径,别用绝对路径,也别用渲染服务返回的临时链接。后者会在某天突然失效,而且是静默失效——读者看到的是一个裂图,你自己完全不知道。我一般会在文档里同时保留图源文件和导出图,导出图只作为展示副本,源文件才是唯一可信来源,出问题随时能重新生成。

6. 我踩过的坑和对应的修法

6.1 中文、特殊字符与解析失败

Mermaid 里中文标签不加引号有时候能渲染,有时候不能,取决于标签里有没有跟着出现括号、冒号、斜杠这类字符。最保险的写法是一律加双引号:A["提交订单 (含优惠券)"]。另外注意别在标签里用未转义的引号,需要引号就用全角引号或者改说法绕开。

PlantUML 的中文问题主要是编码。命令行执行时不加-charset UTF-8和-Dfile.encoding=UTF-8,中文大概率变方块。另外字体也要显式设,不然默认字体找不到中文字形,出来的图里中文全是方框。这两条一起配,基本就不会再出问题。

还有一个隐蔽的坑:从网页或聊天窗口复制文本进源文件,容易带入不可见的零宽字符。这类字符肉眼看不见,但会让解析器报错,而且位置提示往往指不到真正的行。遇到"这行明明没问题却报错",用编辑器的显示不可见字符功能查一遍,十次有八次能抓到。

6.2 布局失控与大图拆分

自动布局的天花板比想象中低。Mermaid 在节点超过十五个、或者出现多个交叉的双向关系之后,出来的线会开始互相缠绕,怎么调都理不顺。PlantUML 好一些,但也撑不住一张几十节点的全景图。

我的处理办法是拆,不是调。把一张大图按层次切成三张:总览图只放五到七个块,每块对应一个子系统;子系统图各自展开内部节点;跨系统的关键链路单独一张时序图。读者按需看,比在一张巨图里找东西舒服得多。

拆图之外还有几个小技巧。Mermaid 用subgraph分组能显著改善布局,分组本身的边框还能起到视觉分区的作用;连线尽量单向,双向依赖拆成两条相向的线,布局算法处理起来更聪明;节点 ID 保持短,长 ID 会占布局空间。PlantUML 里可以用-[hidden]->画隐藏线,用来引导节点的相对位置,这个技巧在需要严格按层排列的部署图里很管用。

6.3 自动渲染流水线的几类故障

把渲染接进构建流水线之后,会遇到一批只在 CI 环境出现的问题。

第一类是环境缺字体。本地渲染正常,流水线里跑出来中文变方块,原因就是镜像里没有中文字体包。解决办法是在镜像里显式装一套中文字体,并在源文件里指定字体名,两边对上。

第二类是 Mermaid 的浏览器依赖。命令行工具底层用无头浏览器渲染,容器环境里经常缺共享库或者沙箱权限不足,报错信息很难懂。常见处理是在启动参数里禁用沙箱,或者换成官方提供的容器镜像,省去自己配环境的时间。

第三类是渲染失败但不阻断构建。默认情况下脚本报错可能被忽略,流水线照样绿灯,结果文档里的图是上一次的旧产物。我的做法是把渲染步骤的退出码当成硬性检查,任何一个文件渲染失败就让整个构建失败,逼迫问题当场解决。这条规矩看着严,实际省下来的排查时间非常多。

第四类是产物缓存。渲染出来的图片如果不带内容哈希就上传,缓存层会一直返回旧图,你明明改了源文件,页面上还是老样子。加上源文件的哈希值作为文件名或者查询参数,这个问题就消失了。

最后再分享一个我一直在用的小技巧:给每类图准备一份"骨架模板",把@startuml、主题设置、常用 skinparam、字体配置全部写好,新建图的时候复制模板改内容。这样做的好处是新图一开始就符合团队规范,不用每次翻文档查参数名,也避免了同一个团队出现五种配色这种尴尬局面。模板放在仓库的templates目录里,定期和维护文档的人对一次,比写在Wiki里没人看强得多。

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

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

立即咨询