toolgarden.xyz
EN
浏览器工具开发MindElixir在线思维导图Markdown前端工程

怎么使用 MindElixir 实现在线思维导图:Markdown、交互与导出

用 React 封装 MindElixir,实现 Markdown 大纲转换、节点状态同步、快捷键、画布拖动、全屏与多格式导出。

ToolGarden 推荐的工具优先在浏览器本地运行,文件和文本不必上传到服务器,适合更注重安全隐私的日常处理。

发布于 2026年7月22日约 13 分钟阅读作者 ToolGarden

MindElixir 可以很快渲染一张可编辑思维导图,但真正可用的在线编辑器远不止 mind.init(data)。用户会期待视觉树与可携带的大纲保持同步、快捷键编辑符合直觉、空白画布可以拖动、全屏后自动适配,并且能够可靠导出成果。

下面这套架构把 MindElixir 视为命令式浏览器运行时,把 Markdown 视为易读的交换格式。React 管理产品外壳和派生 UI 状态;纯工具函数负责 Markdown、JSON 转换;编辑器实例负责节点、历史、连线、概要、专注模式、缩放与渲染。

每个边界只保留一种标准树结构

MindElixir 原生数据以 nodeData 为中心:每个节点包含 id、topic 和可选 children,direction 表示左侧、右侧或双侧布局。视觉编辑过程中应使用这棵原生树;Markdown 和 JSON 是导入导出表示,不是同时竞争的实时状态源。

产品外壳可以在编辑器操作后派生一份快照:当前 Markdown、选中主题、选中数量、是否允许删除或专注、缩放比例、专注模式和布局。这样选择节点时不需要 React 重新渲染整个命令式画布。

数据表示用途
MindElixirData实时编辑数据与原生 JSON 备份
Markdown 大纲人类可读编辑和数据交换
React 快照按钮、状态、选择、缩放与布局 UI
SVG 或 PNG分享视觉结果
独立 HTML同时携带视觉图与可读大纲

组件挂载后再初始化命令式编辑器

在 route layout 引入 mind-elixir/style.css,然后在 effect 中动态加载运行时与 i18n 模块。拿到真实 host 元素后只创建一次实例,卸载时调用 destroy。disposed 标记可以防止异步加载完成后重新激活已卸载的编辑器。

实例应保存在 ref,而不是 React state。改变选择、相机位置或节点文字,都不应该让 React 重新创建画布。还可以用 ResizeObserver 监听容器变化并调用 scaleFit。

const [{ default: MindElixir }, i18n] = await Promise.all([
  import('mind-elixir'),
  import('mind-elixir/i18n'),
]);

const mind = new MindElixir({
  el: hostElement,
  direction: MindElixir.SIDE,
  editable: true,
  contextMenu: {
    locale: locale === 'zh' ? i18n.zh_CN : i18n.en,
    focus: true,
    link: true,
  },
  keypress: true,
  overflowHidden: true,
  toolBar: false,
});

mind.init(data);
mind.scaleFit();

// On unmount:
mind.destroy();

用纯函数和栈解析 Markdown

实用的大纲格式可以同时接受 ATX 标题和缩进列表。把每个非空行解析成 level 与 topic,再用栈记录各层最近节点。添加新节点前,弹出 level 大于等于当前层级的栈项,剩余栈顶就是父节点。

如果文档出现多个顶级根节点,则创建一个兜底根节点。导入时为节点生成新 ID,导出时把 topic 内换行规范为单行。解析错误返回 Outcome,不要把异常直接抛进组件。

type Outcome =
  | { ok: true; data: MindElixirData }
  | { ok: false; message: string };

