- 使用现有库/框架的源码:这是最常见和高效的方式,即使用成熟的 JavaScript 库(如 Mind-.js, JointJS, GoJS 等)来创建思维导图,我会提供几个主流库的源码示例。
- 从零开始编写的源码:这涉及到大量的 Canvas 或 SVG 操作,以及复杂的交互逻辑,我会提供一个极简的、从零开始的 SVG 思维导图示例,帮助你理解其核心原理。
- 相关资源链接:提供一些优秀的开源项目、教程和工具,供你深入学习。
使用现有库(推荐)
对于大多数项目来说,直接使用成熟的库是最佳选择,它们功能强大、稳定,并且有良好的社区支持。
示例 1:使用 Mind-Map 库 (简单易用)
这是一个非常轻量且易于集成的库。
安装
npm install mind-map # 或者直接在 HTML 中引入 CDN
HTML 代码 (index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">思维导图示例</title>
<!-- 引入 Mind-Map 的 CSS 和 JS -->
<link rel="stylesheet" href="https://unpkg.com/mind-map/dist/mind-map.css">
<script src="https://unpkg.com/mind-map/dist/mind-map.js"></script>
<style>
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
#mindMap {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="mindMap"></div>
<script>
// 定义思维导图数据
const data = {
"data": {
"text": "中心主题"
},
"children": [
{
"data": {
"text": "分支 1"
},
"children": [
{ "data": { "text": "子分支 1.1" } },
{ "data": { "text": "子分支 1.2" } }
]
},
{
"data": {
"text": "分支 2"
},
"children": [
{ "data": { "text": "子分支 2.1" } },
{ "data": { "text": "子分支 2.2" } }
]
}
]
};
// 初始化思维导图
const mindMap = new MindMap({
el: document.getElementById('mindMap'), // 挂载的 DOM 元素
data: data, // 数据
theme: 'classic' // 主题
});
</script>
</body>
</html>
源码解析:
- 这个“源码”主要是 HTML 和 JavaScript 的组合。
mind-map.js和mind-map.css是库的核心源码,你可以在其 GitHub 仓库中找到,我们通过 CDN 引入它们,然后调用new MindMap()来实例化一个导图实例。data对象是导图的数据结构,通常是嵌套的 JSON,每个节点包含text(文本) 和children(子节点)。
示例 2:使用 JointJS (功能强大,适合流程图和复杂图表)
JointJS 是一个更底层的图表库,可以用来构建非常复杂的图形应用,包括思维导图。
安装
npm install jointjs
HTML 代码 (index.html)
<!DOCTYPE html>
<html>
<head>JointJS 思维导图</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jointjs/3.7.0/joint.min.css" />
<style>
body {
margin: 0;
padding: 0;
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
}
#paper {
flex: 1;
background-color: #f5f5f5;
}
</style>
</head>
<body>
<div id="paper"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.4.1/backbone-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jointjs/3.7.0/joint.min.js"></script>
<script>
// 创建画布 (Paper)
const paper = new joint.dia.Paper({
el: document.getElementById('paper'),
width: '100%',
height: '100%',
model: new joint.dia.Graph(), // 关联一个图模型
gridSize: 1,
drawGrid: true,
background: {
color: '#ffffff'
},
interactive: true,
snapLinks: { radius: 5 },
linkPinning: false,
highlighting: {
'default': {
name: 'stroke',
options: {
padding: 2,
rx: 5,
ry: 5,
attrs: {
'stroke-width': 2,
stroke: 'rgba(255, 255, 255, 0.5)',
'fill-opacity': 0
}
}
}
}
});
// 定义节点和链接的默认样式
const nodeDefaults = {
attrs: {
body: {
fill: '#ffffff',
stroke: '#333333',
strokeWidth: 2,
rx: 5,
ry: 5
},
label: {
textVerticalAnchor: 'middle',
textAnchor: 'middle',
fontSize: 14,
fill: '#333333'
}
}
};
// 创建中心节点
const centerNode = new joint.shapes.standard.Rectangle({
position: { x: 400, y: 250 },
size: { width: 120, height: 60 },
attrs: {
body: { fill: '#4a90e2' },
label: { text: '中心主题' }
}
});
// 创建分支节点
const branchNode1 = new joint.shapes.standard.Rectangle({
position: { x: 200, y: 150 },
size: { width: 100, height: 50 },
attrs: {
body: { fill: '#7ed321' },
label: { text: '分支 1' }
}
});
const branchNode2 = new joint.shapes.standard.Rectangle({
position: { x: 200, y: 350 },
size: { width: 100, height: 50 },
attrs: {
body: { fill: '#f5a623' },
label: { text: '分支 2' }
}
});
// 创建连接线
const link1 = new joint.shapes.standard.Link({
source: { id: centerNode.id },
target: { id: branchNode1.id },
attrs: {
line: {
stroke: '#333333',
strokeWidth: 2
}
}
});
const link2 = new joint.shapes.standard.Link({
source: { id: centerNode.id },
target: { id: branchNode2.id },
attrs: {
line: {
stroke: '#333333',
strokeWidth: 2
}
}
});
// 将所有元素添加到图中
paper.model.addCell([centerNode, branchNode1, branchNode2, link1, link2]);
</script>
</body>
</html>
源码解析:
- JointJS 的核心是 Graph (图) 和 Paper (画布),Graph 存储所有节点和链接的数据,Paper 负责渲染和交互。
joint.shapes.standard.Rectangle是一个预定义的矩形节点。joint.shapes.standard.Link是一个预定义的链接。- 通过
source和target属性将链接与节点连接起来。 - 你可以在 JointJS GitHub 找到其完整的源码和文档。
从零开始编写一个极简版 SVG 思维导图
这个示例不依赖任何库,直接使用原生 JavaScript 和 SVG 来实现,它能帮助你理解思维导图渲染和交互的本质。
HTML 代码 (index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">原生 SVG 思维导图</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: sans-serif;
background-color: #f0f0f0;
}
#map-container {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: auto;
}
.node {
cursor: pointer;
}
.node text {
user-select: none; /* 防止文本被选中 */
pointer-events: none; /* 让鼠标事件穿透文本,作用在 rect 上 */
}
.link {
fill: none;
stroke: #999;
stroke-width: 2px;
}
</style>
</head>
<body>
<h1>原生 SVG 思维导图</h1>
<div id="map-container"></div>
<script>
// 1. 定义数据
const mindMapData = {
id: 'root',
name: '中心主题',
children: [
{
id: 'c1',
name: '分支 1',
children: [
{ id: 'c1-1', name: '子分支 1.1' },
{ id: 'c1-2', name: '子分支 1.2' }
]
},
{
id: 'c2',
name: '分支 2',
children: [
{ id: 'c2-1', name: '子分支 2.1' },
{ id: 'c2-2', name: '子分支 2.2' }
]
}
]
};
// 2. 渲染函数
function renderMindMap(data, container, x = 400, y = 50, level = 0) {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '100%');
svg.setAttribute('height', '600'); // 固定高度,可动态计算
container.appendChild(svg);
// 递归渲染节点和连接线
function renderNode(node, parentX, parentY, currentX, currentY) {
// 创建连接线
if (parentX !== null && parentY !== null) {
const link = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const d = `M ${parentX} ${parentY} C ${parentX} ${parentY + 50}, ${currentX} ${currentY - 50}, ${currentX} ${currentY}`;
link.setAttribute('d', d);
link.setAttribute('class', 'link');
svg.appendChild(link);
}
// 创建节点组
const nodeGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
nodeGroup.setAttribute('class', 'node');
nodeGroup.setAttribute('transform', `translate(${currentX - 50}, ${currentY - 20})`);
// 创建节点矩形
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('width', '100');
rect.setAttribute('height', '40');
rect.setAttribute('rx', '5');
rect.setAttribute('ry', '5');
rect.setAttribute('fill', level === 0 ? '#4a90e2' : (level === 1 ? '#7ed321' : '#f5a623'));
// 创建节点文本
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', '50');
text.setAttribute('y', '25');
text.setAttribute('text-anchor', 'middle');
text.setAttribute('dominant-baseline', 'middle');
text.setAttribute('fill', 'white');
text.textContent = node.name;
nodeGroup.appendChild(rect);
nodeGroup.appendChild(text);
svg.appendChild(nodeGroup);
// 递归渲染子节点
if (node.children && node.children.length > 0) {
const childCount = node.children.length;
const spacing = 150; // 子节点之间的水平间距
const startX = currentX - (spacing * (childCount - 1)) / 2;
node.children.forEach((child, index) => {
const childX = startX + index * spacing;
const childY = currentY + 120; // 子节点的垂直偏移
renderNode(child, currentX, currentY, childX, childY, level + 1);
});
}
}
// 从根节点开始渲染
renderNode(data, null, null, x, y, 0);
}
// 3. 执行渲染
const container = document.getElementById('map-container');
renderMindMap(mindMapData, container);
</script>
</body>
</html>
源码解析:
- 数据结构:依然是一个嵌套的 JSON 对象。
- SVG:所有图形元素(
<rect>,<text>,<path>)都在 SVG 画布上创建。 - 递归渲染:
renderNode函数是核心,它负责创建当前节点的图形,然后递归地为每个子节点调用自身,并计算子节点的位置。 - 布局算法:这是一个非常简单的层级式布局,父节点在上方,子节点在下方水平排列,更复杂的布局(如树形、径向形)需要更复杂的算法。
- 交互:这个版本没有添加拖拽、编辑等交互,但通过
cursor: pointer和addEventListener可以轻松添加。
相关资源与工具
如果你想深入研究或寻找更多灵感,可以看看以下资源:
-
GitHub 开源项目:
-
在线工具:
- XMind: 付费/免费,桌面端和在线版都非常流行。
- MindNode: Mac/iOS 平台上的优秀工具。
- Miro: 在线白板工具,内置强大的思维导图功能。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 使用现有库 | 开发快、功能稳定、有文档支持 | 定制化受限,需要学习库的API | 绝大多数项目,尤其是商业项目 |
| 从零编写 | 完全可控、无依赖、能深入理解原理 | 开发周期长、bug多、需自己实现所有功能 | 学习研究、高度定制化的特殊需求项目 |
对于初学者和大多数应用场景,强烈推荐从方案一开始,选择一个合适的库进行开发,当你对思维导图的原理有了深入了解后,再考虑从零编写或对现有库进行深度定制。
