Mermaid Local Editor 完全指南:构建零依赖、离线可用的本地图编辑器
【免费下载链接】mermaidGeneration of diagrams like flowcharts or sequence diagrams from text in a similar manner as markdown项目地址: https://gitcode.com/GitHub_Trending/me/mermaid
本指南以 packages/mermaid-local-editor 为对象,系统讲解 Mermaid 官方仓库内嵌的“本地编辑器”子包——一种完全运行于浏览器、不依赖任何外部 CDN、可离线使用的 Mermaid 图编辑器。文章将带你走通从pnpm build:mermaid:full到最终产出dist/静态站的完整构建管线,并深入编辑器源码(index.html、app.js 及 js/ 目录下的 renderer、storage、ui、navigation 等模块),理解其图渲染、安全沙箱、持久化存储与键盘导航等设计实现。读完你既能一键构建并本地预览这套编辑器,也能照此模式为自己的项目打造类似的离线 Mermaid 工作台。
一、项目定位:一个跑在dist/里的独立本地编辑器
Mermaid Local Editor 是仓库中一个private: true的独立子包(见 packages/mermaid-local-editor/package.json),其 README 用一句话定义了它的本质:
Standalone local editor for Mermaid diagrams. Runs entirely from
dist/with no external dependencies.
这意味着它不同于线上版的 Mermaid Live Editor:它把 Mermaid 运行时、DOMPurify 净化库以及编辑器自身的 HTML/CSS/JS 全部打成本地静态文件,构建完成后无需任何后端、无需网络请求即可运行。
核心特性(README Notes 与源码可相互印证)
- 不使用外部 CDN:
static/index.html中只通过本地相对路径引入./vendor/mermaid.min.js与./vendor/purify.min.js,再以<script type="module" src="./app.js">挂载应用入口(见 static/index.html)。 - DOMPurify 本地打包:净化库作为本地 vendor 文件随构建产物一并分发。
- 完全离线可用:构建产物为纯静态文件,可双击打开或由任意静态服务器托管。
- 专为从
dist/直接运行而设计:构建脚本刻意将所有依赖拷贝进同一个输出目录。
从目录结构看,编辑器源码组织得非常清晰:
packages/mermaid-local-editor/ ├── README.md ├── package.json └── static/ ├── index.html # 页面骨架与工具栏 ├── styles.css # 界面样式 ├── app.js # 应用入口:初始化、装配各模块 └── js/ ├── config.js # Mermaid 初始化参数与全局视图状态 ├── renderer.js # 渲染、净化、缩放平移 ├── storage.js # localStorage 持久化 ├── ui.js # 工具栏交互与自动保存 └── navigation.js # 键盘在节点间导航二、一键构建:理解pnpm build:mermaid:full
在仓库根目录执行 README 给出的唯一构建命令即可启动整套流程:
pnpm build:mermaid:full该命令在根 package.json 中的真实定义为:
"build:mermaid:full": "pnpm clean && pnpm build:mermaid && pnpm copy:editor && pnpm copy:mermaid && pnpm copy:dompurify && pnpm serve:dist"下面按 README 描述的五步,逐一对应到根 package.json 中的底层脚本:
1. Clean——清理旧产物
pnpm clean对应脚本"clean": "rimraf packages/mermaid/dist"(package.json),移除上一次构建生成的packages/mermaid/dist,保证每次从干净状态出发。
2. Build Mermaid——编译 Mermaid 运行时
pnpm build:mermaid对应"build:mermaid": "pnpm build:esbuild --mermaid"(package.json),调用仓库统一的 esbuild 构建管线,产出mermaid.min.js等运行时文件。这一步产生的是编辑器后续要引用的核心渲染引擎。
3. Copy Editor——拷贝编辑器静态源
pnpm copy:editor对应"copy:editor": "cpy \"packages/mermaid-local-editor/static/**/*\" packages/mermaid/dist/mermaid-local-editor"(package.json),把本包static/下的index.html、styles.css、app.js与js/*.js原样复制到输出目录packages/mermaid/dist/mermaid-local-editor/。注意 index.html 中引用的就是这些同目录文件,因此复制后结构完全一致即可直接运行。
4. Bundle Dependencies——拷贝运行时依赖
pnpm copy:mermaid pnpm copy:dompurify分别对应(package.json):
"copy:mermaid": "cpy packages/mermaid/dist/mermaid.min.js packages/mermaid/dist/mermaid-local-editor/vendor/ --flat", "copy:dompurify": "cpy node_modules/.pnpm/dompurify@*/node_modules/dompurify/dist/purify.min.js packages/mermaid/dist/mermaid-local-editor/vendor/ --flat"这两步把 Mermaid 浏览器 bundle(mermaid.min.js)和从 pnpm 扁平化依赖目录(.pnpm/dompurify@*/...)中提取的purify.min.js统一放入输出目录下的vendor/子目录——正好对上 index.html 里的两个<script src="./vendor/...">。
5. Serve——本地静态服务预览
pnpm serve:dist对应"serve:dist": "sirv packages/mermaid/dist/mermaid-local-editor --port 8081 --dev --no-clear"(package.json),使用sirv将dist产物托管在http://localhost:8081(默认端口,可改),浏览器打开即可使用编辑器。
构建产物
全部步骤执行完毕后,完整的可运行编辑器位于:
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该目录纯静态,可复制到任意环境离线使用。
三、编辑器是如何启动的:从index.html到app.js
index.html定义了编辑器的基本界面骨架(static/index.html):
- 工具栏(
#toolbar):包含折叠按钮、图表下拉框#diagrams、名称输入框#name,以及 Save / New / Delete / Reset View / Export SVG 五个操作按钮。 - 主工作区(
#main):左侧为源码文本域#srcPanel,右侧为渲染预览区#preview。
app.js作为模块入口,按依赖顺序完成装配(static/app.js):
import { initMermaid, state, IS_E2E } from './js/config.js'; import { createStorage } from './js/storage.js'; import { renderDiagram } from './js/renderer.js'; import { setupUI, refreshList } from './js/ui.js'; import { createNavigation } from './js/navigation.js'; initMermaid(); // ① 按编辑器偏好初始化 Mermaid const storage = createStorage(); // ② 建立 localStorage 存储层 const navigation = createNavigation({...}); // ③ 键盘节点导航 // ... render() / load() / applyTransform() 等核心函数 setupUI({...}); // ④ 绑定工具栏交互 navigation.setupKeyboardNav(); load(storage.current); // ⑤ 载入当前图表并渲染启动后默认加载storage.current指定的图表;若存储为空,storage 模块会自动以默认示例创建名为main的图表(见下文持久化一节)。
四、Mermaid 初始化配置与安全策略
initMermaid()在 static/js/config.js 中对 Mermaid 运行时做了针对本编辑器场景的初始化:
export function initMermaid() { mermaid.initialize({ startOnLoad: false, // 不自动渲染页面中的 <pre class="mermaid"> theme: 'dark', // 深色主题 securityLevel: 'strict', // 最高安全等级:禁用点击跳转/脚本执行 deterministicIds: true, // 确定性 id,利于结果稳定与测试 fontFamily: 'Arial', htmlLabels: false, // 使用非 HTML 标签渲染文本 flowchart: { useMaxWidth: false }, }); }各关键配置的实际作用:
startOnLoad: false:本编辑器需要把用户在文本域中输入的 Mermaid 代码交给 JS 手动渲染(见 renderer),而非依赖页面加载时扫描class="mermaid"元素,因此必须关闭自动启动。securityLevel: 'strict':这是核心安全防线。Mermaid 在 strict 模式下会剥离图表内的点击链接与脚本相关功能,从渲染源头规避了从用户输入的图源码中注入可执行内容的可能。deterministicIds: true:让每次渲染生成的元素 id 稳定可预测,便于自动化测试与节点导航模块对g.node的稳定定位。flowchart.useMaxWidth: false:关闭流程图按容器宽度自适应缩放,配合预览区的自由缩放/平移机制,保证用户缩放后的画布尺寸不被父容器约束回原形。
同一文件还导出了两个全局相关对象:
export const IS_E2E = navigator.webdriver || location.search.includes('graph='); export let state = { scale: 1, panX: 0, panY: 0, iframeRef: null };IS_E2E用于识别“自动化测试或带graph=查询参数”的直出场景;state则集中管理预览区当前的缩放比、平移偏移量及 iframe 引用,是 renderer、navigation、ui 多个模块共享的“视图状态单例”。
五、渲染管线:mermaid.render+ DOMPurify 净化 + 沙箱 iframe
图渲染是编辑器的核心路径,实现在 static/js/renderer.js 的renderDiagram()中。整体流程分两条分支:
常规运行模式(iframe 沙箱)
- 渲染:调用
await mermaid.render(id, srcValue)将 Mermaid 源码编译为 SVG 字符串。 - 净化:
DOMPurify.sanitize(svg, { ADD_TAGS: ['foreignObject'], ADD_ATTR: ['xmlns'] }),保留 Mermaid 渲染所需但 DOMPurify 默认可能剥离的foreignObject标签与xmlns属性,同时清除其余潜在危险内容。 - 二次加固:遍历 SVG 元素,删除所有以
on开头的内联事件属性(如onclick、onload),从 DOM 层面彻底封死事件注入:svgEl.querySelectorAll('*').forEach((el) => { [...el.attributes].forEach((attr) => { if (attr.name.startsWith('on')) { el.removeAttribute(attr.name); } }); }); - 沙箱隔离:新建
<iframe sandbox="allow-same-origin">,把净化后的 SVG 通过iframe.contentDocument.importNode(svgEl, true)写入 iframe 文档。sandbox 属性(不放开allow-scripts)使渲染结果无法执行脚本,预览区域因此与主页面形成隔离边界。 - 注入交互样式:向 iframe 文档写入
<style>,实现节点 hover 高亮(填充蓝色光晕)、.selected-node选中态(蓝色描边 + 发光)、文本浅色化(适配深色主题)以及body { overflow: hidden }等视觉效果。
E2E 模式(直出分支)
当IS_E2E为真时,渲染结果不经过 iframe,而是把净化后的 SVG 直接作为 DOM 插入#preview。这保证了在自动化测试(如 Playwright、截图对比)中无需等待 iframe 布局,SVG 可直接被断言与截图。
缩放与平移
renderer 在 iframe 文档上注册了滚轮与鼠标事件实现漫游交互:
- 缩放:
doc.onwheel中以state.scale += e.deltaY * -0.0015累加缩放系数,并将结果钳制在0.2~4倍之间,随后调用applyTransform()。 - 平移:
mousedown/mousemove/mouseup组合实现按住拖拽画布,同时将拖拽光标切换为grabbing。
applyTransform()(定义于 static/app.js)最终把视图状态落到 SVG 上:
svg.style.transform = `translate(${state.panX}px, ${state.panY}px) scale(${state.scale})`;平移量同样被钳制在 ±20000px 以内,并将view: { scale, panX, panY }同步写回存储——视图位置会随图表一起被记住。
错误处理
任何渲染异常都会被try/catch捕获:清空预览区,并以红色<pre>展示e.message。用户在左侧输入无效 Mermaid 语法时,能在右侧立刻看到清晰的语法错误提示,而不是白屏或控制台报错。
六、多图管理与 localStorage 持久化
static/js/storage.js 用localStorage实现多图表管理与会话保持,涉及两个 key:
mermaid-diagrams:以 JSON 对象存储所有图表,结构为{ [名称]: { 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 }, };storage 模块对外暴露的操作与 static/js/ui.js 中的按钮一一对应:
| 操作 | UI 触发 | 行为 |
|---|---|---|
save | Save 按钮 | 以#name输入值作为当前图表名,保存src与视图状态 |
create(name) | New 按钮(prompt输入名称) | 新建默认内容(flowchart LR\n A --> B)并切换到该图 |
deleteCurrent() | Delete 按钮(confirm确认) | 删除当前图,回退到剩余第一张图或main |
setCurrent/updateCurrent | 下拉框切换、自动保存 | 切换当前图 / 合并更新当前图字段 |
同时编辑器提供了300ms 防抖自动保存:用户在#srcPanel输入时立即重渲染预览,同时clearTimeout + setTimeout(..., 300)延后把最新源码写回存储(static/js/ui.js)。因此即使忘记点 Save,源码也不会大量丢失。
此外 ui.js 还实现了:
- Reset View(↺):将
scale/panX/panY归位为1/0/0并重存视图。 - Export SVG(⬇):从 iframe 中取出当前 SVG,用
Blob([svg.outerHTML], { type: 'image/svg+xml' })生成URL.createObjectURL,以下载名为${当前图表名}.svg的链接触发保存,之后及时revokeObjectURL释放资源(static/js/ui.js)。 - 工具栏折叠:折叠状态以
toolbar-collapsed这个 localStorage key 记忆,刷新页面后保持用户的界面偏好。
七、键盘导航:在节点之间"行走"
static/js/navigation.js 提供了一个很实用的可访问性功能:无需鼠标,用键盘在流程图节点间逐跳导航。
- 导航目标收集:渲染完成后(renderer 通过
rebuildNavNodes回调触发),收集 iframe 内所有svg g.node作为导航节点列表。 - 高亮当前节点:先清除已有节点的
selected-node类,再把classList.add('selected-node')加到当前索引对应的节点上——样式层(renderer 注入的 CSS)随即呈现蓝色描边与发光效果。 - 视图跟随:
centerCurrentNode()计算节点与预览容器的几何中心差并累加到panX/panY。注释中特别说明,偏移有意取1/6而非严格居中(/ 6而非/ 2),使焦点节点偏向画布左上,从而在 LR/TD 布局下让后续节点仍留在可视区域内,便于连续往下走。 - 键盘控制:
ArrowDown前进到下一节点、ArrowUp回到上一节点;当焦点位于源码编辑框时按键不劫持,保证用户仍能正常编辑代码。
这套实现体现了编辑器对键盘可达性的考量:对视障或偏好键盘操作的用户,可逐个节点感知图表结构与空间位置。
八、安全模型小结:三层纵深防御
综合源码可见,编辑器对"渲染用户输入"这一高风险行为做了三层纵深防护,这也是它敢于完全离线、不对输入做任何服务端校验的底气所在:
- Mermaid 配置层:
securityLevel: 'strict'+deterministicIds,从图解析端抑制链接点击与脚本注入; - 内容净化层:渲染出的 SVG 字符串统一经过 DOMPurify
sanitize()(白名单放行foreignObject与xmlns),并额外剥离一切on*内联事件属性; - 运行隔离层:预览 SVG 被写入
sandbox="allow-same-origin"的 iframe(不授予allow-scripts),即便 SVG 中存在残余攻击载荷也无法执行脚本、无法逃逸出沙箱影响主页。
九、在 Mermaid 仓库中该包所处的位置
从构建与测试基础设施看,packages/mermaid-local-editor已被纳入仓库的开发闭环:
- 根 package.json 将其完整构建链固化为
build:mermaid:full一条命令,便于 CI 或本地一键复现; - 它出现在 ESLint 检查范围与 e2e 相关脚本的作用域内(
scripts/e2e-diagram-scope.mjs中显式列出该包路径),说明其源码会与仓库其他代码一同经受静态检查与端到端测试的守护。
对于想要把 Mermaid 能力"离线化/私有化"的开发者,这个子包提供了一个非常干净的参考范式:构建期将mermaid.min.js+purify.min.js打进vendor/,运行时用"渲染 → 净化 → 沙箱 iframe → localStorage 持久化"的最小闭环完成所见即所得的图表编辑体验。整个编辑器的全部业务代码不足十个文件,非常适合作为二次开发或功能裁剪的起点。
【免费下载链接】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),仅供参考