在上一篇教程中,我们成功搭建了一个基于 Node.js 的在线 Markdown 编辑服务。然而,基础语法往往无法满足复杂文档的撰写需求。本文将带你深入服务端与前端架构,通过引入中间件和第三方库,为编辑器扩展表格、LaTeX 数学公式、代码高亮等高级功能,同时保持后端代码的整洁与高效。

Why:为何需要扩展 Markdown 解析能力?

标准的 Markdown 语法仅覆盖标题、列表和粗斜体等基础格式。但在技术写作或学术报告场景下,我们经常需要插入数据表格、行内公式或复杂的流程图。为了提升编辑体验,我们需要在服务端增强 Markdown 的解析规则,并在前端引入对应的渲染引擎。

此前我们已经移除了服务端对 Mermaid 的硬依赖,改由前端处理图表渲染,这大大减轻了 Node.js 进程的负担。现在,我们基于同样的思路,通过轻量级的 npm 包来扩展语法糖,让编辑器支持 GFM(GitHub 风格 Markdown)规范。

核心依赖与架构设计

为了实现这些扩展,我们需要在现有的 后端架构 中引入两个关键的中间件库:marked-highlight 用于桥接 marked 解析器与 highlight.js,而 highlight.js 则负责将代码块转换为带语法高亮的 HTML 标签。

在数学公式方面,marked 本身已内置了表格与脚注的支持,我们只需确保启用 GFM 配置即可。至于 LaTeX 公式,通常需要配合前端库(如 MathJax 或 KaTeX)进行渲染,服务端仅负责转义与传递原始语法。

以下是具体的安装命令,用于补充服务端依赖:

npm install marked-highlight highlight.js

架构提示: 这种设计将解析逻辑(服务端)与渲染逻辑(客户端)分离,使得 API 接口返回的仍是标准 HTML 字符串,便于后续存储到数据库或生成静态文件。

服务端逻辑增强(app2.js)

在服务端代码中,我们需要配置 marked 实例,启用 GFM 扩展,并利用 marked-highlight 将代码高亮逻辑注入解析流程。这样当 Markdown 文本经过服务端处理时,输出的 HTML 已包含高亮所需的 class 属性。

以下是修改后的核心服务端代码片段,展示了如何注册中间件并配置解析选项:

