ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用 tldraw 把 Mermaid 流程图变成可运行的 CI/CD 管线:createMermaidDiagram 与自定义 ShapeUtil 实战

用 tldraw 把 Mermaid 流程图变成可运行的 CI/CD 管线:createMermaidDiagram 与自定义 ShapeUtil 实战 用 tldraw 把 Mermaid 流程图变成可运行的 CI/CD 管线createMermaidDiagram 与自定义 ShapeUtil 实战【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw本文基于 tldraw 官方示例仓库中的custom-shape-mermaids例子README.md展开。该示例演示了一个完整闭环把一段 Mermaidflowchart文本导入 tldraw 画布每个顶点渲染成自定义形状custom shape再从画布上的箭头重建依赖图DAG最后把整张图当做一个支持 AND-join 语义、可随机失败、可逐节点重试的动画 CI/CD pipeline 来调度运行。读完本文你将掌握tldraw/mermaid的blueprintRender.mapNodeToRenderSpec定制点、自定义 ShapeUtil 的注册与 props 设计以及用画布图形作为数据源的二次解析思路。示例概述一张图两套身份这个 example 的定位在 README.md 的 frontmatter 中写得很清楚title: Customize Mermaid diagrams、keywords: [mermaid, diagram, custom, pipeline, workflow]对应组件是CustomShapeMermaids.tsxfrontmatter 中的component字段负责把它挂载进 tldraw 示例站点的用例列表。它解决的问题是导入 Mermaid 图时默认渲染方案可能满足不了你的领域需求——这里节点不是普通的矩形、菱形而是携带运行状态的流水线步骤。因此示例做了三件事通过createMermaidDiagram的blueprintRender.mapNodeToRenderSpec回调把解析出的每个 flowchart 顶点映射为一个自定义的flowchart-util形状并把 Mermaid 的节点 id 存进形状 props便于日后回溯导入完成后不是拿着 Mermaid 文本去算依赖关系而是重新扫描画布上的箭头arrow及其绑定binding用extractFlowchartPipelineFromEditor重建流水线图——画布才是唯一事实来源流水线的运行状态保存在一个进程内共享的 tldraw atom中而非文档 store配合每个节点形状上的状态样式与Step n徽章模拟一次 CI/CD 调度。整套交互位于 CustomShapeMermaids.tsx 的TopPanel粘贴一段flowchart/graph文本 → 点Apply workflow生成画布 → 点Run pipeline开始模拟 → 某步随机失败时点击该形状上的Retry从中断处继续。背景知识tldraw/mermaid 与蓝图渲染管线要理解本例的定制点先看导入背后的执行链。createMermaidDiagram(editor, text, options)的实现位于 packages/mermaid/src/createMermaidDiagram.ts典型流程是懒加载 mermaid通过await import(mermaid)动态引入mermaid 是体积很大的 ESM-only 依赖静态导入在 CommonJS 场景会抛ERR_REQUIRE_ESM并在文件中注释说明——这也是 CustomShapeMermaids.tsx 里用await import(tldraw/mermaid)同样懒加载的原因初始化引擎合并内部默认配置MERMAID_CONFIGflowchart/state 布局为nodeSpacing: 80, rankSpacing: 80, padding: 20并把字号放大 1.4 倍以补偿 tldraw 手绘字体的宽度与用户传入的options.mermaidConfig解析与离屏渲染mermaid.parsemermaid.render到-9999px的离屏容器中随后mermaidAPI.getDiagramFromText拿到各图的数据库对象如 flowchart 的FlowDB.getVertices()/getEdges()转换为蓝图blueprint按diagramType分派——flowchart-v2、sequence、state、mindmap各自有*ToBlueprint转换函数产出统一的中间表示DiagramMermaidBlueprint落画布renderBlueprint(editor, blueprint, options.blueprintRender)真正把蓝图节点物化为 tldraw 形状。蓝图相关类型定义在 packages/mermaid/src/blueprint.tsMermaidDiagramKind flowchart | state | sequence | mindmap——蓝图会标明自己来自哪类图MermaidBlueprintNode携带id、x/y/w/h布局信息与kindflowchart 顶点类型、subgraph 等语义键以及可选的 fill/color/dash/size/align 等样式字段MermaidNodeRenderMapper是一个可选钩子(input: { diagramKind, nodeId, kind, node }) MermaidBlueprintNodeRenderSpec | undefined返回undefined即回退到包的默认映射器返回 spec 则完全接管该节点的渲染决策。MermaidBlueprintNodeRenderSpec有两种变体blueprint.tsvariant: geo使用 tldraw 内置 geo 形状geo字段选择矩形/菱形等variant: shape使用任意已注册到 editor 的 shape type含自定义 ShapeUtil其中的props会在 [默认物化流程] 中与布局推导出的尺寸、颜色、标签文本等 props 合并。本示例选择的就是variant: shape。核心定制点mapNodeToRenderSpec 逐个接管顶点示例的映射器实现非常简短位于 mermaidPipelineBlueprint.tsexport const mapNodeToRenderSpec: MermaidNodeRenderMapper function mapNodeToRenderSpec(input) { if (input.diagramKind ! flowchart) return undefined return { variant: shape, type: CUSTOM_SHAPE_TYPE, // flowchart-util props: { fill: solid, color: grey, mermaidNodeId: input.nodeId, // 记住 Mermaid 侧的身份 }, } }要点拆解diagramKind ! flowchart直接返回undefined告诉渲染器这个节点我不管走默认 geo 渲染。映射器是全局的同一个例子若将来支持 sequence/state 图其它图仍可正常渲染variant: shapetype: CUSTOM_SHAPE_TYPE把顶点物化为自定义形状类型。type字符串必须与注册进 editor 的 ShapeUtil 完全一致即createShape({ type })用的那个字符串props.mermaidNodeId是关键的数据桥导入阶段 Mermaid 的input.nodeId与画布上形状的 id 是两套体系把nodeId存进 props之后无论是重建图还是反查节点Retry都能从画布形状 → Mermaid 顶点双向定位返回的 props 是部分覆盖节点最终尺寸w/h、标签富文本flowchart 顶点的文本等布局推导字段仍由渲染器合并补全无需也不应该在 mapper 里手工布局。在 UI 侧接入点是 CustomShapeMermaids.tsxawait createMermaidDiagram(editor, mermaidText, { blueprintRender: { position: { x: 200, y: 400 }, // 蓝图整体落点 centerOnPosition: false, // 不强制居中 mapNodeToRenderSpec, // 逐个顶点接管 }, })blueprintRender.position给出导入内容的锚点centerOnPosition: false表示按解析出的自然布局摆放而非把内容平移到该点居中。自定义形状FlowchartShapeUtil 是怎么写出来的要让type: flowchart-util真正可用需要两步声明 props 类型、实现 ShapeUtil 类。见 customMermaidShapeUtil.tsx。第一步通过模块扩充声明全局形状 propstldraw 用TLGlobalShapePropsMap作为各形状类型 props 的全局索引自定义类型要进 schema就得扩充该接口L29-L47declare module tldraw { export interface TLGlobalShapePropsMap { [CUSTOM_SHAPE_TYPE]: { w: number; h: number fill: string; color: string; dash: string size: string; font: string richText: TLRichText align: string; verticalAlign: string mermaidNodeId: string /** 1-based Kahn layer after import; set in applyPipelineStepIndices from the canvas DAG. */ pipelineStepIndex: number } } }CUSTOM_SHAPE_TYPE flowchart-util定义在 L27。其中两个业务专用字段正是本示例的巧妙之处mermaidNodeId记录来源顶点pipelineStepIndex记录导入后由 DAG 计算出的 Kahn 层号即界面上的 Step n 徽章。普通几何属性尺寸、颜色、描边、字号、富文本与内置形状保持同构便于复用渲染组件。对应的运行时 props 校验器与默认值写在类的静态字段里L52-L82mermaidNodeId: T.string、pipelineStepIndex: T.number其余用T.number/T.string/richTextValidatorgetDefaultProps()提供w:100,h:100,fill:none,color:black,size:m等兜底值保证任何历史文档都能被安全加载。第二步继承 BaseBoxShapeUtil 并渲染 HTML 覆盖层export class FlowchartShapeUtil extends BaseBoxShapeUtilICustomShape { static override type CUSTOM_SHAPE_TYPE static override props: RecordPropsICustomShape { /* 上述校验器 */ } override canEdit() { return false } // 拒绝双击进入编辑态 override component(shape: ICustomShape) { return CustomShapeComponent shape{shape} / } override getIndicatorPath(shape: ICustomShape) { const path new Path2D() path.rect(0, 0, shape.props.w, shape.props.h) // 选中框/悬停高亮用矩形即可 return path } }component渲染的是一个HTMLContainer覆盖层其内容随流水线状态动态变化L99-L155状态来源const pipeline useValue(pipelineStateAtom)订阅全局 atomstatus props.mermaidNodeId ? pipeline.statusByNodeId[props.mermaidNodeId] ?? pending : pending——没有mermaidNodeId的手工形状一律视为pendingStep 徽章props.pipelineStepIndex 0时在右上角渲染div classNameflowchart-util-shape__step-badgeStep {props.pipelineStepIndex}/div样式见 customMermaidShapeUtil.css文本复用RichTextLabel渲染richText字号由LABEL_FONT_SIZESs/m/l/xl → 1.125/1.375/1.625/2乘theme.fontSize得出注释说明这与内置 note/geo 形状的文字放大系数一致Retry 按钮仅当status failed时出现。按钮用onPointerDown{editor.markEventAsHandled}阻止选择工具把点击误判为拖拽L148onClick调用retryPipelineFromNode(props.mermaidNodeId)。视觉状态由 CSS 类flowchart-util-shape--pending/--running/--passed/--failed区分customMermaidShapeUtil.csspending 灰底、running 黄底 深黄描边、passed 浅绿底、failed 浅红底 深红描边运行状态一眼可辨。容器自身设置了pointer-events: all因为 tldraw 的.tl-html-container默认是pointer-events: none需要覆盖后才能让 Retry 按钮可点。图的真相不在文本而在画布箭头这是本例最反直觉、也最值得借鉴的设计流水线的拓扑结构不是从 Mermaid 文本解析出来的而是导入完成后从画布上的箭头及其绑定重建的。对应 CustomShapeMermaids.tsx 的第 [3] 步注释与 pipelineFromEditor.tsexport function extractFlowchartPipelineFromEditor(editor: Editor): ParseFlowchartPipelineResult { const shapes editor.getCurrentPageShapes() const mermaidByShapeId new Mapstring, string() for (const s of shapes) { if (s.type ! CUSTOM_SHAPE_TYPE) continue const mid (s as ICustomShape).props.mermaidNodeId if (mid) mermaidByShapeId.set(s.id, mid) } // 遍历 arrow用 getBindingsInvolvingShape 找到箭头的两端绑定 // b.props.terminal start/end 决定哪端是起点、哪端是终点 // 通过 mermaidByShapeId 把 shapeId 翻译回 mermaidNodeId产出 [a, b] 边 // 同一对顶点可能有多条箭头用 Setstring 按 ${a}-${b} 去重 return flowchartPipelineFromEdges(pairs) }为什么绕开文本因为用户可以在画布上手改拓扑把s2 - s3的箭头手动重连到别的节点再点一次 Apply新的 pipeline 就按改动后的画布执行。用代码里的注释说就是if you reconnect arrows by hand and apply again the pipeline follows the drawingCustomShapeMermaids.tsx。实现细节上借助editor.getBindingsInvolvingShape(s.id, arrow)拿到与该箭头关联的TLArrowBinding再按terminal属性区分start/end两端目标——这是读取 tldraw 箭头连接关系的标准做法。重建成功后applyPipelineStepIndices(editor, stepIndexByNodeId)pipelineFromEditor.ts会把每个顶点算出的层号回写为props.pipelineStepIndex先在editor.run()事务里批量updateShape层号为 0 的节点不显示徽章。必须是 DAGKahn 分层算法与错误处理图的校验与分层实现在 pipelineGraph.tspredecessorsFromEdgesL15-L26把边表转成每个节点的前驱列表供调度器查询依赖是否满足dagLayersFromEdgesL31-L71是标准Kahn 拓扑分层统计入度 → 反复取出当前入度为 0 的源点集合作为第layer层 → 去掉它们的出边 → 层号 1 继续。若某轮sources.length 0而仍有剩余节点说明存在环返回错误三个结果分支L10-L12给出了面向用户的错误文案没有任何可用边No pipeline edges found between steps. Add arrows between nodes, e.g. a -- b -- c.存在环The flowchart has a cycle. This demo only supports directed acyclic graphs.成功返回nodeIds排序后的顶点集合、edges与stepIndexByNodeId。Step n 徽章 Kahn 层号1-based而不是全局执行序号同层的节点编号相同如并行分支 Unit tests 与 Integration tests 都是 Step 2。这正是 README 中the Step n badges are Kahn layers, so nodes in the same layer share a number的来历。每层内部按节点 id 排序sources.sort()保证结果确定性。调度引擎共享 atom AND-join 随机失败重试流水线状态与调度逻辑集中在 mermaidPipelineState.ts。为什么用页面级共享 atom而不是组件 stateStepStatus pending | running | passed | failed整个状态为export interface PipelineState { nodeIds: string[] edges: [string, string][] statusByNodeId: Recordstring, StepStatus parseError: string | null isRunning: boolean }它被定义在模块顶层的全局 atomexport const pipelineStateAtom atomPipelineState(mermaidPipelineExample, { nodeIds: [], edges: [], ... })代码注释解释了原因画布上每个flowchart-util形状组件都会useValue(pipelineStateAtom)订阅同一个 atom如果按组件各自useAtom每次挂载都会得到一份独立状态节点之间就互相看不见了。全局单例让 TopPanel、所有节点、Retry 按钮共享一份实时状态。README 也强调状态只存在于内存中不写入文档 store刷新或切换页面即清零。AND-join 就绪判定getReadyNodeIdsL51-L62实现严格的 AND-join一个节点只有当自己还是pending、且所有前驱都已passed时才可运行。这带来 README 描述的语义merge nodes run after all incoming steps pass——分支允许汇合必须等齐。模拟执行与失败注入FAIL_PROBABILITY 0.2每个步骤有 20% 概率模拟失败STEP_DELAY_MS_MIN 800、STEP_DELAY_MS_EXTRA 400每步耗时 800–1200ms 随机营造动画流水线观感simulateStepL37-L49先把该节点置为running延时后随机置为passed/faileddrainPipelineStepsL67-L90循环取就绪层依次模拟有 pending 无 ready 时说明被失败阻塞退出循环等待 RetryrunFullPipelineL93-L107把全部节点重置为pending后启动drainPipelineSteps期间用isRunning防止并发触发TopPanel 的 Run 按钮在运行时被禁用retryPipelineFromNode(nodeId)L110-L125只对failed节点有效单独重跑该步若成功则继续drainPipelineSteps从图中断处往后调度——对应 README 的click Retry on a failed shape to resume from there。前端接线与完整操作流程校验输入源flowchartSourceGuard.ts 只放行flowchart/graph开头的图逐行跳过空行与%%注释若首个有效行匹配/^(flowchart|graph)\b/i则通过否则返回提示Use a flowchart or graph diagram only…全部为空则返回Diagram source is empty.。此守卫发生在createMermaidDiagram之前属于廉价的预检。Apply workflow 的状态机CustomShapeMermaids.tsx 中applyWorkflow的完整顺序是预检失败 → 置parseError直接返回清空当前页面所有形状editor.deleteShapes([...editor.getCurrentPageShapeIds()])动态import(tldraw/mermaid)避免页面首屏引入体积庞大的 mermaid 库调用createMermaidDiagram(editor, text, { blueprintRender: { position: {x:200,y:400}, centerOnPosition:false, mapNodeToRenderSpec } })抛错则统一提示An error occurred; please make sure your diagram is valid.extractFlowchartPipelineFromEditor(editor)重建 DAG失败无边/成环则展示相应错误并清空状态成功则把 atom 状态置为全pending并用applyPipelineStepIndices回写 Kahn 层徽章editor.selectNone()收尾。TopPanel 底部的两个按钮与运行提示L140-L151控制整体交互Apply 期间按钮显示 Applying…Run 的可用条件是!pipeline.parseError pipeline.nodeIds.length 0 !pipeline.isRunning !isApplying运行中显示Running simulated steps…。示例自带的默认文本DEFAULT_MERMAID就是一条典型的 CI/CD 分支合流图flowchart LR s1[Checkout] -- s2[Build] s2 -- s3[Unit tests] s2 -- s4[Integration tests] s3 -- s5[Deploy] s4 -- s5上手体验路径运行 tldraw 的 examples 应用进入Customize Mermaid diagrams用例在文本框中粘贴任意flowchart/graph可含分支与合流不可含环点击Apply workflow——画布上出现一排排带flowchart-util样式的节点与箭头节点右上角标着 Step 层号点击Run pipeline——节点逐个进入running黄状态通过者变绿约 20% 概率随机失败变红点失败节点上的Retry——该步重跑通过后调度从断点继续直至全部passed。限制与可扩展方向该 example 是概念验证式演示README 与其代码注释均明确列出的边界如下实际借鉴时应清楚这些前提仅接受flowchart/graph文本flowchartSourceGuard会把其它类型sequence、stateDiagram、mindmap 等挡在门外图必须是 DAG存在环会直接给出错误文案不会尝试破环或拓扑兜底图结构来源于画布而非文本想改拓扑去拖动/重连画布上的箭头比改文本更权威状态仅存于内存pipelineStateAtom是页面级共享 atom刷新即重置也不会随文档持久化失败是随机注入simulateStep用Math.random()模拟步骤结果。正因如此这个骨架非常适合按需替换为真实逻辑把simulateStep换成对真实 CI 系统的请求、把状态从 atom 升级为 store 内文档字段以获得持久化与多人协作、把mapNodeToRenderSpec扩展到按kind如不同顶点类型分发到更多自定义形状——例如把每次Build都渲染成带进度条的专属节点。作为 tldraw 自定义形状 Mermaid 蓝图定制点的完整参考这个 example 把导入、映射、图重建、状态驱动的形状渲染四个环节打通值得在涉及把外部图数据变成可交互领域模型的场景中反复对照。【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表