推荐先阅读:MarkDown(mermaid)绘制流程图详细教程
在 Node.js 18.20 环境下,结合 Express 框架、markdown-it 解析器和 mermaid 10.9 版本来实现 Markdown 中 Mermaid 图表的渲染,提供一个完整可运行的示例方案。
实现思路
- 搭建基础的 Express 服务
- 配置 markdown-it,并集成 mermaid 10.9 插件解析 Mermaid 语法
- 提供一个示例接口/页面,展示 Markdown 转 HTML(包含 Mermaid 图表)的效果
- 确保前端能加载 Mermaid 渲染脚本,完成图表的最终渲染
完整代码实现
1. 初始化项目并安装依赖
bash
mkdir my-app
cd my-app
# 初始化 package.json(按需填写)
npm init -y
# 安装所需依赖
npm install express@4 markdown-it@13
2. 核心代码文件(app.js)
javascript
// app.js
const express = require('express');
const markdownIt = require('markdown-it');
const fs = require('fs');
const path = require('path');
// 初始化 Express 应用
const app = express();
// 配置静态文件目录
app.use(express.static(path.join(__dirname, 'public')));
// 配置 markdown-it(不受 mermaid 模块格式影响)
const md = markdownIt({
html: true,
linkify: true,
typographer: true
});
// 注册 mermaid 解析插件(仅处理代码块,不依赖 mermaid 实例)
md.use(function(md) {
const fence = md.renderer.rules.fence;
md.renderer.rules.fence = function(tokens, idx, options, env, self) {
const token = tokens[idx];
if (token.info.trim() === 'mermaid') {
const code = token.content.trim();
// 仅生成容器标签,前端会负责渲染,后端无需调用 mermaid 核心方法
return `<div class="mermaid">${code}</div>`;
}
return fence(tokens, idx, options, env, self);
};
});
// 读取静态 HTML 文件(缓存)
const indexHtml = fs.readFileSync(path.join(__dirname, 'public', 'index.htm'), 'utf8');
// 处理渲染请求(异步函数)
app.get('/render-md', async (req, res) => {
try {
// 1. 读取 example.md 文件
const mdPath = path.join(__dirname, 'public', 'example.md');
const testMd = fs.readFileSync(mdPath, 'utf8');
// 2. 解析 Markdown 为 HTML(核心逻辑,无需后端 mermaid 实例)
const renderedHtml = md.render(testMd);
// 3. 注入到 index.html 并返回
const finalHtml = indexHtml.replace('<div id="md-content"></div>', renderedHtml);
res.send(finalHtml);
} catch (error) {
console.error('处理 Markdown 时出错:', error);
res.status(500).send(`
<h1>出错了</h1>
<p>无法读取或解析 example.md 文件:${error.message}</p>
`);
}
});
// 启动服务
const port = 8000;
app.listen(port, () => {
console.log(`server start:http://localhost:${port}/render-md`);
});
mkdir public; cd public
3. public/index.htm
html
<!-- public/index.htm -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Mermaid 测试</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.mermaid { margin: 20px 0; }
pre { background: #f5f5f5; padding: 10px; border-radius: 5px; }
</style>
<!-- 引入 mermaid 10.9 前端脚本 -->
<!-- https://cdn.jsdelivr.net/npm/mermaid@10.9/dist/mermaid.min.js -->
<script src="/mermaid.min.js"></script>
</head>
<body>
<!-- 预留占位符,用于注入解析后的 HTML 内容 -->
<div id="md-content"></div>
<script>
// 前端初始化 mermaid 并渲染图表
mermaid.initialize({ startOnLoad: true });
</script>
</body>
</html>
public/example.md
markdown
### Mermaid 测试示例
这是一个使用 mermaid 10.9 渲染的流程图:
```mermaid
flowchart TD
A[Node.js 18.20] --> B[Express]
B --> C[markdown-it]
C --> D[mermaid 10.9]
D --> E[渲染图表]
饼图示例
40% 30% 30% 技术栈占比 Express markdown-it mermaid
时序图示例
mermaid markdown-it Express 前端 mermaid markdown-it Express 前端 请求渲染 Markdown 解析 Markdown 文本 识别 Mermaid 代码块 返回容器标签 返回解析后的 HTML 发送带容器的 HTML 渲染图表
代码关键部分解释
-
Mermaid 配置:
startOnLoad: false关闭自动加载,避免前端脚本重复初始化securityLevel: 'loose'放宽安全限制,适合开发测试环境(生产环境可调整为strict)
-
Markdown-It 插件:
- 重写
fence渲染规则,识别 ```````mermaid```` 代码块 - 将 Mermaid 代码包裹在
<div class="mermaid">容器中,供前端脚本识别渲染
- 重写
-
前端渲染:
- 通过 CDN 引入与后端版本一致的 mermaid 10.9 前端脚本
- 前端执行
mermaid.initialize({ startOnLoad: true })触发图表渲染
运行与验证
cd public
curl -O https://cdn.jsdelivr.net/npm/mermaid@10.9/dist/mermaid.min.js
- 执行代码: node app.js
- 访问地址:http://localhost:8000/render-md
- 效果:页面会显示 Markdown 解析后的内容,且包含两个可正常渲染的 Mermaid 图表(流程图和饼图)
进阶使用建议
-
读取本地 Markdown 文件 :
可替换测试用的
testMd变量,改为读取本地.md文件:javascriptconst testMd = fs.readFileSync(path.join(__dirname, 'test.md'), 'utf8'); -
生产环境优化:
- 将 mermaid 前端脚本下载到本地,避免依赖 CDN
- 调整
securityLevel为strict,限制不安全的代码执行 - 增加错误处理,捕获 Markdown 解析或 Mermaid 渲染异常
-
自定义 Mermaid 主题 :
可在初始化时指定自定义主题:
javascriptmermaid.initialize({ theme: 'forest', // 可选:default、forest、dark、neutral themeVariables: { primaryColor: '#61a955', primaryTextColor: '#fff' } });
总结
- Node.js 18.20 下使用 mermaid 10.9 需后端解析 Markdown 并生成 Mermaid 容器 ,前端引入对应版本脚本完成渲染。
- 核心步骤是通过 markdown-it 插件识别 mermaid 代码块,包裹专属容器,再由前端 mermaid 脚本渲染。
- 确保前后端 mermaid 版本一致(均为 10.9),避免兼容性问题。
如果 example.md 不在后端,是由客户端通过网页上传的,如何编程?
为了实现让客户端通过网页上传 example.md 文件,而非将文件固定在后端的 public 目录下,我会帮你完成这个完整的上传+解析+渲染流程,核心是实现文件上传接口、解析上传的 Markdown 文件、再渲染包含 Mermaid 图表的内容。
实现思路
- 扩展 Express 服务,添加文件上传功能(使用
multer处理文件上传)。 - 前端页面新增文件上传表单,允许用户选择本地 Markdown 文件并提交。
- 后端接收上传的 Markdown 文件,读取内容后解析为 HTML。
- 将解析后的内容注入到
index.html中返回给前端,完成 Mermaid 图表渲染。
完整实现步骤
1. 安装新增依赖
需要安装 multer 处理文件上传:
bash
npm install multer@1.4 --save
2. 改造前端页面(public/index.html)
新增文件上传表单,让用户可以选择并上传 Markdown 文件:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Mermaid 测试-上传.md</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.mermaid { margin: 20px 0; }
pre { background: #f5f5f5; padding: 10px; border-radius: 5px; }
.upload-form { margin-bottom: 40px; padding: 20px; border: 1px solid #eee; border-radius: 8px; }
input[type="file"] { margin: 10px 0; }
button { padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #0056b3; }
</style>
<!-- 引入 mermaid 10.9 前端脚本 -->
<!-- https://cdn.jsdelivr.net/npm/mermaid@10.9/dist/mermaid.min.js -->
<script src="/mermaid.min.js"></script>
</head>
<body>
<!-- 文件上传表单 -->
<div class="upload-form">
<h3>上传 Markdown 文件(支持 Mermaid 图表)</h3>
<form id="mdUploadForm" enctype="multipart/form-data" method="POST" action="/upload-md">
<input type="file" name="mdFile" accept=".md,.MD" required>
<br>
<button type="submit">上传并渲染</button>
</form>
</div>
<!-- 渲染后的 Markdown 内容占位符 -->
<div id="markdown-content"></div>
<script>
// 前端初始化 mermaid 并渲染图表
mermaid.initialize({ startOnLoad: true });
</script>
</body>
</html>
3. 改造后端代码(server.js)
添加文件上传处理逻辑,接收客户端上传的 Markdown 文件并解析:
javascript
// server.js
const express = require('express');
const markdownIt = require('markdown-it');
const fs = require('fs');
const path = require('path');
const multer = require('multer');
// 初始化 Express 应用
const app = express();
// 配置静态文件目录
app.use(express.static(path.join(__dirname, 'public')));
// ====================== 配置文件上传 ======================
// 改造 fileFilter:不直接抛错,而是通过 req 传递错误信息
const fileFilter = (req, file, cb) => {
const allowedTypes = ['.md', '.MD'];
const ext = path.extname(file.originalname).toLowerCase();
if (allowedTypes.includes(ext)) {
cb(null, true);
} else {
// 不抛错,而是标记错误类型,后续在路由中处理
req.fileValidationError = '仅支持上传 .md 或 .MD 格式的文件!';
cb(null, false); // 第二个参数为 false 表示拒绝该文件
}
};
// 配置 multer
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 1024 * 1024 * 5 // 5MB
},
fileFilter: fileFilter // 使用改造后的 fileFilter
});
// ====================== 配置 markdown-it ======================
const md = markdownIt({
html: true,
linkify: true,
typographer: true
});
// 注册 mermaid 解析插件
md.use(function(md) {
const fence = md.renderer.rules.fence;
md.renderer.rules.fence = function(tokens, idx, options, env, self) {
const token = tokens[idx];
if (token.info.trim() === 'mermaid') {
const code = token.content.trim();
return `<div class="mermaid">${code}</div>`;
}
return fence(tokens, idx, options, env, self);
};
});
// 读取静态 HTML 文件(缓存)
const indexHtml = fs.readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8');
// ====================== 接口定义 ======================
// 1. 首页:返回上传页面
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// 2. 处理 Markdown 文件上传 + 解析 + 渲染
app.post('/upload-md', upload.single('mdFile'), async (req, res) => {
try {
// 1. 检查文件类型错误(来自 fileFilter)
if (req.fileValidationError) {
return res.status(400).send(`
<h1>上传失败</h1>
<p>${req.fileValidationError}</p>
<a href="/">返回上传页面</a>
`);
}
// 2. 检查是否有文件上传
if (!req.file) {
return res.status(400).send(`
<h1>上传失败</h1>
<p>请选择要上传的 Markdown 文件!</p>
<a href="/">返回上传页面</a>
`);
}
// 3. 读取并解析 Markdown 内容
const mdContent = req.file.buffer.toString('utf8');
const renderedHtml = md.render(mdContent);
const finalHtml = indexHtml.replace('<div id="markdown-content"></div>', renderedHtml);
res.send(finalHtml);
} catch (error) {
// 4. 捕获其他未知错误:只返回通用提示,不暴露服务器细节
console.error('文件处理异常:', error); // 控制台保留详细日志(供开发者排查)
res.status(500).send(`
<h1>处理失败</h1>
<p>文件处理出错,请稍后重试!</p>
<a href="/">返回上传页面</a>
`);
}
});
// ====================== 全局错误处理中间件(兜底) ======================
// 捕获所有未处理的错误,统一返回友好提示
app.use((err, req, res, next) => {
console.error('全局异常:', err); // 控制台记录详细错误
res.status(500).send(`
<h1>服务器出错</h1>
<p>抱歉,服务器暂时无法处理你的请求,请稍后重试!</p>
<a href="/">返回上传页面</a>
`);
});
// 启动服务
const port = 8000;
app.listen(port, () => {
console.log(`服务已启动:http://localhost:${port}`);
console.log(`访问 http://localhost:${port} 即可上传 Markdown 文件`);
});
关键代码解释
-
文件上传配置(multer):
memoryStorage():将上传的文件临时存储在内存中,避免写入磁盘(适合小体积 Markdown 文件),如果需要持久化,可改用diskStorage配置存储路径。fileFilter:过滤文件类型,仅允许.md/.markdown文件,防止上传恶意文件。limits.fileSize:限制文件大小为 5MB,避免超大文件占用服务器资源。
-
文件内容读取:
- 上传的文件通过
req.file.buffer获取(Buffer 格式),需用toString('utf8')转为字符串才能被 markdown-it 解析。
- 上传的文件通过
-
接口逻辑:
GET /:返回带上传表单的首页。POST /upload-md:接收上传的文件,解析后渲染内容,核心流程是「接收文件 → 读取内容 → 解析 Markdown → 注入 HTML → 返回结果」。
运行验证
-
确保目录结构:
你的项目/
├── server.js
├── public/
│ └── index.html 和 mermaid.min.js
└── node_modules/ -
安装依赖:
npm install express markdown-it multer。 -
启动服务:
node server.js。 -
访问
http://localhost:8000,会看到文件上传表单:- 选择本地的 Markdown 文件(包含 Mermaid 代码块)。
- 点击「上传并渲染」,页面会显示解析后的内容,Mermaid 图表正常渲染。
进阶优化建议
-
持久化上传文件 (可选):
如果需要保存用户上传的 Markdown 文件,可修改 multer 存储配置:
javascript// 配置磁盘存储 const storage = multer.diskStorage({ destination: (req, file, cb) => { const uploadDir = path.join(__dirname, 'uploads'); // 确保目录存在 if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir); cb(null, uploadDir); }, filename: (req, file, cb) => { // 自定义文件名:时间戳 + 原文件名 const filename = `${Date.now()}-${file.originalname}`; cb(null, filename); } }); const upload = multer({ storage: storage, /* 其他配置 */ }); -
支持多文件上传 :
将
upload.single('mdFile')改为upload.array('mdFiles', 5)(最多上传 5 个文件),并遍历处理每个文件。 -
前端异步上传 (无刷新):
用 AJAX/Fetch 实现异步上传,避免页面刷新:
javascript// 在 index.html 的 <script> 中添加 document.getElementById('mdUploadForm').addEventListener('submit', async (e) => { e.preventDefault(); // 阻止默认表单提交 const formData = new FormData(e.target); const response = await fetch('/upload-markdown', { method: 'POST', body: formData }); const html = await response.text(); // 替换页面内容(仅保留渲染结果) document.body.innerHTML = html; });
总结
- 客户端上传 Markdown 文件的核心是用
multer处理文件上传,优先选择内存存储(小文件)或磁盘存储(需持久化)。 - 后端流程:接收文件 → 读取内容(Buffer 转字符串)→ markdown-it 解析 → 注入 HTML 返回。
- 关键限制:过滤文件类型、限制文件大小,避免服务器安全风险。