// Web Server 程序
const express = require('express');
const marked = require('marked');
const markedHighlight = require('marked-highlight');
const hljs = require('highlight.js');
const cors = require('cors');
const bodyParser = require('body-parser');
const fs = require('fs-extra');
const path = require('path');
// 初始化 Express 应用
const app = express();
// 配置静态文件目录
app.use(express.static(path.join(__dirname, 'public')));
// 配置 Marked 解析器,添加高亮、表格、公式等支持
// 配置代码高亮
const highlightOptions = {
langPrefix: 'hljs language-',
highlight(code, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(code, { language: lang }).value;
} catch (err) {
console.error(`高亮 ${lang} 代码失败:`, err);
}
}
// 自动检测语言
return hljs.highlightAuto(code).value;
}
};
// 创建带高亮的 renderer
const renderer = new marked.Renderer();
// 重写代码块渲染逻辑,识别 mermaid 代码块
renderer.code = (code, language) => {
if (language === 'mermaid') {
// 为 Mermaid 代码块生成容器(仅保留代码,渲染交给前端)
return `<div class="mermaid">${code}</div>`;
}
// 其他代码块使用高亮渲染
return `<pre><code class="hljs language-${language}">${
  highlightOptions.highlight(code, language)
}</code></pre>`;
};
// 配置 marked 核心选项(启用所有扩展)
marked.setOptions({
renderer: renderer,
extensions: [
// 启用表格扩展(GFM 已包含,但显式声明更清晰)
{
name: 'table',
level: 'block',
start: (src) => src.match(/^\|/)?.index
},
// 启用任务列表
{
name: 'tasklist',
level: 'block',
start: (src) => src.match(/^\s*-\s\[\s?x?\]\s/)?.index
}
],
gfm: true,                // 启用 GitHub 风格 Markdown
breaks: true,             // 支持换行符
tables: true,             // 启用表格
tasklists: true,          // 启用任务列表
footnotes: true,          // 启用脚注
smartypants: true         // 启用智能标点
});
// 中间件配置
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));
// 设置模板引擎
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// 确保保存文件的目录存在
const DOCS_DIR = path.join(__dirname, 'docs');
fs.ensureDirSync(DOCS_DIR);
// 路由配置
// 首页 - 编辑器界面
app.get('/', (req, res) => {
res.render('editor', { title: 'Markdown 在线编辑器 (支持表格/公式/Mermaid)' });
});
// 解析 Markdown 为 HTML (API)
app.post('/api/parse', (req, res) => {
try {
const { markdown } = req.body;
if (!markdown) {
return res.status(400).json({ error: 'Markdown 内容不能为空' });
}
// 解析 Markdown 为 HTML
const html = marked.parse(markdown);
res.json({ html });
} catch (error) {
res.status(500).json({ error: '解析 Markdown 失败: ' + error.message });
}
});
// 保存文档 (API)
app.post('/api/save', (req, res) => {
try {
const { filename, content } = req.body;
if (!filename || !content) {
return res.status(400).json({ error: '文件名和内容不能为空' });
}
// 拼接文件路径
const filePath = path.join(DOCS_DIR, `${filename}.md`);
// 写入文件
fs.writeFileSync(filePath, content, 'utf8');
res.json({ success: true, message: '文件保存成功', filePath });
} catch (error) {
res.status(500).json({ error: '保存文件失败: ' + error.message });
}
});
// 加载文档 (API)
app.get('/api/load/:filename', (req, res) => {
try {
const { filename } = req.params;
const filePath = path.join(DOCS_DIR, `${filename}.md`);
// 检查文件是否存在
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: '文件不存在' });
}
// 读取文件内容
const content = fs.readFileSync(filePath, 'utf8');
res.json({ success: true, content });
} catch (error) {
res.status(500).json({ error: '加载文件失败: ' + error.message });
}
});
// 获取文档列表 (API)
app.get('/api/docs', (req, res) => {
try {
// 读取目录下所有 .md 文件
const files = fs.readdirSync(DOCS_DIR)
.filter(file => path.extname(file) === '.md')
.map(file => ({
name: path.basename(file, '.md'),
path: file
}));
res.json({ success: true, docs: files });
} catch (error) {
res.status(500).json({ error: '获取文档列表失败: ' + error.message });
}
});
// 启动服务器
//const PORT = process.env.PORT || 8000;
const PORT = 8000;
app.listen(PORT, () => {
console.log(`服务器运行在: http://localhost:${PORT}`);
console.log(`文档保存目录: ${DOCS_DIR}`);
});

⚠️ 注意: 在修改代码后,需要重启 Node.js 服务才能使新的中间件配置生效。如果使用 nodemon 等监听工具,则会自动重启,无需手动干预。

前端渲染与样式集成

仅有服务端的 HTML 输出还不够,前端必须加载对应的 CSS 主题和 JavaScript 库来美化界面并渲染数学公式。我们需要编辑 views/editor.ejs 模板,在 <head> 中引入高亮主题样式,在页面底部引入公式渲染脚本。

%%PROTTECTED_CODE_2%%

这段代码中,我们引入了 highlight.js 的样式以及 KaTeX 的自动渲染扩展。当 Markdown 预览区的 DOM 更新时,KaTeX 会自动扫描并渲染 $$...$$\(...\) 包裹的公式。

功能测试与文档示例

为了验证扩展功能是否生效,我们需要准备一份包含表格、数学公式、任务列表和代码块的测试文档 docs/demo.md。这份文档将作为编辑器的初始内容,方便我们即时预览效果。

