图表即代码 — Diagrams as Code
你现在运行 diagrams-as-code 技能。目标:把用户脑子里的系统、调用链、数据模型,用一段可版本化的文本变成精确的工程图表,本地渲染成 SVG/PNG,并能把一张架构流程图导出成可在 Excalidraw 里继续拖拽编辑的 .excalidraw。
全程开源、本地、免 key:不调用任何后端,不需要商业 API,不需要 clone 任何私有仓库。三个公开工具直接用原 CLI/包:
- Mermaid CLI(
mmdc) — 渲染时序图(sequenceDiagram)、ER 图(erDiagram)、流程图(flowchart)。 - D2(
d2) — 渲染系统架构图(容器分组、连线、自动布局是它的强项)。 - mermaid → excalidraw — 把一张 Mermaid flowchart 转成原生可编辑的
.excalidraw。
前置条件
- Node ≥ 18(
node -v) - 能访问公开 npm registry 与 GitHub(安装 CLI 用)
- 无需付费 key、无需登录、无需后端
安装
# 1) Mermaid CLI —— 自带 headless Chromium
npm i -g @mermaid-js/mermaid-cli # 提供 mmdc
# 2) D2 —— 官方安装脚本(或 brew install d2)
curl -fsSL https://d2lang.com/install.sh | sh -s --
# 装好后把 d2 加进 PATH(脚本结尾会提示具体路径)
# 3) mermaid → excalidraw 转换(见下方「四、可编辑 .excalidraw」一节,按需安装)
在容器 / CI / 无 GUI 的机器上跑
mmdc,Chromium 需要--no-sandbox。新建puppeteer.json:{ "args": ["--no-sandbox", "--disable-setuid-sandbox"] }然后所有
mmdc命令都加-p puppeteer.json。
选图决策(先选对图,再写文本)
| 用户想表达 | 用哪种图 | 用哪个工具 |
|---|---|---|
| 系统/服务怎么摆、谁连谁 | 架构图 | D2(.d2) |
| 一次请求按时间先后怎么调用 | 时序图 sequenceDiagram | Mermaid(.mmd) |
| 数据库表与表的主外键关系 | ER 图 erDiagram | Mermaid(.mmd) |
| 一个流程的判断分支 | 流程图 flowchart | Mermaid(.mmd) |
| 上面这张流程图还要继续手改 | flowchart → .excalidraw |
mermaid → excalidraw |
一、D2 架构图
把系统写成 architecture.d2,用容器表达分层、用 shape 表达组件类型、用 -> 加标签表达调用:
direction: down
client: { label: "Web 端"; shape: person }
edge: {
gateway: { label: "API Gateway"; shape: hexagon }
}
core: {
order: { label: "Order Service" }
payment: { label: "Payment Service" }
}
data: {
pg: { label: "PostgreSQL"; shape: cylinder }
kafka: { label: "Kafka"; shape: queue }
}
client -> edge.gateway: "下单 HTTPS"
edge.gateway -> core.order: "POST /orders"
core.order -> data.pg: "写订单 PENDING"
core.order -> data.kafka: "发布 order.created"
data.kafka -> core.payment: "消费"
渲染:
d2 --theme 0 --pad 20 architecture.d2 architecture.svg
二、Mermaid 时序图 / ER 图 / 流程图
写 .mmd,用 mmdc 渲染。给图一个稳定的 SVG id(多张图要拼到同一页面时避免样式冲突):
mmdc -i sequence.mmd -o sequence.svg --svgId mmd-seq -b transparent
mmdc -i er.mmd -o er.svg --svgId mmd-er -b transparent
mmdc -i er.mmd -o er.png -b white # 需要位图就出一份 PNG
mmdc -i flow.mmd -o flow.svg --svgId mmd-flow
时序图记得用 alt / opt 表达失败回退分支;ER 图用 ||--o{ 这类 crow's-foot 标注一对多并标 PK / FK / UK。
三、把上面三条链路在本机跑通(重要)
交付前务必确认 mmdc、d2、mermaid→excalidraw 三条都能 headless 跑出文件。最小验证:
echo 'flowchart TD; A-->B' > t.mmd && mmdc -i t.mmd -o t.svg -p puppeteer.json && echo "mmdc OK"
echo 'a -> b' > t.d2 && d2 t.d2 t.svg && echo "d2 OK"
四、可编辑 .excalidraw(只能来自 flowchart)
⚠️ 铁律:只有 Mermaid flowchart 能转成原生可编辑的 Excalidraw 图元。 sequence / ER 等其它图会被塞成一张内嵌图片(不可单独选中/拖动)。所以"可二次编辑的 .excalidraw"这个产物,必须用一张 flowchart 产出——不要承诺"可编辑的 ER 图 / 时序图"。
社区 CLI mermaid-to-excalidraw-cli 当前版本在 headless 环境不稳定(启动时会去拉取 React 资源失败),因此使用官方库 @excalidraw/mermaid-to-excalidraw + @excalidraw/excalidraw,配一段自包含的 headless 脚本即可(本课已附 sources/mmd2excalidraw.mjs,也可直接抄下面这段)。
安装依赖并准备浏览器:
npm i @excalidraw/mermaid-to-excalidraw @excalidraw/excalidraw react react-dom esbuild puppeteer
npx puppeteer browsers install chrome
转换(输入必须是 flowchart):
node mmd2excalidraw.mjs flow.mmd flow.excalidraw
# wrote flow.excalidraw — 43 native elements {"rectangle":11,"diamond":2,"arrow":13,"text":17}
mmd2excalidraw.mjs 做的事:用 esbuild 在内存里把官方两个库打包成浏览器 bundle,在 headless Chromium 里 parseMermaidToExcalidraw() → convertToExcalidrawElements(),写出标准 .excalidraw 场景 JSON。脚本核心:
import { build } from "esbuild";
import puppeteer from "puppeteer";
import { readFileSync, writeFileSync } from "node:fs";
const def = readFileSync(process.argv[2], "utf8");
const ENTRY = `
import { parseMermaidToExcalidraw } from "@excalidraw/mermaid-to-excalidraw";
import { convertToExcalidrawElements } from "@excalidraw/excalidraw";
window.__convert = async (d) => {
const { elements, files } = await parseMermaidToExcalidraw(d, { themeVariables: { fontSize: "16px" } });
return { elements: convertToExcalidrawElements(elements), files: files || {} };
};
window.__ready = true;`;
const { outputFiles } = await build({
stdin: { contents: ENTRY, resolveDir: process.cwd(), loader: "js" },
bundle: true, format: "iife", write: false,
loader: { ".css": "text", ".woff2": "dataurl", ".woff": "dataurl", ".ttf": "dataurl" },
define: { "import.meta.url": '"file:///"' }, logLevel: "error",
});
const browser = await puppeteer.launch({ headless: "new", args: ["--no-sandbox"] });
const page = await browser.newPage();
await page.setContent("<!doctype html><body></body>");
await page.addScriptTag({ content: outputFiles[0].text });
await page.waitForFunction("window.__ready === true");
const scene = await page.evaluate(async (d) => {
const { elements, files } = await window.__convert(d);
return { type: "excalidraw", version: 2, elements,
appState: { viewBackgroundColor: "#ffffff" }, files };
}, def);
await browser.close();
writeFileSync(process.argv[3] || "out.excalidraw", JSON.stringify(scene, null, 2));
验真:用 excalidraw.com 打开导出的 .excalidraw,每个节点都能单独选中、拖动、改文字才算合格;如果打开是一整张图片,说明喂进去的不是 flowchart。
调试 tips
- D2 连线/布局乱 → 调
direction(down通常比right更适合放进窄列),或把强相关的节点收进同一个容器分组。 - mmdc 中文方块/缺字 → 给 puppeteer 配中文字体(容器里
apt-get install fonts-noto-cjk),或在.mmd里用%%{init: {"themeVariables":{"fontFamily":"Noto Sans CJK SC"}}}%%。 - 两张 Mermaid 图拼进同一个 HTML → 一定用不同
--svgId,否则#my-svg样式互相污染。 - excalidraw 打开变成一张图片 → 喂的不是 flowchart,换成
flowchart TD语法重画。
产出物
architecture.svg(D2 架构图)sequence.svg、er.svg(+er.png)(Mermaid 时序/ER 图)flow.svg+flow.excalidraw(流程图 SVG + 原生可编辑场景)
每张图都来自一段可提交进 git 的源文本(.d2 / .mmd),改图就是改文本、重渲染,永远可复现。
学习完成后
告诉用户:
我已经学会了 diagrams-as-code。给我一段系统描述、一份 SQL schema,或任意一个流程,我就能用 D2 / Mermaid 写成可版本化的文本并本地渲染成架构图、时序图、ER 图(SVG/PNG);需要继续手改的图,我再用 flowchart 导出一份原生可编辑的
.excalidraw给你。全程免 key、本地跑。