# 📝如何将思维导图导出导入为 XMind 文件
Xmind
文件本质上是一个 ZIP
压缩包。其中包含了多个 JSON 文件 (如 content.json
, metadata.json
等),这些文件描述了思维导图的内容和结构
# 1. 🛠️ 准备工作
- JSZip: 用于创建 ZIP 文件。允许我们轻松创建符合 XMind 规范的 ZIP 文件
- file-saver: 用于保存生成的文件。这个库简化了浏览器环境下的文件下载过程。
# 2. 🔍 定义数据结构
# Node
接口
interface Node { | |
id: string; // 节点 ID | |
text: string; // 节点文本 | |
position: [number, number]; // 节点位置 | |
children: string[]; // 子节点 ID 列表 | |
size: [number, number]; // 节点尺寸 | |
collapsed: boolean; // 折叠状态 | |
direction?: 'left' | 'right' | 'none'; // 方向属性 | |
} |
清晰的表示每个节点及其关系
# Topic
接口
// 定义思维导图主题类型 | |
interface Topic { | |
id: string; | |
structureClass: string; | |
title: string; | |
children?: { | |
attached: Topic[]; // 子节点 | |
}; | |
} |
Topic
是 XMind 中的主体 (节点) 模型,它不仅包含节点的基本属性,还允许递归地定义子节点。能够构建复杂的树状结构
# Sheet
接口
// 定义思维导图工作表类型 | |
interface Sheet { | |
id: string; | |
class: string; | |
title: string; | |
extensions: unknown[]; | |
topicPositioning: string; | |
topicOverlapping: string; | |
coreVersion: string; | |
rootTopic: Topic; | |
} |
Sheet
是 XMind 中的工作表模型,它包含一个根主题,以及各种属性,如标题、扩展、主题定位、主题重叠等。有助于我们组织整个思维导图的数据结构。
# ContentJSON
, MetadataJSON
, ManifestJSON
类型
// 定义 content.json 的类型 | |
type ContentJSON = Sheet[]; | |
// 定义 metadata.json 的类型 | |
interface MetadataJSON { | |
modifier: string; | |
dataStructureVersion: string; | |
creator: { name: string }; | |
layoutEngineVersion: string; | |
activeSheetId: string; | |
} | |
// 定义 manifest.json 的类型 | |
interface ManifestJSON { | |
"file-entries": { | |
[filename: string]: { "media-type": string }; | |
}; | |
} |
这些事 XMind 文件中的核心 JSON 文件, ContentJSON
包含了思维导图的所有内容; MetadataJSON
提供了元数据信息,如版本号和创建者; ManifestJSON
描述了文件清单,指定了每个文件的名称和媒体类型。这些文件共同构成了一个完整的 XMind 文件。
# 3. 🧩 辅助函数
我们将编写辅助函数来构建思维导图的内容
# buildChildren
函数
递归的构建子节点树状结构, 将每个节点转换为 Topic
对象。
// 辅助函数:递归构建子节点 | |
const buildChildren = (nodes: Record<string, Node>, parentId: string): Topic[] => { | |
console.log(`Processing parentId: ${parentId}`); | |
if (!nodes[parentId]) { | |
console.warn(`Node with id "${parentId}" not found`); | |
return []; | |
} | |
const childrenIds = nodes[parentId].children; // 获取当前父节点的子节点 ID 列表 | |
console.log(`Children IDs for parentId "${parentId}":`, childrenIds); | |
// 根据子节点 ID 构建子节点列表 | |
return childrenIds | |
.map((childId): Topic | null => { | |
const childNode = nodes[childId]; | |
if (!childNode) { | |
console.warn(`Child node with id "${childId}" not found`); | |
return null; | |
} | |
return { | |
id: childNode.id, | |
structureClass: 'org.xmind.ui.logic.right', | |
title: childNode.text, | |
children: { | |
attached: buildChildren(nodes, childNode.id), // 递归处理子节点 | |
}, | |
}; | |
}) | |
.filter((topic): topic is Topic => !!topic); // 过滤掉无效的子节点 | |
}; |
# createContentJSON
函数
用于构建 content.json
文件,该文件包含了整个思维导图的所有节点信息,需要确保所有节点都正确转换成 XMind 格式
// 辅助函数:构建 content.json | |
const createContentJSON = (): ContentJSON => { | |
const { nodes } = useMindmapStore.getState(); | |
// 构建根节点 | |
const rootNode = nodes['root']; | |
if (!rootNode) { | |
throw new Error('未找到根节点'); | |
} | |
const rootTopic: Topic = { | |
id: 'root', | |
structureClass: 'org.xmind.ui.logic.right', | |
title: rootNode.text, | |
children: { | |
attached: buildChildren(nodes, 'root'), // 从根节点开始递归 | |
}, | |
}; | |
return [ | |
{ | |
id: 'simpleMindMap_1744799393059', | |
class: 'sheet', | |
title: '思维导图标题', | |
extensions: [], | |
topicPositioning: 'fixed', | |
topicOverlapping: 'overlap', | |
coreVersion: '2.100.0', | |
rootTopic, | |
}, | |
]; | |
}; |
# createMetadataJSON
函数
用于构建 metadata.json
文件,该文件包含了思维导图的元数据信息,如版本号、创建者等。
// 辅助函数:构建 metadata.json | |
const createMetadataJSON = (): MetadataJSON => { | |
return { | |
modifier: '', // 修改者(可为空) | |
dataStructureVersion: '2', // 数据结构版本 | |
creator: { | |
name: 'mind-map', // 创建者名称 | |
}, | |
layoutEngineVersion: '3', // 布局引擎版本 | |
activeSheetId: 'simpleMindMap_1744799393059', // 当前活动的工作表 ID | |
}; | |
}; |
# createManifestJSON
函数
用于构建 manifest.json
文件,该文件描述了文件清单,指定了每个文件的名称和媒体类型。这对解压和解析 XMind 文件很重要。
// 辅助函数:构建 manifest.json | |
const createManifestJSON = (): ManifestJSON => { | |
return { | |
"file-entries": { | |
"content.json": { "media-type": "application/json" }, //content.json 的媒体类型 | |
"metadata.json": { "media-type": "application/json" }, //metadata.json 的媒体类型 | |
"manifest.json": { "media-type": "application/json" }, //manifest.json 的媒体类型 | |
}, | |
}; | |
}; |
# 4. 🚀 导出 XMind 文件
最后,我们将使用这些内容打包成 .xmind
文件下载。
通过 JSZip
库,我们可以将多个 JSON 文件打包成一个 ZIP 文件。模拟 XMind 文件的内部结构。生成的文件可以直接被 XMind 应用程序打开。
// 导出为 XMind 文件 | |
export const exportAsXMind = (): void => { | |
try { | |
// 获取节点数据 | |
const { nodes } = useMindmapStore.getState(); | |
// 检查 nodes 是否为空 | |
if (!nodes || Object.keys(nodes).length === 0) { | |
console.error('没有节点可以导出'); | |
return; | |
} | |
// 构建 content.json | |
const contentJson: ContentJSON = createContentJSON(); | |
// 构建 metadata.json | |
const metadataJson: MetadataJSON = createMetadataJSON(); | |
// 构建 manifest.json | |
const manifestJson: ManifestJSON = createManifestJSON(); | |
// 使用 JSZip 打包 | |
const zip = new JSZip(); | |
zip.file('content.json', JSON.stringify(contentJson)); | |
zip.file('metadata.json', JSON.stringify(metadataJson)); | |
zip.file('manifest.json', JSON.stringify(manifestJson)); | |
// 生成并下载文件 | |
zip.generateAsync({ type: 'blob', compression: 'DEFLATE' }).then((blob) => { | |
saveAs(blob, 'mindmap.xmind'); | |
}); | |
} catch (error) { | |
console.error('导出失败:', error); | |
} | |
}; |
# 🎉
通过以上步骤,我们可以轻松地将思维导图数据导出为 XMind 格式的文件。
# ✨更新:2025-4-17
更新内容:
- 添加了导入 xmind 格式文件的功能。
# 1. 数据结构定义
这里和之前类似
// 定义思维导图主题类型 | |
interface Topic { | |
id: string; | |
structureClass: string; | |
title: string; | |
size: [number, number]; // 节点尺寸 | |
position: [number, number]; // 节点位置 | |
collapsed: boolean; // 折叠状态 | |
children?: { | |
attached: Topic[]; // 子节点 | |
}; | |
} | |
// 定义思维导图工作表类型 | |
interface Sheet { | |
id: string; | |
class: string; | |
title: string; | |
extensions: unknown[]; | |
topicPositioning: string; | |
topicOverlapping: string; | |
coreVersion: string; | |
rootTopic: Topic; | |
} | |
// 定义 content.json 的类型 | |
type ContentJSON = Sheet[]; | |
// 定义解析后的数据结构 | |
interface ParsedData { | |
nodes: Record<string, Node>; // 节点记录 | |
connections: string[]; // 连接关系列表 | |
} |
# 2. 递归解析子节点函数
通过递归的方式解析子节点,并构建节点记录和连接关系列表。
// 辅助函数:递归解析子节点 | |
const parseChildren = (topic: Topic, parentId: string, parsedData: ParsedData): void => { | |
const { id, title, position, size, collapsed, children } = topic; | |
// 创建当前节点 | |
const node: Node = { | |
id, | |
text: title, | |
position: position || [0, 0], // 如果没有提供位置,则使用默认值 | |
children: [], // 子节点 ID 列表 | |
size: size || [200, 60], // 如果没有提供尺寸,则使用默认值 | |
collapsed: collapsed || false, // 如果没有提供折叠状态,则使用默认值 | |
direction: 'right', // 方向属性是可选的 | |
}; | |
// 添加到节点记录 | |
parsedData.nodes[id] = node; | |
// 如果有父节点,则建立连接 | |
if (parentId) { | |
parsedData.connections.push(`${parentId}---${id}`); | |
parsedData.nodes[parentId].children.push(id); | |
} | |
// 递归处理子节点 | |
if (children?.attached) { | |
children.attached.forEach((childTopic) => { | |
parseChildren(childTopic, id, parsedData); | |
}); | |
} | |
}; |
# 3. 解析 XMind 文件函数
读入文件返回一个 Promise,解析文件内容并返回解析后的数据。
由于 XMind 文件是一个 ZIP 格式的文件,所以需要使用 JSZip 库来解析。
// 导入 xmind 文件 | |
export const importFromXMind = (file: File): Promise<void> => { | |
return new Promise((resolve, reject) => { | |
const zip = new JSZip(); | |
zip.loadAsync(file) | |
.then((unzipped) => { | |
// 读取 content.json | |
return unzipped.file('content.json')?.async('text'); | |
}) | |
.then((contentJsonText) => { | |
if (!contentJsonText) { | |
throw new Error('Missing content.json'); | |
} | |
// 解析 content.json | |
const contentJson: ContentJSON = JSON.parse(contentJsonText); | |
// 初始化解析数据 | |
const parsedData: ParsedData = { | |
nodes: {}, | |
connections: [], | |
}; | |
// 解析根节点 | |
const rootSheet = contentJson[0]; | |
if (!rootSheet || !rootSheet.rootTopic) { | |
throw new Error('Invalid content.json structure'); | |
} | |
parseChildren(rootSheet.rootTopic, '', parsedData); | |
// 更新 store 状态 | |
useMindmapStore.setState({ | |
nodes: parsedData.nodes, | |
connections: parsedData.connections, | |
selectedNodeId: null, // 如果需要,可以从其他地方获取 | |
layoutStyle: 'left-to-right', // 如果需要,可以从 metadata.json 获取 | |
}); | |
resolve(); | |
}) | |
.catch((error) => { | |
reject(new Error(`Failed to import XMind file: ${error.message}`)); | |
}); | |
}); | |
}; |
# 😘
现在不仅可以导出为 Xmind 文件,也可以导入 Xmind 文件了,虽然导入只限于本思维导图的导出的导入,但是导出的是可以适配 Xmind 的,所以可以很方便的导入到 Xmind 中。