平时做需求设计,状态图是绕不开的东西。订单状态、审批流程、设备运行周期,几乎每个业务域都有一堆“状态和流转”要讲清楚。最开始时,我习惯打开专用的UML建模工具或者在线画图网站,画完再截图贴到文档里,后来整个团队切到 Typora,用 Mermaid 语法直接在文档里画状态图,体验完全不一样——图不是“贴”进去的,而是文档的一部分,改一行文本,图就跟着变。这篇文章就把我用 Typora 画状态图的完整经验整理出来,从选型理由到基础语法、从复合状态到实战案例,再到导出时容易踩的坑,希望对正在折腾 U状态图和状态机文档的人有点用。
1. 为什么是 Typora + Mermaid:状态图选型里的真实理由
状态图在软件设计里并不是一个“锦上添花”的图,它回答的是这个对象到底能怎么活、怎么死的问题。用 Typora 画状态图,核心不是 Typora 这个编辑器本身有多强,而是它把“写 Markdown”和“渲染图表”这两件事无缝接在了一起。你不用再打开第二个软件,不用手动拖拽连线,也不用担心图和文档内容对不上。
1.1 状态图在项目里到底解决什么
一个对象从创建到销毁,会经历哪些状态,哪些事件触发状态切换,切换时有哪些前置条件,这就是状态机。状态图就是把这个状态机画出来,让产品、开发、测试能站在同一张图前对齐认知。很多隐藏分支就是在画图的过程中被翻出来的,而不是在测试阶段才暴露。
我自己的经验是:如果只给一段需求文字,不画状态图,代码里最容易出现的 bug 就是“非法状态迁移”——测试会发现对象从一个不该存在的状态跳到了另一个状态。而一旦把状态图画出来,这种非法路径在评审阶段就会被直接指出来。比如某个状态漏了出口、某个事件画了双向箭头但实际业务只允许单向,这些问题看图比看文字清楚得多。
1.2 为什么不用 Visio、draw.io、PlantUML
不是这些工具不好,而是它们和“文档跟着代码走”的工作流不太匹配。下面这张表是我在实际团队里对比后的感受:
| 工具 | 优势 | 我放弃它的关键原因 |
|---|---|---|
| Visio | 专业,UML模板全,线条样式多 | 收费,且图与文档分离,改了逻辑要手动改图 |
| draw.io | 免费,浏览器可用,导出方便 | 同样是图与源文件分离,多人协作时容易版本冲突 |
| PlantUML | 文本建模,适合写码的人 | 依赖 Java 环境,配置稍重,实时预览体验一般 |
| Typora + Mermaid | 文本即图,渲染实时,图随文档走 | 样式自定义能力不如专业工具,超大图有性能上限 |
选型的时候想清楚自己的核心诉求就行。状态图大部分时候是给开发团队和产品对齐用的,不是要交付给客户的精美设计稿。所以“容易改、和文档在一起、能进 Git 走 diff”,远比“细节样式炫酷”重要。Typora 的 Mermaid 方案正好命中这几个点。
1.3 Typora 渲染 Mermaid 的机制
Typora 本身不画图,它提供的是 Mermaid 代码块的实时渲染。在 Typora 里,凡是代码块语言标记为 mermaid,编辑器会调用内置的 Mermaid 解析器,把代码块内容解析成图,直接显示在文档中。日常编辑时,光标离开代码块就显示图,点进代码块就切回源码。这种“所见即所得”的交互,是我选它的关键理由。
这里要澄清一个常见的误会:很多人以为 Typora 是装了某个插件才支持 Mermaid,其实不是。它从较早就内置了 Mermaid 支持,不需要额外插件,打开 md 文件就能用。不过不同版本的 Typora 内置的 Mermaid 引擎版本不一样,一些很老的版本可能不支持更新的语法。这个点我放到最后一章细说。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 状态图最低可运行的语法结构:状态、迁移与标签
2.1 一个最小状态图的写法
在 Typora 中新建一个 md 文件,输入反引号创建代码块,语言标记为 mermaid,然后写:
mermaid复制stateDiagram-v2
[*] --> 待支付
待支付 --> [*]
这个图只有两个元素:起始点 [*] 和状态“待支付”,进入状态后直接回到终止点。虽然简单,但它是完整的、可运行的。凡是能在 Typora 里被渲染成功,说明语法链路没问题。后面所有复杂状态图,都是在这个最小结构上不断加节点、加箭头。
我习惯每次新建状态图文件时,先把这个最小用例跑一遍,确认当前环境没问题,再往里面填内容。不然写到一半发现渲染不出来,容易分不清是环境问题还是代码问题。
2.2 状态名的三种表达方式
状态名的定义看起来简单,实操里坑不少。Mermaid 状态图支持三种写法。
第一种,直接写状态名:
mermaid复制stateDiagram-v2
[*] --> 待支付
要求状态名里不能包含空格和特殊字符,否则解析会出问题。如果状态名是英文且没有空格,这样写最省事,但大型项目里状态名往往一长串,比如 PENDING_PAYMENT,直接写也行。
第二种,加双引号,适合状态名里带空格或特殊字符时使用:
mermaid复制stateDiagram-v2
[*] --> "Pending Payment"
这种写法的缺点是显示名只能是一个字符串,不方便在代码引用时保持稳定的英文标识符。
第三种,也是我最推荐的一种,用别名:
mermaid复制stateDiagram-v2
state "待支付" as PENDING_PAY
[*] --> PENDING_PAY
PENDING_PAY --> [*]
代码层面状态名是稳定的英文标识符,图里显示的是中文。后边改显示文案,只需要改引号里的部分,箭头逻辑完全不用动。
我建议团队统一用 state "文案" as 标识符 这套写法。状态一多,别名能显著降低改图和 review 的成本。你自己画着玩无所谓,但进了团队协作,这套约定比想象中有用。
2.3 迁移箭头和事件标签
状态图最核心的语义就是迁移:从哪个状态出发、在什么事件或条件下、到达哪个状态。Mermaid 里用 --> 表示方向,冒号后面写触发事件或条件:
mermaid复制stateDiagram-v2
state "待支付" as PENDING
state "已支付" as PAID
[*] --> PENDING
PENDING --> PAID : 支付成功
PAID --> [*]
这里 PENDING 走到 PAID,靠的是“支付成功”这个事件。写标签的时候,我习惯用“动词 + 名词”的格式,比如“支付成功”“用户取消”“超时关闭”,而不是写“状态变更为已支付”这种废话。标签是给读图的人看的,背后通常对应代码里的一个方法名或消息名,越简洁越好。
Mermaid 还允许在标签里写条件表达式:
mermaid复制stateDiagram-v2
state "待审核" as REVIEW
state "通过" as PASS
state "驳回" as REJECT
[*] --> REVIEW
REVIEW --> PASS : score >= 60
REVIEW --> REJECT : score < 60
这种写法在需求评审阶段特别好用,把判断条件直接画在箭头上,大家一眼就能看到分支依据。
2.4 起始与终止状态
Mermaid 中,[*] 既代表起始状态,也代表终止状态。一个状态图可以没有终止状态,比如常驻系统;但起始状态最好有,否则读图的人不知道从哪看起。
一个状态图只能有一个起始点,但可以有多个终止点。如果业务上确实存在多个出口,直接画多条指向 [*] 的线就行。别自作聪明画两个起始点,Mermaid 不会报错,但需求评审时大家会困惑。
3. 从简单到复杂:嵌套、并发与条件分支的表达方式
3.1 复合状态:把同组状态收进一个容器
状态一旦多起来,平铺的图会像蜘蛛网。Mermaid 的复合状态语法可以把整组状态嵌套在一个容器里:
mermaid复制stateDiagram-v2
[*] --> 处理中
state 处理中 {
[*] --> 参数校验
参数校验 --> 业务执行
业务执行 --> 结果回写
结果回写 --> [*]
}
处理中 --> [*]
注意复合状态内部的 [*] 表达的是“进入这个复合状态后的起点”和“离开这个复合状态前的终点”,和外层图的 [*] 语义不同。这也是很多人第一次用的时候绕晕的地方。
我的建议是:把复合状态想象成一个独立的子状态机,它自己有开始和结束。复合状态对外表现成一个整体,对内则是一套完整流程。比如订单的“售后处理中”,里面可能包含“用户申请”“客服审核”“退款打款”“结果通知”这几个子状态,外部只关心“进入售后”和“售后结束”。
3.2 分区:在复合状态里并列展示
如果复合状态内部的子状态有明确的职责划分,可以用 -- 作为分区符号:
mermaid复制stateDiagram-v2
state "订单处理" as ORDER {
[*] --> 预检查
预检查 --> 支付环节
--
支付环节 --> 配送环节
配送环节 --> [*]
}
[*] --> ORDER
ORDER --> [*]
这里的 -- 表示在同一个复合状态内,将区域分成上下或左右两部分。我的用法是把它对应到代码里的多个模块:上半区是校验逻辑,下半区是执行逻辑。节点不多的情况下,图看起来会很清爽。
3.3 并发状态:用分隔符表达并行子流程
业务里常见“主流程和异步任务并行”的场景。Mermaid 的 stateDiagram-v2 支持把复合状态内部用 -- 分隔成多个并行的分支,官方称之为 concurrent state:
mermaid复制stateDiagram-v2
[*] --> 运行中
state 运行中 {
[*] --> 主流程
--
[*] --> 异步任务
--
[*] --> 监控任务
}
运行中 --> [*]
这里三个分支是并行执行的,渲染出来的图会显示为多个并列区域。我通常用它描述“一个状态同时包含多个子任务”的场景,比如后台任务同时在做数据拉取和日志上报。画完这种图,建议人工对着代码检查一遍:并行的分支真的互不依赖吗?如果某一个分支失败,会影响其他分支吗?这往往是需求设计阶段最值得确认的事。
3.4 自环:状态原地循环
自环就是状态迁移到自身,语法简单但很容易被忽略:
mermaid复制stateDiagram-v2
[*] --> 运行
运行 --> 运行 : 心跳
运行 --> [*] : 关机
“运行”在收到心跳事件时仍然停留在“运行”状态,但这次事件本身需要记录。业务里最常见的自环是定时任务、心跳上报、轮询检查。画出自环能提醒开发人员:这个状态不是停留在那里不动,而是每过一个周期都要处理一次事件。如果不画出来,实现阶段很容易漏掉周期任务。
3.5 方向控制
状态图默认从上到下排列。我更喜欢在处理线性流程时指定左到右:
mermaid复制stateDiagram-v2
direction LR
[*] --> 开始
开始 --> 处理
处理 --> 结束
结束 --> [*]
direction LR 放在 stateDiagram-v2 下面第一行。横向布局适合“启动→处理→结束”这种线性流程,纵向布局适合深层嵌套和复杂分支。到底选哪种,取决于你显示器宽还是高,以及最终要贴到文档里的排版位置。
4. 实战拆解:一个订单状态机从规则梳理到成图
4.1 先列业务规则,再动手画
很多人画状态图,一上来就写代码,结果状态漏了一堆。我现在的习惯是先建一张表,把状态、触发事件、前置条件、后置状态、备注五项列清楚。以订单为例:
| 当前状态 | 触发事件 | 前置条件 | 后置状态 |
|---|---|---|---|
| 待支付 | 提交订单 | 商品有库存 | 待支付 |
| 待支付 | 用户取消 | 未支付 | 已关闭 |
| 待支付 | 支付回调成功 | 支付网关回调 | 已支付 |
| 已支付 | 商家发货 | 已支付 | 已发货 |
| 已发货 | 确认收货 | 物流签收 | 已完成 |
| 已完成 | 申请售后 | 在售后时效内 | 售后处理中 |
| 售后处理中 | 退款成功 | 审核通过 | 已退款 |
这张表比图更重要。图的每一根箭头,必须能在表里找到一个来源;如果表里写了某条迁移,但图上没有,那就是漏了。反过来也一样。我见过太多“图和表对不上”的设计文档,评审的时候还得一项一项核对,很消耗耐心。
4.2 定义状态和事件
根据上面的表格,可以确定状态集合:待支付、已支付、待发货、已发货、已完成、已关闭、售后处理中、已退款。事件集合:提交订单、用户取消、支付回调、商家发货、确认收货、申请售后、退款成功。
这里要特别留意“待支付”的出口有两个:一个是正常支付,一个是取消关闭。一个状态可以有多个出口,但每个出口要有明确的事件和条件,不要让读者替你做判断。
定义的时候还要想清楚一件事:哪些状态是终态?已关闭、已退款,属于终态;已完成状态下还能申请售后,所以“已完成”在业务上并不是绝对的终态。这种边界如果不画图,很容易被忽略。
4.3 完整代码与成图效果
下面这个例子是我在 Typora 里实际用的订单状态机,覆盖了订单核心闭环:
mermaid复制stateDiagram-v2
direction LR
state "待支付" as PENDING
state "已支付" as PAID
state "待发货" as TO_SHIP
state "已发货" as SHIPPED
state "已完成" as DONE
state "已关闭" as CLOSED
state "售后处理中" as AFTER_SALE
state "已退款" as REFUNDED
[*] --> PENDING : 提交订单
PENDING --> PAID : 支付回调成功
PENDING --> CLOSED : 用户取消
PAID --> TO_SHIP : 支付完成
TO_SHIP --> SHIPPED : 商家发货
SHIPPED --> DONE : 确认收货
DONE --> AFTER_SALE : 申请售后
AFTER_SALE --> REFUNDED : 退款成功
AFTER_SALE --> DONE : 撤销售后
CLOSED --> [*]
REFUNDED --> [*]
如果你在 Typora 里画完,发现箭头乱成一团,可以先删掉 direction LR,或者把某些状态挪到复合状态里,这个根据实际情况调整。图的目标是清楚,不是完整到每一条流程都平铺出来。
4.4 在 Typora 里从零到交付的工作流
我现在在 Typora 里绘制状态图的标准流程是:
- 先建 md 文件,写上章节标题,比如“订单状态机设计”。
- 在下一行插入 mermaid 代码块,先用最小结构跑通。
- 按表格顺序逐个添加状态节点。
- 每添加一条迁移,就确认一次渲染结果,不要把几十行代码一次性写完再渲染。
- 全部完成后,把状态图前后的描述性文字补全,这样别人阅读时既有说明又有图,不会面对一个干巴巴的离线条。
这个流程的本质是“小步快跑”,每步都验证。一次性写 100 行代码然后发现语法解析失败,排查起来很痛苦,尤其是复合状态和并发分支混在一起的时候。
4.5 状态图命名规范小结
最后聊一下命名。状态名用名词,比如“待支付”“已发货”;事件用动词短语,比如“确认收货”“提交订单”。不要在事件里重复状态名。很多人喜欢写“点击支付按钮跳到支付状态”,这种描述放注释里可以,放在箭头标签里就太啰嗦了。状态图是给开发、测试、产品看的,不是给销售看的,标签越精炼越好。
5. 导出、外观调整与 Typora 里的隐形坑
5.1 图片导出与跨平台分享
Typora 渲染出来的 Mermaid 图,可以直接右键复制为图片,粘贴到文档或聊天工具里。右键菜单里有“复制为图片”和“复制为 SVG”两个选项。只发到聊天窗口,PNG 够用;后续要放进画图工具二次修改,SVG 更合适。
导出文档为 HTML 或 PDF 时,Typora 会自动把 Mermaid 图渲染成图片嵌入,不需要手动处理。这是它在导出体验上比其他纯 Markdown 编辑器好的地方。
有一个坑很容易踩:把 md 文件发给同事,对方如果没装 Typora,或者版本太老,mermaid 代码块不会渲染,有的工具会直接把源码原样显示出来。跨工具协作的时候,要么同时导出一份 PDF 或 HTML 版本,要么约定大家用同一版本的 Typora 打开。别指望每个人都愿意为了看图去折腾环境。
5.2 主题颜色与自定义样式
Mermaid 状态图默认是浅色主题加蓝绿配色。如果觉得千篇一律,可以在 mermaid 代码块顶部加一段 init 配置:
mermaid复制%%{init: {"theme": "neutral"}}%%
stateDiagram-v2
[*] --> 待支付
待支付 --> [*]
theme 可以从 default、neutral、dark 里面选。如果你想微调颜色,可以用 base 配合 themeVariables 指定 primaryColor、lineColor 等变量。不过说实话,stateDiagram-v2 下能改的字段有限,效果远不如 Flowchart 灵活。我的建议是:主体内容稳定之后再去调样式,不要在画图过程中反复折腾颜色,否则容易分心。
5.3 解析失败的常见原因
Typora 里状态图不渲染,九成是下面几种情况:
一是代码块语言标记写错,比如写成 mermmaid,或者根本没写成代码块。
二是用了中文标点。箭头那行写成“待支付 → 已支付”,这里的箭头不是 ASCII 的 -->,必须换成英文。曾经有同事因为这个卡了半天,图看着没问题,就是不渲染。
三是 stateDiagram-v2 和 stateDiagram 混用。建议一律用 stateDiagram-v2。老版 stateDiagram 对复合状态里的部分语法支持不完整,而 stateDiagram-v2 是官方推荐的现代写法。
四是缩进问题。Mermaid 对缩进的容忍度比 Python 高,但复合状态内部的子状态如果没有统一缩进,解析器会分不清层级。建议统一用四个空格缩进。
五是全角冒号。迁移标签那里必须用英文冒号 :,不是中文冒号 :。这个错误很隐蔽,因为渲染失败时报错信息往往不太友好,只能靠肉眼排查。
mermaid复制stateDiagram-v2
[*] --> 待支付
待支付 --> 已支付 : 支付成功
上面这个才是对的,把代码里的中文冒号替换成英文冒号即可。
5.4 Typora 版本与 Mermaid 版本的关系
Typora 内置的 Mermaid 引擎会在软件升级时一起更新,所以“同一段 mermaid,在这台电脑能渲染、在另一台不能”,大概率是版本不一致。如果你的 Typora 比较旧,建议优先升级到新版;如果团队成员的 Linux 包版本各不相同,碰到新语法解析失败是常事。
这里有一个很实用的经验:团队内知识库里的状态图,尽量使用最稳定的基础语法子集。越核心的文档,越别用冷门高级语法。stateDiagram-v2 加上状态、迁移、标签、复合状态这几个基础能力,已经能覆盖绝大多数状态机场景。高级语法自己在本地实验可以,别写进要全员共享的架构文档里。
旧版本 Typora 在渲染大图时还有一个问题:状态特别多、箭头特别密的时候,编辑器可能会卡顿。遇到这种情况,把一个大图拆成几个小图,或者把部分细节挪到文字描述里,比单纯调样式实在。
5.5 我最近在用的协作方式
最后分享一个小技巧。我会把状态图 md 文件和状态说明表格放在同一个文档目录,图用 md 文件存,说明表用表格截图或独立 csv 存。每次更新完图,顺手把表格和代码里的状态枚举一起更新。文档、图、代码三处保持一致,才不会出现“图是新的,代码里还是旧状态名”的尴尬。
做设计评审的时候,直接投屏 Typora 页面,点开代码块改节点,屏幕上的图实时变化。这种即时反馈对讨论很有帮助,比“我把图改好,明天发你”高效太多了。
最后还是那句话:画图本身不复杂,复杂的是把业务规则想清楚。状态图最大的价值,不是最终那张图有多漂亮,而是画图的过程逼着把所有分支梳理出来,让状态迁移里的疑难问题在需求阶段就暴露。如果你过去习惯用画图软件一张张画状态图,可以换个思路,试试 Typora 加 Mermaid 的工作流。第一次可能觉得语法别扭,但等你跑通一个完整的订单状态机,基本就回不去了。
