MarkmapDocker部署与优化
Markmap Docker 部署与优化完整笔记
一、项目概述
基于 Node.js 和 markmap 库构建的思维导图渲染服务,支持将 Markdown 文件渲染为交互式思维导图,部署在 Podman 容器中,使用 Caddy 作为反向代理。
二、项目结构
/data/markmap/
├── Dockerfile # 容器构建文件
├── server.js # 主服务程序
├── public/
│ └── js/ # 前端 JavaScript 库
│ ├── d3.min.js
│ └── markmap-view.js
├── docs/ # Markdown 文件存放目录
│ ├── dy.mm.md
│ └── dn.mm.md
└── Caddyfile # Caddy 反向代理配置三、Dockerfile 配置
FROM docker.1ms.run/node:26-alpine
WORKDIR /app
# 安装 markmap 核心依赖
RUN npm install express markmap-lib markmap-view d3
# 复制前端库到可访问目录
RUN mkdir -p public/js && \
cp node_modules/d3/dist/d3.min.js public/js/ && \
cp node_modules/markmap-view/dist/browser/index.js public/js/markmap-view.js
COPY server.js .
COPY public /app/public
EXPOSE 3000
CMD ["node", "server.js"]四、Caddy 反向代理配置
zmdmap.zhaopeng.site {
reverse_proxy mdmap:3000
}五、Server.js 完整代码
const express = require('express');
const path = require('path');
const fs = require('fs');
const markmapLib = require('markmap-lib');
const { Transformer } = markmapLib;
const app = express();
const PORT = process.env.PORT || 3000;
// 关闭所有日志 - 减少 IO
console.log = function() {};
console.info = function() {};
console.warn = function() {};
app.disable('x-powered-by');
// 静态文件服务
app.use('/js', express.static(path.join(__dirname, 'public/js')));
// 处理 .md 和 .mm.md 文件
app.get(/\/[^/]+\.(mm\.)?md$/, async (req, res) => {
try {
const filePath = path.join(__dirname, 'docs', req.path);
if (!fs.existsSync(filePath)) {
return res.status(404).send('File not found');
}
const content = fs.readFileSync(filePath, 'utf-8');
const transformer = new Transformer();
const result = transformer.transform(content);
const root = result.root;
const html = generateHTML(root, req.path);
res.send(html);
} catch (err) {
res.status(500).send(`
<h2>处理文件时出错</h2>
<p>${err.message}</p>
`);
}
});
// 首页 - 显示空白
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Markmap</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
background: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.message {
color: #ccc;
font-size: 14px;
letter-spacing: 2px;
user-select: none;
}
</style>
</head>
<body>
<div class="message">Markmap</div>
</body>
</html>
`);
});
// 404 处理
app.use((req, res) => {
res.status(404).send(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
background: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.message {
color: #ddd;
font-size: 13px;
letter-spacing: 1px;
user-select: none;
}
</style>
</head>
<body>
<div class="message">404</div>
</body>
</html>
`);
});
function generateHTML(root, filePath) {
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Markmap - ${path.basename(filePath)}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #f5f5f5;
}
body {
display: flex;
justify-content: center;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
padding: 20px;
}
#mindmap {
width: 100%;
height: 100%;
max-width: 1400px;
max-height: 90vh;
background: white;
border-radius: 16px;
box-shadow: 0 4px 24px rgba(0,0,0,0.1);
padding: 20px;
position: relative;
min-height: 80vh;
}
#mindmap svg {
width: 100% !important;
height: 100% !important;
display: block !important;
background: #fafafa !important;
border-radius: 8px;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
min-height: 500px;
color: #999;
font-size: 18px;
}
.error {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #e74c3c;
font-size: 16px;
padding: 20px;
text-align: center;
flex-direction: column;
gap: 10px;
}
/* 节点圆点样式微调 */
.markmap-node circle {
fill: #3498db !important;
stroke: #2980b9 !important;
stroke-width: 2px !important;
cursor: pointer !important;
}
.markmap-node text {
font-size: 14px !important;
font-weight: 500 !important;
fill: #2c3e50 !important;
}
.markmap-node {
cursor: pointer !important;
}
</style>
</head>
<body>
<div id="mindmap">
<div class="loading">加载思维导图中...</div>
</div>
<script src="/js/d3.min.js"></script>
<script src="/js/markmap-view.js"></script>
<script>
(function() {
// 禁用浏览器控制台日志
console.log = function() {};
console.info = function() {};
console.warn = function() {};
console.debug = function() {};
const container = document.getElementById('mindmap');
const data = ${JSON.stringify(root)};
if (!data || !data.children || data.children.length === 0) {
container.innerHTML = '<div class="error">思维导图数据为空</div>';
return;
}
if (typeof d3 === 'undefined') {
container.innerHTML = '<div class="error">d3.js 未加载</div>';
return;
}
if (typeof markmap === 'undefined') {
container.innerHTML = '<div class="error">markmap-view 未加载</div>';
return;
}
try {
const { Markmap } = window.markmap;
container.innerHTML = '';
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '100%');
svg.setAttribute('height', '100%');
svg.style.width = '100%';
svg.style.height = '100%';
svg.style.display = 'block';
svg.style.background = '#fafafa';
container.appendChild(svg);
const mm = Markmap.create(svg, {
autoFit: false, // 关闭自动适应全图(关键:防止点击节点触发缩放复位)
fitRatio: 0.92,
duration: 150, // 保留轻微平滑过度,防止图形闪烁
maxWidth: 500,
colorFreezeLevel: 2,
zoom: true,
pan: true,
nodeMinHeight: 28,
spacingVertical: 15,
spacingHorizontal: 20,
color: (node, depth) => {
const colors = ['#3498db', '#2ecc71', '#f39c12', '#e74c3c', '#9b59b6', '#1abc9c', '#e67e22', '#1dd1a1'];
return colors[depth % colors.length] || '#3498db';
},
toggleRecursively: false
}, data);
// 仅在首次渲染时全图居中一次
requestAnimationFrame(() => {
mm.fit();
const loading = container.querySelector('.loading');
if (loading) loading.style.display = 'none';
});
} catch (err) {
container.innerHTML = \`
<div class="error">
<strong>渲染失败</strong>
<br>
\${err.message}
</div>
\`;
}
})();
</script>
</body>
</html>`;
}
app.listen(PORT, '0.0.0.0', () => {});全屏版
const express = require('express');
const path = require('path');
const fs = require('fs');
const markmapLib = require('markmap-lib');
const { Transformer } = markmapLib;
const app = express();
const PORT = process.env.PORT || 3000;
// 关闭所有日志 - 减少 IO
console.log = function() {};
console.info = function() {};
console.warn = function() {};
app.disable('x-powered-by');
// 静态文件服务
app.use('/js', express.static(path.join(__dirname, 'public/js')));
// 处理 .md 和 .mm.md 文件
app.get(/\/[^/]+\.(mm\.)?md$/, async (req, res) => {
try {
const filePath = path.join(__dirname, 'docs', req.path);
if (!fs.existsSync(filePath)) {
return res.status(404).send('File not found');
}
const content = fs.readFileSync(filePath, 'utf-8');
const transformer = new Transformer();
const result = transformer.transform(content);
const root = result.root;
const html = generateHTML(root, req.path);
res.send(html);
} catch (err) {
res.status(500).send(`
<h2>处理文件时出错</h2>
<p>${err.message}</p>
`);
}
});
// 首页 - 显示空白
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Markmap</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
background: #ffffff;
display: flex;
justify-content: center;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.message {
color: #ccc;
font-size: 14px;
letter-spacing: 2px;
user-select: none;
}
</style>
</head>
<body>
<div class="message">Markmap</div>
</body>
</html>
`);
});
// 404 处理
app.use((req, res) => {
res.status(404).send(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
background: #ffffff;
display: flex;
justify-content: center;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.message {
color: #ddd;
font-size: 13px;
letter-spacing: 1px;
user-select: none;
}
</style>
</head>
<body>
<div class="message">404</div>
</body>
</html>
`);
});
function generateHTML(root, filePath) {
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Markmap - ${path.basename(filePath)}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100vw;
height: 100vh;
overflow: hidden;
background: #ffffff;
}
#mindmap {
width: 100vw;
height: 100vh;
background: #ffffff;
position: relative;
}
#mindmap svg {
width: 100% !important;
height: 100% !important;
display: block !important;
background: #ffffff !important;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #999;
font-size: 18px;
}
.error {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #e74c3c;
font-size: 16px;
padding: 20px;
text-align: center;
flex-direction: column;
gap: 10px;
}
/* 节点圆点样式微调 */
.markmap-node circle {
fill: #3498db !important;
stroke: #2980b9 !important;
stroke-width: 2px !important;
cursor: pointer !important;
}
.markmap-node text {
font-size: 14px !important;
font-weight: 500 !important;
fill: #2c3e50 !important;
}
.markmap-node {
cursor: pointer !important;
}
</style>
</head>
<body>
<div id="mindmap">
<div class="loading">加载思维导图中...</div>
</div>
<script src="/js/d3.min.js"></script>
<script src="/js/markmap-view.js"></script>
<script>
(function() {
// 禁用浏览器控制台日志
console.log = function() {};
console.info = function() {};
console.warn = function() {};
console.debug = function() {};
const container = document.getElementById('mindmap');
const data = ${JSON.stringify(root)};
if (!data || !data.children || data.children.length === 0) {
container.innerHTML = '<div class="error">思维导图数据为空</div>';
return;
}
if (typeof d3 === 'undefined') {
container.innerHTML = '<div class="error">d3.js 未加载</div>';
return;
}
if (typeof markmap === 'undefined') {
container.innerHTML = '<div class="error">markmap-view 未加载</div>';
return;
}
try {
const { Markmap } = window.markmap;
container.innerHTML = '';
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '100%');
svg.setAttribute('height', '100%');
svg.style.width = '100%';
svg.style.height = '100%';
svg.style.display = 'block';
svg.style.background = '#ffffff';
container.appendChild(svg);
const mm = Markmap.create(svg, {
autoFit: false,
fitRatio: 0.95, // 调大缩放占比,使全图初始化时利用更多可视空间
duration: 150,
maxWidth: 600, // 调大节点最大宽度,适应大屏显示
colorFreezeLevel: 2,
zoom: true,
pan: true,
nodeMinHeight: 28,
spacingVertical: 15,
spacingHorizontal: 20,
color: (node, depth) => {
const colors = ['#3498db', '#2ecc71', '#f39c12', '#e74c3c', '#9b59b6', '#1abc9c', '#e67e22', '#1dd1a1'];
return colors[depth % colors.length] || '#3498db';
},
toggleRecursively: false
}, data);
// 仅在首次渲染时全图居中一次
requestAnimationFrame(() => {
mm.fit();
const loading = container.querySelector('.loading');
if (loading) loading.style.display = 'none';
});
} catch (err) {
container.innerHTML = \`
<div class="error">
<strong>渲染失败</strong>
<br>
\${err.message}
</div>
\`;
}
})();
</script>
</body>
</html>`;
}
app.listen(PORT, '0.0.0.0', () => {});六、部署命令
构建镜像
podman build -t markmap .运行容器
podman run -d --name mdmap -p 3000:3000 -v /data/markmap/docs:/app/docs markmap查看容器状态
podman ps
podman logs -f mdmap停止和删除容器
podman stop mdmap
podman rm mdmap七、问题排查与解决方案
问题1: Transformer is not defined
错误信息:
ReferenceError: Transformer is not defined原因: markmap-lib 的导出方式问题。
解决方案:
const markmapLib = require('markmap-lib');
const { Transformer } = markmapLib;问题2: SVG 内容为空
错误信息:
📐 SVG content length: 55
📐 SVG children count: 2原因: markmap-view API 使用方式不正确。
解决方案: 使用 Markmap.create() 方法,传入数据和配置。
问题3: color is not a function
错误信息:
TypeError: color is not a function原因: color 配置项需要是函数,不是数组。
解决方案:
color: (node, depth) => {
const colors = ['#3498db', '#2ecc71', '#f39c12', '#e74c3c', '#9b59b6', '#1abc9c'];
return colors[depth % colors.length] || '#3498db';
}问题4: 节点点击抖动
原因:
- 过渡动画导致元素位置变化
- 点击区域过小
- hover 效果触发重排
解决方案:
- 禁用所有 transition
- 增大 circle 半径
- 禁用 hover 效果
- 设置 duration: 0
问题5: CORS 错误
错误信息:
Access to fetch at 'https://rumtc.myalicdn.com/v1/common/manage' from origin has been blocked by CORS policy原因: 外部监控脚本加载失败,不影响核心功能。
解决方案: 忽略此错误,或移除相关脚本。
八、配置参数说明
Markmap 核心配置
| 参数 | 值 | 说明 |
|---|---|---|
| autoFit | true | 自动适配视图 |
| fitRatio | 0.92 | 适配比例 |
| duration | 0 | 动画持续时间(0=禁用) |
| maxWidth | 500 | 节点最大宽度 |
| zoom | true | 启用缩放 |
| pan | true | 启用拖拽 |
| nodeMinHeight | 28 | 节点最小高度 |
| spacingVertical | 15 | 垂直间距 |
| spacingHorizontal | 20 | 水平间距 |
| toggleRecursively | false | 仅切换当前节点 |
九、访问方式
- 首页: https://zmdmap.zhaopeng.site/ (显示空白)
- 思维导图: https://zmdmap.zhaopeng.site/文件名.md
- 示例: https://zmdmap.zhaopeng.site/dy.mm.md
十、性能优化
- 禁用所有日志输出,减少 IO 操作
- 禁用动画效果,提升响应速度
- 增大节点间距,提升交互体验
- 使用静态文件服务,减少重复加载
- 容器化部署,资源隔离
十一、注意事项
- 确保 docs 目录存在并有正确的文件权限
- 文件必须使用 .md 或 .mm.md 扩展名
- Caddy 需要正确配置反向代理
- 容器重启后需重新挂载数据卷
- 生产环境建议使用更严格的安全配置