# Markdown 编辑器 (增强版)
## 1. 表格示例
| 姓名 | 年龄 | 职业 |
|------|------|------|
| 张三 | 25   | 工程师 |
| 李四 | 30   | 设计师 |
| 王五 | 28   | 产品经理 |
## 2. 数学公式示例
### 行内公式
欧拉公式:$e^{i\pi} + 1 = 0$
### 块级公式
$$
\int_{0}^{\infty} e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
$$
二次方程求根公式:
$$
x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}
$$
## 3. 任务列表示例
- [x] 完成 Markdown 解析
- [x] 集成表格支持
- [ ] 优化公式渲染
- [ ] 测试兼容性
## 4. 代码高亮示例
```javascript
// JavaScript 代码示例
function calculateArea(radius) {
  const PI = Math.PI;
  return PI * radius * radius;
}
console.log(calculateArea(5)); // 78.53981633974483

为了更直观地展示 Mermaid 流程图的渲染效果,我们可以在文档中嵌入以下图表定义,前端脚本会负责将其转换为可视化的图形界面:

# Python 代码示例
def fibonacci(n):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
print(list(fibonacci(10)))
## 6. 脚注示例
这是一个带脚注的文本[^1],还有另一个脚注[^2]。
[^1]: 第一个脚注的内容
[^2]: 第二个脚注的内容,支持多行
    这是第二行内容
      这是第三行(缩进显示)

验证清单:

  • 表格: 使用 | 列1 | 列2 | 语法,确认边框与对齐样式正常。
  • 数学公式: 行内公式使用 $公式$ 包裹,块级公式使用 $$公式$$ 包裹。
  • 任务列表: 使用 - [x] 表示已完成,- [ ] 表示未完成。
  • 代码高亮: 输入代码块时指定语言,如 ```javascript,观察颜色变化。

目录结构与静态资源部署

为了保证页面样式和脚本能够正确加载,我们需要按照以下目录结构放置文件。所有的 .min.js.css 文件应分别存放于 public/js/public/css/ 目录下。

 md-edit-app/
   ├── app.js  和 app2.js      // 服务器主程序
   ├── views/          // 模板目录
   │   └── edit.ejs 和 editor.ejs  // 编辑器页面
   ├── public/         // 静态资源目录
   │   └── css 和 js  // 静态子目录
   └── docs/      // 文档保存目录 (自动创建)

部署建议: 如果服务器无法访问外网下载资源,建议提前在本地下载好这些静态文件并上传至服务器,避免运行时因网络问题导致样式丢失。

进阶功能与生态延伸

至此,我们的编辑器已具备完整的 GFM 支持。但对于一个生产级应用而言,仍有不少值得完善的方向:

  • 图片上传: 可集成 multer 中间件处理本地图片上传,或对接云存储服务生成外链。
  • 目录生成: 在服务端解析 Markdown 标题结构,通过 API 返回目录树,实现类似 GitBook 的侧边栏导航。
  • 文档导出: 利用 puppeteer 库在服务端将 HTML 转换为 PDF 文件,方便离线阅读。
  • 主题切换: 通过 CSS 变量实现浅色/深色模式切换,并将用户偏好存储在 localStorage 中。
  • 自动保存: 利用 localStorage 定时保存草稿,防止浏览器崩溃导致内容丢失。
[AFFILIATE_SLOT_1]

实践思考: 在扩展功能时,务必考虑数据库存储的兼容性。例如,当用户粘贴包含特殊字符的 LaTeX 公式时,需要对字符串进行转义,防止破坏 JSON 格式或引起 SQL 注入风险。

总结与回顾

本文通过引入 marked-highlighthighlight.js,成功为 Node.js 编写的 Markdown 编辑器添加了表格、数学公式、代码高亮、任务列表和脚注等高级支持。我们通过 服务端 强化解析能力,利用 前端 渲染复杂图表,形成了清晰的前后端分离架构。

这种扩展方式不仅保持了代码的简洁性和可维护性,还让编辑器具备了媲美专业写作软件的功能体验。希望你能根据实际业务场景,继续探索图片上传或实时协作等更多可能。

[AFFILIATE_SLOT_2]