图示
SequenceFigure - 时序图
一条随时间展开的调用链:谁问谁、按什么顺序、又回来了什么。
用法
什么时候用它
来自@misoto22/folio/diagrams
import { SequenceFigure } from '@misoto22/folio/diagrams'说明
一条随时间展开的调用链:谁问谁、按什么顺序、又回来了什么。
这是这里唯一一张纵轴有含义的图。其他每张图的 y 都是布局;时序图的 y 是时间——所以消息带的是显式的 y 而不是序号:相隔 8 个单位的两次调用是同时发生的,相隔 200 个单位的不是;把消息等距排开会一边显得更整齐、一边把这个区别抹掉。
生命线是细线而不是实线,因为它们是坐标轴而不是内容。一条画得跟消息一样重的生命线,会让七条竖线和十二条横线互相抢戏。
激活条说的是谁正忙着,这正是一张时序图能承载、而一份调用清单承载不了的事实——第三个参与者的条和第二个重叠,才是画它们的理由。
区段把纵轴分成几个阶段——请求、回退、响应——印成一条线加一行等宽说明,而不是一块着色的面板,理由和架构图的边界一样:图里的第二层地面会压在每一条消息标签的遮罩下面。
return 消息既是虚线又用空心箭头。两个信号而不是一个,因为在一条密集的调用链里,回包是读者最常需要挑出来的东西,而单靠虚线,做的已经是 dashed 在异步调用上做过的那份工作。
结构
| 部件 | 说明 |
|---|---|
| 图外框必填 | 画面外面的那个框。一条调用链交给它的是一份消息列表,而这份列表是按 messages 数组书写的顺序发布的——那也是看不见纵轴的读者能拿到的唯一顺序。 |
| 参与者头必填 | 每个参与者在自己那一列顶上有一块 148 单位宽的板块,顺序由 participants 数组给定。meta.column_fit: "spread" 会把每一列都撑到最宽的那个名字,而不是守着固定宽度。 |
| 生命线必填 | 从每个头往下拉到轴上最后一样东西下方的一根细线。是细线而不是普通线,因为它就是那根轴:七条按消息线重画的生命线,就是七条竖线在跟十二条横线抢。 |
| 消息必填 | 在自己的 y 上的一条横线,两端都从生命线往里缩,让箭头落在轴之外,文字压在它上方的一块遮罩上。返回消息既是虚线、又用空心箭头——两个信号,因为读者要从一条密集的调用链里挑出来的正是回包。 |
| 激活条 | 生命线上从一个 y 到另一个 y 的一根窄条:谁正忙着。这是时序图承载、而一份调用清单承载不了的事实;两根条彼此重叠,正是这张图值得画出来的全部理由。 |
| 分段带 | 一条横贯全图的虚线,配一个落在遮罩块上的等宽小标题——request、fallback、response。是一个小标题而不是一块着色面板,因为图里再来一层底色,就会压在每一条消息标签的遮罩底下。 |
实践建议
推荐
- 消息要按 y 从小到大写。画面读的是纵轴,而旁边那份摘要列表遍历的是数组,所以一条顺序写错的消息,画在这次交互的一个位置上,念出来却在另一个位置。
- 参与者的名字比固定列宽还长时,就用 meta.column_fit: "spread"。板块会折成两行,仍然放不下的部分打省略号;而把名字改短来迁就宽度不是修复——名字就是数据本身。
- 消息之间的间距要按实际发生的情况给。没有任何东西会把 y 归一化,所以两次调用相隔八个单位读作同时发生、相隔两百个单位读作一段等待;均匀排开的列表是整齐些,但它说出了这条调用链没说的事。
避免
- 点名了一个 participants 没有声明的参与者的消息,会连同摘要列表一起消失——没有两列可以让它画在中间,而一份文字等价物若报告了画面根本展示不了的调用,就是同一张图的两半在互相打架。开发环境会打印 DIAGRAM_EDGE_DANGLING 并带上两端,那也是这条消息唯一被报出来的地方。
- 这张图是在断言顺序。一个服务并发发出的三次调用,因为总得给个 y 而被给了三个 y,画出来就是在声称第二次等了第一次——「这几件事是重叠的」该说的地方,是激活条或者分段小标题。
示例
default
一次缓存未命中,顺着页面往下读,而竖轴是时间、不是布局:未命中和查询之间那 50 个单位是一段真实的间隔,而一份六次调用等距排开的列表,会把它抹掉,同时看起来还更整齐。那些激活条承载的是一份调用列表承载不了的事实——在 API 保持打开的那 210 个单位里,Redis 忙了 45 个单位。返回用虚线、配空心箭头,两个信号而不是一个,因为在一条密集的调用轨迹里,读者最先挑出来的就是回复。
A cache miss
4 elements and 6 relationships. The vertical axis is time, not layout.
- Web app (frontend) — React
- API (backend) — handler
- Redis (database)
- Postgres (database)
- Web app → API: GET /me
- API → Redis: read
- Redis → API: miss
- API → Postgres: select
- Postgres → API: rows
- API → Web app: 200 JSON
- Phase Request covers GET /me, read, miss.
- Phase Fallback covers select, rows, 200 JSON.
- API is busy from GET /me to 200 JSON.
- Redis is busy from read to miss.
- Postgres is busy from select to rows.
an asynchronous hop
一次投递中异步的那一半。入队是虚线,因为 Billing 不等它——Billing 压根没有激活条,而正是这一点说明随后那两次尝试对调用方不花任何代价。两次对外调用都带着 security 变体,图例把它读作跨越信任边界,而在这里这是真的:那个端点属于客户。column_fit 用的是 spread,因为「Partner webhook endpoint」装不进固定的 148 单位列宽,而为了塞得下去把名字缩短,那会是另一张图了。
Webhook delivery
3 elements and 5 relationships. A queue absorbs the retry the caller never waits for.
- Billing (backend) — invoice writer
- Outbox (messagebus) — delivery queue
- Partner webhook endpoint (external) — customer-operated
- Billing → Outbox: enqueue
- Outbox → Partner webhook endpoint: POST /hooks
- Partner webhook endpoint → Outbox: 503
- Outbox → Partner webhook endpoint: attempt 2
- Partner webhook endpoint → Outbox: 200
- Phase Retry covers attempt 2, 200.
- Outbox is busy from enqueue to 200.
time as the axis
一个显式的 y 买到了什么。前三次调用相隔 12 个单位,因为它们是一起离开网关的;而供应商的回复在它那次调用下方 186 个单位处,因为它就是花了这么久——换成按下标摆放,这六条消息会等距排开,那个差别也就没了。那些激活条用第二种方式承载同一个事实:两条早早收尾,而第三条一路跑到图的底部,那就是长尾,在任何人读到一个标签之前就已经看见了。
A slow dependency
4 elements and 6 relationships. Three calls in 24 units, then 186 of waiting.
- Gateway (backend)
- Search (backend)
- Ranker (backend)
- Pricing vendor (external)
- Gateway → Search: query
- Gateway → Ranker: features
- Gateway → Pricing vendor: quote
- Search → Gateway: hits
- Ranker → Gateway: scores
- Pricing vendor → Gateway: price
- Gateway is busy from query to price.
- Search is busy from features to hits.
- Ranker is busy from quote to scores.
- Pricing vendor is busy from hits to price.
- Search and Ranker both answer within 75 units of the first call.
- The vendor holds its request open for 186.
names that do not fit
column_fit 用 spread,它会把每一列都撑到最宽的那个参与者名字,而不是守着那个固定值。只要一个真实的名字会被裁掉,就立刻去拿它:板子会折成两行、其余部分打省略号,而为了塞得下去把名字缩短不是一种修复——名字就是数据,dealer-portal-api 和 api 是两个不同的服务。一条点名了 participants 里没有声明的参与者的消息,根本不会被画出来,因为没有列可以让它画在中间;它会以原始 id 的形式留在摘要列表里,于是文字报出了一次画面没有展示的调用。
Signing in to the dealer portal
3 elements and 4 relationships. Every column as wide as the longest real name.
- Dealer portal (browser) (frontend) — Next.js
- dealer-portal-api (backend) — Django + DRF
- SQL Server 2025 (database) — legacy schema
- Dealer portal (browser) → dealer-portal-api: POST /api/v1/token/
- dealer-portal-api → SQL Server 2025: SELECT dealer WHERE email = ?
- SQL Server 2025 → dealer-portal-api: one row
- dealer-portal-api → Dealer portal (browser): access + refresh
- dealer-portal-api is busy from SELECT dealer WHERE email = ? to access + refresh.
- SQL Server 2025 is busy from SELECT dealer WHERE email = ? to one row.
无障碍
- 返回消息既是虚线、又用空心箭头——两个信号,因为在一条密集的调用链里,读者最常要挑出来的就是回包。
- 图旁边的消息列表按顺序读,那就是纵轴的绘制顺序。