查看: 291|回复: 1

Node.js+Express+marked+mermaid.js:搭建Markdown在线编辑器支持流程图渲染

[复制链接]
发表于 昨天 11:00 | 显示全部楼层 |阅读模式
在技术文档和开发笔记中,Markdown因其简洁语法成为首选,但若能在线实时预览并嵌入流程图(如mermaid),将极大提升写作效率。本文基于Node.js 18.20,使用Express 4、marked 12、mermaid.js和EJS模板引擎,构建一个可直接运行的在线Markdown编辑器,支持实时解析、保存/加载文档,并原生支持mermaid流程图渲染。

## 实现思路
1. 用Express搭建Web服务,提供静态资源与API接口;
2. 使用marked解析Markdown,自定义渲染器识别mermaid代码块,由前端mermaid.js客户端渲染;
3. 提供左侧编辑、右侧预览的双面板界面,点击按钮触发解析或保存/加载;
4. 基于本地文件系统(docs目录)实现简单的文档持久化。

## 1. 项目初始化与依赖安装
创建项目目录并初始化:
  1. mkdir md-edit-app
  2. cd md-edit-app
  3. npm init -y
复制代码
安装核心依赖(指定版本以保证兼容性,当前Node.js推荐18.20):
  1. npm install express@4.18.3 marked@12.0.2
  2. npm install cors@2.8.5 body-parser@1.20.2 fs-extra@11.2.0 ejs@3.1.9
复制代码

## 2. 服务器主程序(app.js)
创建app.js,实现以下功能:
- 配置marked解析器,自定义code渲染器:遇到语言为mermaid的代码块时输出<div class="mermaid">内容</div>,由前端mermaid.js接管渲染;其它代码块保留常规<pre><code>结构。
- 提供API:
  - POST /api/parse:接收markdown原文,返回解析后的HTML;
  - POST /api/save:保存文档(.md文件到docs目录);
  - GET /api/load/:filename:加载已保存文档;
  - GET /api/docs:列出所有.md文件。
- 设置模板引擎为EJS,视图目录为views。
- 启动服务监听8000端口。

完整代码如下(已修正原文中模板字符串错误):
  1. const express = require('express');
  2. const marked = require('marked');
  3. const cors = require('cors');
  4. const bodyParser = require('body-parser');
  5. const fs = require('fs-extra');
  6. const path = require('path');
  7. const app = express();
  8. const renderer = new marked.Renderer();
  9. renderer.code = (code, language) => {
  10.   if (language === 'mermaid') {
  11.     return `<div class="mermaid">${code}</div>`;  // 原文笔误已修正
  12.   }
  13.   return `<pre><code class="language-${language}">${code}</code></pre>`;
  14. };
  15. marked.setOptions({
  16.   renderer: renderer,
  17.   highlight: true,
  18.   breaks: true,
  19.   gfm: true
  20. });
  21. app.use(cors());
  22. app.use(bodyParser.json());
  23. app.use(bodyParser.urlencoded({ extended: true }));
  24. app.use(express.static(path.join(__dirname, 'public')));
  25. app.set('view engine', 'ejs');
  26. app.set('views', path.join(__dirname, 'views'));
  27. const DOCS_DIR = path.join(__dirname, 'docs');
  28. fs.ensureDirSync(DOCS_DIR);
  29. app.get('/', (req, res) => {
  30.   res.render('editor', { title: 'Markdown 在线编辑器 (支持Mermaid)' });
  31. });
  32. app.post('/api/parse', (req, res) => {
  33.   try {
  34.     const { markdown } = req.body;
  35.     if (!markdown) return res.status(400).json({ error: 'Markdown 内容不能为空' });
  36.     const html = marked.parse(markdown);
  37.     res.json({ html });
  38.   } catch (error) {
  39.     res.status(500).json({ error: '解析失败: ' + error.message });
  40.   }
  41. });
  42. app.post('/api/save', (req, res) => {
  43.   try {
  44.     const { filename, content } = req.body;
  45.     if (!filename || !content) return res.status(400).json({ error: '文件名和内容不能为空' });
  46.     fs.writeFileSync(path.join(DOCS_DIR, `${filename}.md`), content, 'utf8');
  47.     res.json({ success: true, message: '保存成功' });
  48.   } catch (error) {
  49.     res.status(500).json({ error: '保存失败: ' + error.message });
  50.   }
  51. });
  52. app.get('/api/load/:filename', (req, res) => {
  53.   try {
  54.     const filePath = path.join(DOCS_DIR, `${req.params.filename}.md`);
  55.     if (!fs.existsSync(filePath)) return res.status(404).json({ error: '文件不存在' });
  56.     const content = fs.readFileSync(filePath, 'utf8');
  57.     res.json({ success: true, content });
  58.   } catch (error) {
  59.     res.status(500).json({ error: '加载失败: ' + error.message });
  60.   }
  61. });
  62. app.get('/api/docs', (req, res) => {
  63.   try {
  64.     const files = fs.readdirSync(DOCS_DIR)
  65.       .filter(file => path.extname(file) === '.md')
  66.       .map(file => ({ name: path.basename(file, '.md'), path: file }));
  67.     res.json({ success: true, docs: files });
  68.   } catch (error) {
  69.     res.status(500).json({ error: '获取列表失败: ' + error.message });
  70.   }
  71. });
  72. const PORT = 8000;
  73. app.listen(PORT, () => {
  74.   console.log(`服务器运行在: http://localhost:${PORT}`);
  75. });
