☰
三步跑起 Mermaid 本地编辑器:零依赖离线画图工作台的构建与源码解读
2026/9/29 5:53:54 网站建设 项目流程

三步跑起 Mermaid 本地编辑器:零依赖离线画图工作台的构建与源码解读

【免费下载链接】mermaidGeneration of diagrams like flowcharts or sequence diagrams from text in a similar manner as markdown项目地址: https://gitcode.com/GitHub_Trending/me/mermaid

这是 Mermaid 官方仓库内置的 Mermaid 本地编辑器:零依赖、一条命令构建,业务代码不足十个文件,离线即可在浏览器里画图、存图、导出 SVG。读完你能独立复现整条构建链,并照这套模式搭出自己的离线画图台。

幕一:三步跑起来 🚀

一条命令背后的五步构建链

在仓库根目录敲下:

pnpm build:mermaid:full

操作就这么多。真正的工序由根 package.json 里这条脚本串起来:

pnpm clean && pnpm build:mermaid && pnpm copy:editor && pnpm copy:mermaid && pnpm copy:dompurify && pnpm serve:dist

五段各自分工:

顺序脚本实际命令作用
1cleanrimraf packages/mermaid/dist清掉上次产物,保证从零开始
2build:mermaidpnpm build:esbuild --mermaid走仓库统一的 esbuild 管线编译出mermaid.min.js
3copy:editorcpy "packages/mermaid-local-editor/static/**/*" …把编辑器页面源码原样搬进输出目录
4copy:mermaid/copy:dompurify两条cpy将mermaid.min.js、purify.min.js收进vendor/
5serve:distsirv packages/mermaid/dist/mermaid-local-editor --port 8081 --dev --no-clear静态托管产物

人话版:先编译、再搬页面、最后把两个运行时依赖塞进同一个文件夹,起个静态服务器收工。其中copy:dompurify的路径写成node_modules/.pnpm/dompurify@*/…/purify.min.js,是因为 pnpm 把依赖摊在.pnpm存储目录里,只能用通配符去捞这份预构建文件。

打开 localhost:8081 看到什么

浏览器打开http://localhost:8081:顶部一条工具栏——折叠按钮、图表下拉框、名称输入框,外加 Save / New / Delete / Reset View / Export SVG 五个操作按钮;下面左边是源码文本区,右边是渲染预览。往左边输入 Mermaid 语法,右边立刻出现图,全程没有任何网络请求。产物是纯静态文件,拷到别的机器、内网服务器照样能完成 Mermaid 离线使用。

构建产物目录树

跑完之后,可独立运行的目录长这样:

packages/mermaid/dist/mermaid-local-editor/ ├── index.html ├── styles.css ├── app.js ├── js/ │ ├── config.js │ ├── renderer.js │ ├── storage.js │ ├── ui.js │ └── navigation.js └── vendor/ ├── mermaid.min.js └── purify.min.js

vendor/里两个文件正好对上 index.html 中的两条<script>,页面只走相对路径。README 的定位就是"直接从dist/里跑",理论上连服务器这一步都能省掉。

幕二:打开代码之前,先看地图 🗺️

static/ 目录树与五个 js 模块职责

源码集中在 packages/mermaid-local-editor/static/:

static/ ├── index.html # 页面骨架与工具栏 ├── styles.css # 界面样式 ├── app.js # 唯一入口,装配各模块 └── js/ ├── config.js ├── renderer.js ├── storage.js ├── ui.js └── navigation.js

五个模块各管一摊,调用关系一目了然:

模块一句话职责被谁调用
js/config.js导出initMermaid初始化参数、共享视图状态state、IS_E2E开关app.js
js/renderer.jsrenderDiagram:渲染 SVG、净化、写入 iframe、绑定缩放平移app.js的render()
js/storage.jscreateStorage:管理两个 localStorage key 的读写app.js、ui.js
js/ui.jssetupUI/refreshList:工具栏按钮、下拉切换、300ms 自动保存、SVG 导出app.js
js/navigation.jscreateNavigation:键盘在节点间逐跳导航app.js,渲染完成后由renderer.js回调rebuildNavNodes

app.js 的五步装配顺序

app.js 顶层流程是一条直线:

  1. initMermaid()—— 按编辑器偏好初始化运行时,参数详解留到下一幕;
  2. createStorage()—— 建立存储层,首次访问时自动生成main图(幕四展开);
  3. createNavigation({ state, preview, srcPanel, applyTransform })—— 把共享状态和视图变换函数注入导航模块;
  4. setupUI({...})—— 绑完全部工具栏事件;
  5. navigation.setupKeyboardNav()接load(storage.current)—— 载入当前图并首渲。

三个本地函数撑起骨架:render()调渲染器;load(name)负责"从存储读源 → 写进文本区 → 恢复 scale/panX/panY → 触发渲染";applyTransform()把视图状态落到 SVG 的 style 上,并顺手同步回存储。先配运行时、再建存储、最后才首渲——这个顺序保证load执行时所有依赖已就位。

