博客管理面板实现原理与使用说明 🛠️
前言
在把博客搭起来之后,我发现“写文章”这件事光靠手动改 Markdown 文件还是太原始了: 要记文件路径、要手写 frontmatter、要手动创建目录、还要担心 YAML 格式写错。 于是便有了这套轻量级的博客管理面板:一个浏览器里的可视化编辑器,加上一个本地后端服务,直接管理 docs/posts/ 下的所有文章。
这篇文章就总结一下它的实现原理和使用说明,方便以后回看,也顺便把这个工具本身记录下来。
整体架构
管理面板采用最简单的前后端分离结构:
┌──────────────┐ HTTP API ┌──────────────────┐
│ 浏览器前端 │ <──────────────> │ admin-server.mjs │
│ admin/ │ │ (Node.js) │
└──────────────┘ └────────┬─────────┘
│
│ 读写文件
▼
docs/posts/*.md- 前端:纯 HTML + 原生 ES Module,没有框架依赖。
- 后端:
admin-server.mjs,基于 Node.js 内置http模块,零额外依赖。 - 存储:没有数据库,直接操作 Markdown 文件,博客源码本身就是数据。
前端实现
1. 模块化结构
前端代码按职责拆成几个模块,全部放在 admin/js/ 下:
| 模块 | 职责 |
|---|---|
state.js | 全局状态:当前文章、文件树、编辑器实例、脏标记等 |
eventBus.js | 轻量事件总线,模块间解耦通信 |
api.js | 封装对后端 API 的调用 |
editor.js | 封装 Vditor 初始化与读写操作 |
ui.js | 所有 DOM 操作与样式切换 |
fileTree.js | 左侧文件树渲染、折叠、搜索过滤与拖拽移动 |
postManager.js | 新建 / 打开 / 保存 / 删除 / 移动文章的业务逻辑 |
utils.js | HTML 转义、文件名生成、轻量 frontmatter 解析等工具函数 |
main.js | 入口模块:初始化编辑器、加载配置、绑定全局事件 |
每个模块只负责自己的事,不越界。比如 editor.js 只操作 Vditor,ui.js 只读写 DOM。
2. Vditor 编辑器
编辑器选用 Vditor,因为它同时支持:
- 所见即所得(WYSIWYG)
- 即时渲染(IR):类似 Typora,输入即渲染
- 分屏预览(SV)
为了做到离线可用,我把 Vditor 的资源从 node_modules/vditor 映射到 /vditor-assets,不依赖外部 CDN:
cdn: '/vditor-assets',当前默认模式是 即时渲染(IR),并隐藏了模式菜单里的“所见即所得”选项。还扩展了表情字典,把默认较少的 emoji 换成了 240+ 个常用表情,覆盖表情、人物、动物、食物、物品、符号等类别。
3. 左侧文件树
文件树从后端获取整棵 docs/posts/ 目录树,递归渲染为可折叠的目录和文件列表。
顶部搜索框会实时过滤文件名,方便文章多了之后快速定位。每个目录后面还会显示文件数量徽章,一眼就能看到该分类下有几篇文章。
拖拽移动:可以直接把文件拖拽到另一个目录上,松开即可完成移动。前端调用 POST /api/move,后端用 fs.rename 完成重定位,并尝试清理空目录。移动时会检查:
- 源路径是否只读(只读文章无法移动)
- 目标位置是否已存在同名文件(防止覆盖)
- 路径是否仍在
docs/posts/范围内(防止目录穿越)
提示
移动后 VitePress 会自动检测 docs/posts/ 下 .md 文件的变化并重启,刷新浏览器即可看到新侧边栏。
后端实现
1. REST API
admin-server.mjs 只暴露了六个接口:
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /api/config | 返回前端所需配置(标题、预览地址等) |
GET | /api/posts | 递归列出所有文章,返回文件树 |
GET | /api/post?path=xxx | 读取单篇文章,解析 frontmatter 和正文 |
PUT | /api/post | 创建或更新文章,自动建目录 |
POST | /api/move | 将文章移动到指定目录 |
DELETE | /api/post?path=xxx | 删除文章 |
2. frontmatter 解析
每篇文章顶部有标准的 YAML frontmatter:
---
title: 文章标题
date: 2026-07-01
category: 工具
---后端用一个简单的正则匹配 ---\n...\n--- 块,再用行级正则提取 key: value,不引入 YAML 解析库,保持轻量。
3. 安全防范
所有文件操作都经过 resolveSafe() 函数:
function resolveSafe(subPath) {
const resolved = path.resolve(POSTS_DIR, subPath)
if (!resolved.startsWith(POSTS_DIR)) throw new Error('路径越权: ' + subPath)
return resolved
}这样可以防止通过 ../ 等路径穿越攻击,确保只能读写 docs/posts/ 下的文件。
4. 只读保护
为了避免误改或误删关键文章,加入了两种只读保护机制:
- 文章级:在 frontmatter 中设置
readonly: true,该文章打开后编辑器、元数据表单和保存按钮都会被禁用,删除也会失败。 - 配置级:在
admin/config.json的protectedFiles数组中填入 glob 路径(如blog-admin-panel.md、life/*.md),后端会用简单的*/?/**通配符匹配进行保护。
只读检查同时运行在保存、删除和移动接口中,后端会通过读取目标文件现有 frontmatter 来判断。
使用说明
1. 启动管理面板
在项目根目录执行:
node admin-server.mjs默认会监听 3456 端口,控制台会输出访问地址:
http://localhost:34562. 基本操作
打开后台后,界面分为左右两部分:
- 左侧:文件树 + 搜索框 + 新建按钮
- 右侧:编辑器 + 元数据表单 + 保存/删除/预览按钮
常用操作:
| 操作 | 方式 |
|---|---|
| 新建文章 | 点击左侧「新建文章」,填写标题、分类、文件名 |
| 编辑文章 | 点击左侧文件树中的文件名 |
| 保存 | Ctrl + S 或右上角「保存」按钮 |
| 删除 | 右上角「删除」按钮 |
| 移动文章 | 把文件拖拽到左侧目标目录上 |
| 预览 | 右上角「预览」按钮,会打开 VitePress 开发服务器对应页面 |
3. 新建文章
新建文章时会自动生成文件名(基于日期和标题拼音/首字母),也可以手动修改。保存后文件会立即出现在 docs/posts/ 对应目录下。
4. 分类与路径
文章分类对应目录结构。选择分类后,文章会保存到该分类子目录中;选择根目录则保存在 docs/posts/ 根目录。
5. 与 VitePress 的自动联动
为了让侧边栏与目录结构保持同步,VitePress 配置 docs/.vitepress/config.mts 做了两件事:
- 自动生成侧边栏:启动时会扫描
docs/posts/,把每个子目录映射成一个分组(如testing/→🧪 测试笔记),组内index.md作为概述,其余文章按文件名排序。根目录下的.md文件会进入「📄 未分类文章」分组。 - 文件监听自动重启:配置里注册了一个 Vite 插件
watchPostsSidebar,监听docs/posts/下.md文件的新增和删除。当在管理面板中移动、新建或删除文章时,插件会自动触发 VitePress 重启,重新生成侧边栏。
所以,现在在管理面板里调整文章目录后,只要刷新浏览器页面,就能看到最新的侧边栏,不需要再手动重启 VitePress 了。
当前的一些取舍
这套工具 intentionally 保持简单,所以有几个特点:
- 无数据库:所有数据就是 Markdown 文件,方便用 Git 管理。
- 无认证:本地使用,不处理登录;如果要部署到公网,需要额外加一层认证。
- 无图片上传:目前图片还是通过 VitePress 的
public/目录或外部图床管理,后续可以考虑扩展上传功能。
可移植性:如何迁移到另一个 VitePress 博客
为了让这套管理面板能在多个 VitePress 项目之间复用,我把所有可能变化的项目信息抽离到了 admin/config.json:
{
"title": "博客管理面板",
"postsDir": "docs/posts",
"port": 3456,
"previewUrl": "http://localhost:5173",
"previewPathPrefix": "/posts/",
"assetsPrefix": "/vditor-assets",
"protectedFiles": []
}其中:
| 字段 | 含义 | 迁移时是否需要修改 |
|---|---|---|
title | 面板标题 | 是 |
postsDir | Markdown 文章存放目录 | 是 |
port | 管理后台服务端口 | 可选 |
previewUrl | VitePress 开发服务器地址 | 是 |
previewPathPrefix | 预览时的路径前缀 | 是 |
assetsPrefix | 本地 Vditor 资源前缀 | 通常不动 |
迁移步骤
- 把
admin/目录和admin-server.mjs复制到目标 VitePress 项目根目录。 - 确认目标项目已安装
vditor:bashnpm install vditor - 编辑
admin/config.json:- 修改
postsDir为目标项目的文章目录,例如docs/blog。 - 修改
previewUrl与previewPathPrefix以匹配 VitePress 路由。
- 修改
- 在
package.json中添加启动脚本:json"admin": "node admin-server.mjs" - 启动:bash
npm run admin
环境变量覆盖
如果同一台机器上同时管理多个博客,端口可能冲突,这时无需修改配置文件,直接用环境变量覆盖:
# Windows CMD
set ADMIN_PORT=3457 && node admin-server.mjs
# Windows PowerShell
$env:ADMIN_PORT=3457; node admin-server.mjs
# macOS / Linux
ADMIN_PORT=3457 node admin-server.mjs支持的环境变量:
ADMIN_POSTS_DIR— 覆盖postsDirADMIN_PORT— 覆盖portADMIN_PREVIEW_URL— 覆盖previewUrlADMIN_PREVIEW_PREFIX— 覆盖previewPathPrefix
不引入线上部署风险
admin/ 目录和 admin-server.mjs 位于仓库根目录,但 VitePress 构建时只处理 docs/ 目录下的内容,因此:
- 默认情况下,
admin/和admin-server.mjs不会被打包进docs/.vitepress/dist。 - 上线部署时只发布
docs/.vitepress/dist即可,管理面板文件不会暴露到公网。 - 目前管理面板没有鉴权,仍然建议仅在本地开发环境使用。
近期面板还增加了几个实用细节:
- 未保存标记:当编辑中的文章与上次保存内容不一致时,左侧文件树对应文件后会显示红色
*,顶部文件路径旁也会同步显示,避免误以为自己已经保存。 - 只读保护:在文章 frontmatter 中设置
readonly: true,或在admin/config.json的protectedFiles列表中添加路径,可防止文章被保存或删除。只读文章打开后编辑器、元数据表单和保存按钮都会自动禁用。 - 描述 / 摘要字段:frontmatter 表单增加了
description输入框,可直接写入文章描述,方便在列表或摘要中展示。
总结
博客管理面板虽然不大,但已经覆盖了日常写作的核心流程:浏览、新建、编辑、保存、删除、移动、预览。 它让写博客的体验从“改文件”变成了“打开网页就能写”,而底层依然是最干净的 Markdown 文件。
后续如果还想继续完善,可以考虑加入:
- [X] 侧边栏按目录自动生成
- [X] 文件变化后自动重启 VitePress
- [X] 拖拽移动文章
- [X] 只读保护
- [ ] 图片/附件上传
- [ ] 文章草稿状态
- [ ] 更完整的 frontmatter 字段(如标签、封面图)
- [ ] 部署前的 Git 自动提交钩子
但就目前而言,它已经够用了。
📝 工具是为人服务的,不要让工具本身变成负担。能用、好用、够用,就是最好的状态。