1. 项目概述
这个项目实现了一个完整的Markdown文件浏览系统,前后端分离架构,后端使用Node.js + Express + marked,前端采用Vite + Vue3 + mermaid技术栈。系统核心功能包括:指定目录下的Markdown文件浏览、文件上传、内容渲染(支持Mermaid图表)等。
作为一名全栈开发者,我在实际工作中经常需要处理Markdown文档的展示和管理需求。传统的静态网站生成器虽然功能强大,但缺乏灵活性。而这个方案提供了轻量级的动态解决方案,特别适合需要快速搭建Markdown文档预览系统的场景。
2. 技术选型解析
2.1 后端技术栈
后端选择了Express作为Web框架,主要考虑因素包括:
- 轻量级且成熟稳定
- 中间件生态丰富(如multer处理文件上传)
- 与Node.js原生API配合良好
marked作为Markdown解析器,相比其他方案优势在于:
- 解析速度快
- 支持自定义渲染器
- 活跃的社区维护
2.2 前端技术栈
前端采用Vite + Vue3的组合,主要优势:
- Vite的极速启动和热更新
- Vue3的组合式API更适合复杂交互
- Element Plus提供丰富的UI组件
mermaid用于图表渲染,选择原因:
- 纯前端实现,无需后端支持
- 支持多种图表类型(流程图、时序图等)
- 配置简单,渲染效果好
3. 后端实现详解
3.1 项目初始化与依赖安装
后端项目初始化步骤如下:
bash复制mkdir md-editor-server
cd md-editor-server
npm init -y
npm install express@4 cors multer@1.4.4
npm install fs-extra path marked@12
关键依赖说明:
fs-extra:增强版文件系统模块,提供更多实用方法multer:处理文件上传的中间件marked:Markdown解析器cors:解决跨域问题的中间件
3.2 核心代码解析
3.2.1 服务初始化
javascript复制const express = require('express');
const cors = require('cors');
const multer = require('multer');
const fs = require('fs-extra');
const path = require('path');
const marked = require('marked');
const app = express();
app.use(cors());
app.use(express.json());
3.2.2 目录安全验证
javascript复制function validateAndNormalizeDir(dirPath) {
if (!dirPath || dirPath.trim() === '') {
return DEFAULT_MD_DIR;
}
const absPath = path.resolve(dirPath.trim());
if (!fs.existsSync(absPath)) {
throw new Error(`目录不存在: ${absPath}`);
}
if (!fs.statSync(absPath).isDirectory()) {
throw new Error(`不是有效的目录: ${absPath}`);
}
fs.accessSync(absPath, fs.constants.R_OK);
return absPath;
}
3.2.3 marked配置(支持Mermaid)
javascript复制const renderer = new marked.Renderer();
renderer.code = (code, lang) => {
if (lang === 'mermaid') {
return `<div class="mermaid">${code}</div>`;
}
return `<pre><code class="language-${lang}">${code}</code></pre>`;
};
marked.setOptions({
renderer,
gfm: true,
breaks: true,
sanitize: false
});
3.3 接口实现
3.3.1 获取MD文件列表
javascript复制app.get('/api/get-md-files', (req, res) => {
try {
const dirPath = validateAndNormalizeDir(req.query.dirPath);
const files = fs.readdirSync(dirPath)
.filter(file => {
const ext = path.extname(file).toLowerCase();
return ext === '.md' && !file.startsWith('.');
})
.sort();
res.json(files);
} catch (err) {
console.error('获取文件列表失败:', err);
res.status(400).json({ error: err.message });
}
});
3.3.2 加载并渲染MD文件
javascript复制app.get('/api/load-md-file', (req, res) => {
try {
const filename = decodeURIComponent(req.query.filename || '');
if (!filename || filename.includes('..') || !filename.endsWith('.md')) {
return res.status(400).json({ error: '无效的文件名' });
}
const dirPath = validateAndNormalizeDir(req.query.dirPath);
const filePath = path.join(dirPath, filename);
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: `文件不存在: ${filename}` });
}
const mdContent = fs.readFileSync(filePath, 'utf8');
const html = marked.parse(mdContent);
res.json({ html });
} catch (err) {
console.error('加载MD文件失败:', err);
res.status(400).json({ error: err.message });
}
});
4. 前端实现详解
4.1 项目初始化与依赖安装
前端项目初始化命令:
bash复制npm create vite@latest md-editor-client -- --template vue
cd md-editor-client
npm install axios mermaid@10.9.5 element-plus
关键依赖说明:
axios:HTTP客户端,用于与后端API交互mermaid:图表渲染库element-plus:Vue3版本的Element UI组件库
4.2 核心配置
4.2.1 Vite配置(vite.config.js)
javascript复制import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
export default defineConfig({
plugins: [vue()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:5555',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api')
}
}
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
});
4.2.2 全局配置(main.js)
javascript复制import { createApp } from 'vue';
import App from './App.vue';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import mermaid from 'mermaid';
mermaid.initialize({ startOnLoad: true });
const app = createApp(App);
app.use(ElementPlus);
app.mount('#app');
4.3 核心组件实现
4.3.1 文件列表加载
javascript复制async function loadFileList() {
if (dirPath.value && isInvalidDirPath(dirPath.value)) {
dirError.value = '无效的目录路径格式';
return;
}
loading.value = true;
dirError.value = '';
try {
const params = dirPath.value ? { dirPath: dirPath.value } : {};
const res = await axios.get('/api/get-md-files', { params });
fileList.value = res.data;
ElMessage.success('文件列表加载成功');
} catch (err) {
dirError.value = err.response?.data?.error || '加载失败';
ElMessage.error(dirError.value);
} finally {
loading.value = false;
}
}
4.3.2 Mermaid渲染处理
javascript复制async function loadMdFile(filename) {
loading.value = true;
try {
const params = {
filename,
dirPath: dirPath.value
};
const res = await axios.get('/api/load-md-file', { params });
renderedHtml.value = res.data.html;
await nextTick();
renderMermaid();
ElMessage.success(`加载文件 ${filename} 成功`);
} catch (err) {
ElMessage.error(err.response?.data?.error || '加载文件失败');
} finally {
loading.value = false;
}
}
function renderMermaid() {
const mermaidElements = document.querySelectorAll('.markdown-content .mermaid');
if (mermaidElements.length === 0) return;
mermaid.run();
}
5. 系统部署与运行
5.1 启动步骤
- 启动后端服务:
bash复制cd md-editor-server
node server.js
- 启动前端开发服务器:
bash复制cd md-editor-client
npm run dev
- 访问前端界面:
code复制http://localhost:5173
5.2 目录结构说明
完整项目目录结构如下:
code复制md-editor/
├── md-editor-server/ # 后端项目
│ ├── node_modules/
│ ├── md-files/ # 默认Markdown文件存储目录
│ ├── package.json
│ └── server.js # 后端主入口文件
└── md-editor-client/ # 前端项目
├── node_modules/
├── public/
├── src/
│ ├── App.vue # 主组件
│ └── main.js # 前端入口文件
├── package.json
└── vite.config.js # Vite配置文件
6. 功能扩展与优化建议
6.1 功能扩展
-
文件编辑功能:
- 集成monaco-editor提供代码编辑体验
- 实现保存功能,将修改内容写回原文件
-
文件管理功能:
- 添加新建文件功能
- 实现文件删除和重命名
-
增强渲染功能:
- 集成highlight.js实现代码高亮
- 支持LaTeX数学公式渲染
6.2 性能优化
-
前端缓存:
- 对已加载的文件内容进行缓存
- 实现LRU缓存策略控制内存使用
-
后端优化:
- 添加文件内容缓存
- 实现增量读取大文件
-
渲染优化:
- 对Mermaid图表实现懒加载
- 添加渲染进度指示器
7. 常见问题与解决方案
7.1 Mermaid图表不渲染
问题现象:
图表代码块显示为源代码,未转换为图形
解决方案:
- 确保已正确初始化mermaid:
javascript复制mermaid.initialize({ startOnLoad: false });
- 在内容更新后手动触发渲染:
javascript复制nextTick().then(() => {
mermaid.run();
});
7.2 文件上传失败
可能原因:
- 文件大小超过限制(默认5MB)
- 文件类型不符合要求(非.md/.txt)
- 后端服务未正确配置CORS
排查步骤:
- 检查浏览器控制台网络请求
- 查看后端日志输出
- 测试直接调用API接口
7.3 目录加载失败
常见错误:
- 路径格式不正确
- 目录不存在
- 无读取权限
处理方法:
javascript复制function isInvalidDirPath(path) {
if (!path || path.trim() === '') return false;
const trimedPath = path.trim();
const winDriveRegex = /^[a-zA-Z]:[\\/]/;
const unixRootRegex = /^\/|^~/;
const isWindows = winDriveRegex.test(trimedPath);
const isUnix = unixRootRegex.test(trimedPath);
if (!isWindows && !isUnix) return true;
const illegalChars = /[<>"|?*]/;
if (illegalChars.test(trimedPath)) return true;
return false;
}
8. 安全注意事项
-
路径安全:
- 始终验证用户输入的路径
- 使用path.resolve处理相对路径
- 检查路径遍历攻击(如包含"..")
-
文件上传:
- 限制上传文件类型和大小
- 对上传内容进行安全检查
- 不要直接执行上传的文件
-
XSS防护:
- 对渲染的HTML内容进行过滤
- 设置合适的CSP策略
- 避免使用innerHTML直接插入未经验证的内容
9. 项目总结与个人体会
在实际开发过程中,有几个关键点值得特别注意:
-
Mermaid渲染时机:必须确保DOM更新完成后再调用mermaid.run(),这是最容易出错的地方。我通过nextTick()确保渲染顺序正确。
-
路径处理安全:任何时候处理用户提供的路径时,都必须进行严格的验证和规范化,防止目录遍历攻击。
-
跨域问题:开发环境下配置Vite代理解决,生产环境需要正确配置CORS或使用同域部署。
-
性能考量:对于大型Markdown文件(特别是包含多个Mermaid图表),需要考虑分块渲染或虚拟滚动优化。
这个项目虽然不大,但涵盖了前后端协作的多个关键技术点,包括文件处理、Markdown解析、图表渲染等。在实际应用中,可以根据需求进一步扩展功能,比如添加用户认证、实现文件版本控制等。