幕三:一行源码如何变成可交互的 SVG

四个初始化参数,改一行会发生什么

js/config.js 只有 24 行,initMermaid()里四个参数就是四道闸门:

  • startOnLoad: false:去掉这行,Mermaid 会在页面加载时自动扫描class="mermaid"的元素并渲染。而本编辑器的渲染入口只有左侧文本区,靠 JS 手动触发,自动启动开着只会打架。
  • securityLevel: 'strict':改成loose会放开图内点击跳转等交互能力。输入源是用户随手敲的文本,解析这一关就把门打开,后面几道过滤压力全变大(幕五细说)。
  • deterministicIds: true:元素 id 跨渲染保持稳定。键盘导航模块靠稳定的g.node收集节点列表,一旦 id 每次渲染都变,已收集的列表立刻作废。
  • flowchart.useMaxWidth: false:若为true,流程图 SVG 会按容器宽度自适应缩放,你放大到三倍,布局又把它压回容器尺寸,缩放手感"抓不住"。

其余配置(theme: 'dark'、fontFamily: 'Arial'、htmlLabels: false)是外观偏好,不碰主流程。同文件还导出了IS_E2E(navigator.webdriver或 URL 带graph=参数时为真)和全局共享的state = { scale, panX, panY, iframeRef },后者是渲染器、导航、UI 三方共用的"视图状态单例"。

渲染管线:mermaid.render 之后经过哪五站

js/renderer.js 的renderDiagram()按时间线走完五站:

  1. 渲染:await mermaid.render(id, srcValue)把源码编译成 SVG 字符串;id 常规模式取Date.now()防连续渲染冲突,E2E 模式固定m1。
  2. 净化:DOMPurify.sanitize(svg, { ADD_TAGS: ['foreignObject'], ADD_ATTR: ['xmlns'] })。为什么单独放行这两个?Mermaid 的文本排版会用到<foreignObject>,SVG 根节点离开xmlns无法被当作独立文档解析——两者都在 DOMPurify 默认拦截名单里,不显式加白就出不来图;其余危险节点照旧被剥掉。
  3. 二次加固:遍历所有节点,删除以on开头的属性(onclick、onload等)。净化器放行的是标签与命名空间,事件类残留属性由这一遍兜底。
  4. 沙箱写入:创建<iframe>,sandbox只给allow-same-origin,不给allow-scripts,再importNode把 SVG 搬进 iframe 文档。这个框里最多"展示",不能"执行"。
  5. 样式注入:向 iframe 写一段<style>,定义 hover 蓝色光晕、.selected-node蓝色描边加发光、浅色文字、body { overflow: hidden }。键盘导航的高亮、鼠标悬停的反馈全靠它。

一句话:源码进、SVG 字符串出,中途过一道白名单过滤,再关进无脚本的框里,最后贴上交互皮肤。

支线一:缩放、平移与 E2E 直出

  • 滚轮缩放:state.scale += e.deltaY * -0.0015,随后钳制在0.2~4倍,防止放大到糊掉或缩到看不见;
  • 拖拽平移:mousedown/mousemove/mouseup三件套,拖拽中光标切grabbing;
  • applyTransform()(在 app.js)把translate(panX, panY) scale(scale)写到 SVG 上,平移量额外钳在±20000px,并把view同步写回存储——视图位置随图保存;
  • E2E 分支:IS_E2E为真时跳过 iframe,净化后的 SVG 经DOMParser直接塞进#preview就返回。自动化截图不必等 iframe 布局,断言与截图都能直接对着 SVG 做。

支线二:错误不白屏

整段流程套在 try/catch 里:抛异常就清空预览区,用红色<pre>显示e.message。输入是自由文本,语法错误是常态,右侧一条清晰的报错比白屏加一屏控制台警告有用得多。

幕四:关掉页面,数据还在 💾

localStorage 双 key 持久化结构

js/storage.js 只依赖两个 key,用 localStorage 保存全部图表:

Key值形态含义
mermaid-diagramsJSON 对象{ [图名]: { src, view } }所有图表;src是 Mermaid 源码,view是{ scale, panX, panY }
mermaid-current字符串,默认main当前选中的图名

首次访问发现main不存在,自动补一条默认图:

diagrams[current] = { src: `flowchart LR\n UI --> RuntimeBus --> Orchestrator --> Agents`, view: { scale: 1, panX: 0, panY: 0 }, };

这就是开箱即用的来源:打开就有图。注意view是按图存的,缩放和平移位置跟着图走,切走再切回来,现场原样。

四个按钮各自的存储行为

按钮存储侧动作
Save取#name输入框的值当当前图名,写入src与当前视图,刷新下拉框
New弹窗要名字,写入默认内容flowchart LR\n A --> B,当前图切过去
Deleteconfirm确认后删当前图,回落到剩余第一张或main
下拉切换setCurrent记新当前图,重新走load