function parseLine(line: string) {
  const heading = line.match(/^\s*(#{1,6})\s+(.+)$/u);
  if (heading) return { level: heading[1].length, topic: heading[2] };

  const bullet = line.match(/^(\s*)[-*+]\s+(.+)$/u);
  if (bullet) {
    return {
      level: Math.floor(bullet[1].replace(/\t/g, '  ').length / 2) + 2,
      topic: bullet[2],
    };
  }

  return null;
}
  • 约定两个空格为一级列表缩进,并统一规范 Tab。
  • 只移除行首列表标记,不破坏主题内部标点。
  • 导出时根节点使用标题,后代使用缩进列表。
  • 解析和序列化都不依赖 React 与 DOM。

监听编辑器事件同步,不要轮询

MindElixir 提供 operation、selection、新节点和 scale 等事件。初始化后一次性订阅,并把快照读取安排到下一个任务,让编辑器先完成内部数据与 DOM 更新。cleanup 时必须移除完全相同的监听函数引用。

用户打开 Markdown 大纲并正在编辑时,不要用选择事件覆盖输入框。可以用 ref 保存最近一次已提交 Markdown,只有用户明确应用大纲或大纲面板关闭时才替换 draft,否则点一下节点就可能丢掉写到一半的内容。

  • operation:更新文档派生 UI,并识别 beginEdit。
  • selectNewNode:同步刚创建、正准备编辑的新节点。
  • selectNodes 与 unselectNodes:更新操作按钮可用状态。
  • scale:只更新缩放百分比,不重建地图。

让快捷键编辑行为符合预期

命令式编辑器通常会临时创建 contenteditable 节点用于改名。全局快捷键可能在内容提交前触发,导致 Enter 或 Tab 一边新增节点,一边让当前文字停留在临时状态。可以在编辑器容器捕获 keydown:Enter、Tab 先提交,Escape 恢复原文,然后再同步快照。

通过按钮开始编辑时,要等输入节点真正出现,再聚焦并选中文字。结束后把焦点还给地图容器,撤销、重做、新增与删除快捷键才能继续工作。

增加画布拖动,但不要破坏节点交互

完整编辑器应该允许拖动空白画布,但拖动主题、展开按钮、连线、概要、输入框或按钮时必须保留原生行为。pointerdown 时先判断目标是否位于交互元素内,只有真正的背景拖动才 capturePointer。

记录上一次指针位置,在 pointermove 调用 mind.move(dx, dy)。pointerup 或 pointercancel 时释放捕获、恢复鼠标样式并把焦点还给编辑器。Pointer Events 可以统一处理鼠标、触控笔和触摸。

用小型适配器暴露编辑器能力

用一个 runMapAction 包住命令式操作,统一捕获错误、延迟同步快照和恢复焦点。按钮就可以安全调用 addChild、insertSibling、beginEdit、removeNodes、undo、redo、scaleFit、initLeft、initRight、initSide、focusNode、cancelFocus、createSummary 或 createArrow,不必重复生命周期代码。

创建连线时,把来源节点与单向/双向模式存进 ref,下一次点击有效主题时把它作为目标,然后清除连线模式。这比在无关 selection 事件之后猜测两个选中节点更容易理解。

为编辑、分享和恢复提供不同导出格式

原生 JSON 最适合往返备份,因为它保留 MindElixir 数据;Markdown 最容易阅读和交换;SVG 适合文档中的高清矢量图;PNG 方便即时分享;独立 HTML 可以把导出的 SVG 和可折叠 Markdown 大纲放在一起。

导入 JSON 时必须递归验证 nodeData,不能只做类型断言。每个节点都要有字符串 id、topic,children 必须是递归节点数组,direction 只能取支持值。下载使用 Blob Object URL,完成后立即 revoke。

  • JSON:原生可编辑备份。
  • Markdown:便携大纲和文本编辑。
  • SVG:不受分辨率影响的视觉导出。
  • PNG:方便直接分享图片。
  • HTML:包含可访问大纲的独立快照。

全屏和容器变化后重新适配

沿用其他画布工具的“原生全屏加 fixed 降级”模式。每次模式切换后,等布局稳定再调用 scaleFit。map host 上的 ResizeObserver 还能处理响应式侧栏、屏幕方向和其他容器尺寸变化。

组件卸载时销毁 observer、事件监听、指针状态、连线模式和 MindElixir 实例。否则热更新后可能留下重复的键盘与事件总线监听,让每个操作执行两次。

总结

可维护的 MindElixir 架构要把命令式编辑器与 React 分开,也要把转换逻辑与两者分开。实例只初始化一次,通过事件同步,保护正在编辑的大纲,用统一错误边界适配操作,并同时提供原生 JSON 与易读 Markdown,才能从画布示例成长为真正的在线工具。

常见问题

Q.Markdown 和 MindElixirData 谁应该是实时唯一数据源?

视觉编辑期间使用 MindElixirData。每次已提交操作后派生 Markdown,用户明确应用大纲草稿时再导入。每个事件都从 Markdown 重建编辑器,会丢失编辑器专属状态,也可能覆盖未完成输入。

Q.为什么 MindElixir 实例要放在 ref?

它是可变的命令式运行时,不是渲染状态。ref 能让实例跨 React render 保持不变,也不会因为选择、缩放或历史变化触发组件重渲染。

Q.为什么卸载时必须调用 destroy?

实例拥有 DOM、事件总线订阅、快捷键和其他资源。destroy 并移除自定义监听,可以避免切换路由或热更新后出现重复处理器。

Q.哪种导出格式最适合以后恢复编辑?

原生 JSON 最适合往返编辑,因为它能保留树结构和编辑器专属数据。Markdown 更便携、更易读,但未必保留连线、概要、样式和所有布局细节。

Q.在线思维导图如何保护隐私?

让解析、渲染、导入和导出都在浏览器完成,不把节点文本或文件发送到 API,也不在统计中记录内容。同时要说明:如果没有导出备份,用户清除浏览器数据后,本地状态可能消失。