复制代码

## 3. 前端编辑器页面(views/editor.ejs)
前端集成mermaid.js进行客户端渲染。页面包含:
- 控制栏:文件名输入框、保存/加载/刷新预览按钮;
- 左侧textarea编辑区(预填充示例内容);
- 右侧预览容器(id="preview")。

点击“刷新预览”时,通过fetch POST /api/parse获取HTML,然后调用mermaid.run渲染所有mermaid图表。注意mermaid初始化需在DOM解析后执行。

核心模板内容:
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title><%= title %></title>
  7. <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
  8. <style>
  9. /* 略,完整样式可参考原文 */
  10. </style>
  11. </head>
  12. <body>
  13. <div class="header">
  14.   <h1>Markdown 在线编辑器 (支持 Mermaid 流程图)</h1>
  15. </div>
  16. <div class="controls">
  17.   <input type="text" id="filename" placeholder="输入文件名(无需.md)" value="demo">
  18.   <button id="saveBtn">保存文档</button>
  19.   <button id="loadBtn">加载文档</button>
  20.   <button id="refreshBtn">刷新预览</button>
  21. </div>
  22. <div class="container">
  23.   <div class="editor-container">
  24.     <textarea id="editor" placeholder="请输入 Markdown 内容...">
  25. ## Markdown 编辑器 (支持 Mermaid)
  26. ### 功能说明
  27. - 支持标准 Markdown 语法
  28. - 支持 Mermaid 流程图渲染
  29. - 支持文档保存/加载
  30. ### Mermaid 示例
  31. [code]
  32. graph TD
  33. A[开始] --> B{选择操作}
  34. B -->|编辑| C[Markdown 编辑]
  35. B -->|预览| D[实时预览]
  36. C --> E[保存文档]
  37. D --> E
  38. E --> F[结束]
复制代码

普通代码示例
console.log('Hello, Markdown!');
    </textarea>
  </div>
  <div class="preview-container">
    <div id="preview" class="preview"></div>
  </div>
</div>

<script>
// mermaid初始化
mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'loose' });

function refreshPreview() {
  fetch('/api/parse', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ markdown: document.getElementById('editor').value })
  })
  .then(res => res.json())
  .then(data => {
    document.getElementById('preview').innerHTML = data.html;
    mermaid.run({ querySelector: '.mermaid' });
  })
  .catch(err => alert('解析失败: ' + err));
}

document.getElementById('refreshBtn').addEventListener('click', refreshPreview);
// 保存/加载文档逻辑类似,通过fetch调用/api/save和/api/load/:filename
// 完整脚本可参考原文,此处略
</script>
</body>
</html>
[/code]

## 运行与使用
1. 创建目录结构:
  1. md-edit-app/
  2. ├── app.js
  3. ├── views/
  4. │   └── editor.ejs
  5. ├── public/   (空文件夹)
  6. └── docs/     (自动创建)
复制代码
2. 启动服务器:
  1. node app.js
复制代码
3. 打开浏览器访问 http://localhost:8000
4. 在左侧编辑Markdown,点击“刷新预览”查看渲染结果;输入文件名后点击“保存文档”可持久化到服务器;输入已保存文件名点击“加载文档”可恢复内容。

## 技术要点总结
- 服务端使用marked解析Markdown,通过自定义renderer.code将mermaid代码块原样输出为div,避免服务端渲染mermaid,减少Node端依赖。
- 前端引入mermaid.js CDN,在获得HTML后调用mermaid.run()动态渲染图表,支持graph TD、flowchart等主流图表类型。
- 文件操作使用fs-extra的ensureDirSync确保docs目录存在,同步读写适合轻量场景。
- API设计遵循RESTful风格,支持跨域(cors中间件),便于后续扩展或分离前后端。

## 扩展建议
- 添加用户认证、版本管理。
- 集成Markdown扩展如表格、数学公式(Katex)。
- 将文件存储切换为数据库,支持多人协作。
- 优化前端UI,增加自动保存和分屏拖拽等功能。

本方案开箱即用,代码可直接复制运行。通过自定义marked渲染器结合前端mermaid.js,在没有复杂构建工具的情况下实现了客户端流程图渲染,适合快速搭建个人笔记或团队文档工具。
回复

使用道具 举报

发表于 昨天 11:10 | 显示全部楼层

Re: Node.js+Express+marked+mermaid.js:搭建Markdown在线编辑器支持流程图渲染

这个实现很实用,Markdown + Mermaid 正好解决了技术文档里写流程图还得切换到其他工具的痛点。用 marked 自定义渲染器来分离处理,思路清晰。提个小细节:代码里 `highlight: true` 可能不会直接生效,因为 marked 本身没有高亮能力,通常需要配合 highlight.js 或 prism,不过对于 mermaid 渲染不影响。另外如果后续要支持更复杂的 mermaid 主题或动态更新,可以考虑用 `mermaid.run` 取代全局 `mermaid.init`,这样能更好控制渲染时机。整体框架搭得很完整,收藏备用。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-6-14 03:07 , Processed in 0.021942 second(s), 17 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部