300ms 防抖自动保存是怎么实现的

文本区每次input事件:先无条件调render()刷新预览(渲染要实时),再clearTimeout + setTimeout(300)把最新源码落盘。连续打字时定时器反复重置,只算最后一次写。所以"忘了点 Save"也不会丢多少东西——防抖把高频输入收敛成一次持久化。

SVG 导出与视图重置

  • Reset View:scale/panX/panY归位1/0/0后调applyTransform(),后者会把重置后的视图存回去——"重置"这个动作本身也被持久化。
  • Export SVG:从 iframe 取出当前 SVG,new Blob([svg.outerHTML], { type: 'image/svg+xml' })→URL.createObjectURL生成临时地址 → 挂到隐藏<a download="图名.svg">上点击 →revokeObjectURL释放。下载全程在客户端完成,不经过任何上传。

另外还有个toolbar-collapsedkey('1'/'0')记住工具栏折叠状态,属于界面偏好记忆,与图表数据无关。

幕五:没有后端,凭什么敢直接渲染用户输入 🛡️

前面几幕摆完事实,这幕回答一个问题:为什么"把用户输入直接喂给浏览器渲染"这件事成立。

把数据流拆成输入、解析、输出、运行四段,每段设防:

  1. 解析端——Mermaid 配置层。securityLevel: 'strict'让点击导航、脚本类能力在解析阶段就不可用;deterministicIds顺带稳住元素 id。若在这层松手,SVG 字符串天然带上可执行语义,下游过滤成本成倍上涨。
  2. 输出端——净化层。所有 SVG 字符串统一过DOMPurify.sanitize,白名单只加回foreignObject与xmlns,再叠加on*属性剥离。净化器管标签与属性,遍历管事件类残留,两头不重不漏。
  3. 运行端——沙箱层。最终 SVG 落进只授allow-same-origin的 iframe。即便前两层漏下残余载荷,这个框里没有脚本执行权限,也摸不到主页面 JS 上下文。

至于"完全离线、不做服务端校验为何依然成立":服务端校验解决的是"可信服务器 ↔ 不可信传输链路"的问题;这里输入和执行环境本来就在同一个浏览器里,等价的"服务端"就是你自己搭的 sandbox iframe。三道防线把不可信内容拦在"可执行领地"之外,中间不需要任何网络环节——离线不是省事的妥协,而是这条链路本来就够短。

幕六:拿它当模板 🔧

键盘节点导航的三个可借鉴细节

js/navigation.js 体量不大,细节值得抄进自己的项目:

  • 只用两个键:ArrowDown下一节点、ArrowUp上一节点。节点列表来自 iframe 内全部g.node,每次渲染结束由rebuildNavNodes回调重新收集并重置索引,避免操作失效的旧节点。
  • 居中偏移取 1/6 而非 1/2:centerCurrentNode里"中心"按top + height / 6计算。严格居中会让焦点节点恰好顶到画面上缘,LR/TD 布局下后续节点直接掉出视野;取 1/6 把焦点压向画布左上,"下一步"仍在可视区里,连续下键体验才顺。
  • 编辑时不劫持按键:事件处理先判断焦点是否在源码文本区,是则直接 return。代码输入优先用键盘,导航只在非输入态接管。

高亮本身由 renderer 注入的 CSS(.selected-node蓝框加发光)呈现,导航模块只负责切换 class——样式与逻辑分家,两边可独立改。

二次开发:可裁剪点与可迁移模式

子包业务代码合计就八个文件,入口只有一个app.js,裁剪点很明显:

  1. 删导航:js/navigation.js及 app.js 里两处调用,主流程无感;
  2. 换存储层:createStorage的返回值只被用到setCurrent / updateCurrent / deleteCurrent / create四个方法,换成 IndexedDB 或文件方案只需对齐这几个签名;
  3. 换主题:theme: 'dark'与注入 iframe 的那段 CSS 是仅有的两处外观相关代码。

可迁移的模式更短:构建期把mermaid.min.js和purify.min.js打进vendor/;运行期走"渲染 → 净化 → 沙箱 → 持久化"一整圈。凡是需要离线画图、内网工具嵌入图表渲染的场景,照这个装配顺序就能复刻。

它在 monorepo 构建与测试体系中的位置

根 package.json 把整条构建链固化成build:mermaid:full一条命令,CI 或本地都可一键复现;e2e 范围脚本 e2e-diagram-scope.mjs 显式列出了packages/mermaid-local-editor/路径,涉及该包的改动会同步触发对应端到端测试。根lint命令对整个仓库跑 ESLint,这份源码也在同一套静态检查范围内。

【免费下载链接】mermaidGeneration of diagrams like flowcharts or sequence diagrams from text in a similar manner as markdown项目地址: https://gitcode.com/GitHub_Trending/me/mermaid

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询