Skip to content

博客管理面板实现原理与使用说明 🛠️

前言

在把博客搭起来之后,我发现“写文章”这件事光靠手动改 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.jsHTML 转义、文件名生成、轻量 frontmatter 解析等工具函数
main.js入口模块:初始化编辑器、加载配置、绑定全局事件

每个模块只负责自己的事,不越界。比如 editor.js 只操作 Vditor,ui.js 只读写 DOM。

2. Vditor 编辑器

编辑器选用 Vditor,因为它同时支持:

  • 所见即所得(WYSIWYG)
  • 即时渲染(IR):类似 Typora,输入即渲染
  • 分屏预览(SV)

为了做到离线可用,我把 Vditor 的资源从 node_modules/vditor 映射到 /vditor-assets,不依赖外部 CDN:

js
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:

yaml
---
title: 文章标题
date: 2026-07-01
category: 工具
---

后端用一个简单的正则匹配 ---\n...\n--- 块,再用行级正则提取 key: value,不引入 YAML 解析库,保持轻量。

3. 安全防范

所有文件操作都经过 resolveSafe() 函数:

js
function resolveSafe(subPath) {
  const resolved = path.resolve(POSTS_DIR, subPath)
  if (!resolved.startsWith(POSTS_DIR)) throw new Error('路径越权: ' + subPath)
  return resolved
}

这样可以防止通过 ../ 等路径穿越攻击,确保只能读写 docs/posts/ 下的文件。

4. 只读保护

为了避免误改或误删关键文章,加入了两种只读保护机制:

  1. 文章级:在 frontmatter 中设置 readonly: true,该文章打开后编辑器、元数据表单和保存按钮都会被禁用,删除也会失败。
  2. 配置级:在 admin/config.jsonprotectedFiles 数组中填入 glob 路径(如 blog-admin-panel.mdlife/*.md),后端会用简单的 */?/** 通配符匹配进行保护。

只读检查同时运行在保存、删除和移动接口中,后端会通过读取目标文件现有 frontmatter 来判断。

使用说明

1. 启动管理面板

在项目根目录执行:

bash
node admin-server.mjs

默认会监听 3456 端口,控制台会输出访问地址:

http://localhost:3456

2. 基本操作

打开后台后,界面分为左右两部分:

  • 左侧:文件树 + 搜索框 + 新建按钮
  • 右侧:编辑器 + 元数据表单 + 保存/删除/预览按钮

常用操作:

操作方式
新建文章点击左侧「新建文章」,填写标题、分类、文件名
编辑文章点击左侧文件树中的文件名
保存Ctrl + S 或右上角「保存」按钮
删除右上角「删除」按钮
移动文章把文件拖拽到左侧目标目录上
预览右上角「预览」按钮,会打开 VitePress 开发服务器对应页面

3. 新建文章

新建文章时会自动生成文件名(基于日期和标题拼音/首字母),也可以手动修改。保存后文件会立即出现在 docs/posts/ 对应目录下。

4. 分类与路径

文章分类对应目录结构。选择分类后,文章会保存到该分类子目录中;选择根目录则保存在 docs/posts/ 根目录。

5. 与 VitePress 的自动联动

为了让侧边栏与目录结构保持同步,VitePress 配置 docs/.vitepress/config.mts 做了两件事:

  1. 自动生成侧边栏:启动时会扫描 docs/posts/,把每个子目录映射成一个分组(如 testing/🧪 测试笔记),组内 index.md 作为概述,其余文章按文件名排序。根目录下的 .md 文件会进入「📄 未分类文章」分组。
  2. 文件监听自动重启:配置里注册了一个 Vite 插件 watchPostsSidebar,监听 docs/posts/.md 文件的新增和删除。当在管理面板中移动、新建或删除文章时,插件会自动触发 VitePress 重启,重新生成侧边栏。

所以,现在在管理面板里调整文章目录后,只要刷新浏览器页面,就能看到最新的侧边栏,不需要再手动重启 VitePress 了。

当前的一些取舍

这套工具 intentionally 保持简单,所以有几个特点:

  • 无数据库:所有数据就是 Markdown 文件,方便用 Git 管理。
  • 无认证:本地使用,不处理登录;如果要部署到公网,需要额外加一层认证。
  • 无图片上传:目前图片还是通过 VitePress 的 public/ 目录或外部图床管理,后续可以考虑扩展上传功能。

可移植性:如何迁移到另一个 VitePress 博客

为了让这套管理面板能在多个 VitePress 项目之间复用,我把所有可能变化的项目信息抽离到了 admin/config.json

json
{
  "title": "博客管理面板",
  "postsDir": "docs/posts",
  "port": 3456,
  "previewUrl": "http://localhost:5173",
  "previewPathPrefix": "/posts/",
  "assetsPrefix": "/vditor-assets",
  "protectedFiles": []
}

其中:

字段含义迁移时是否需要修改
title面板标题
postsDirMarkdown 文章存放目录
port管理后台服务端口可选
previewUrlVitePress 开发服务器地址
previewPathPrefix预览时的路径前缀
assetsPrefix本地 Vditor 资源前缀通常不动

迁移步骤

  1. admin/ 目录和 admin-server.mjs 复制到目标 VitePress 项目根目录。
  2. 确认目标项目已安装 vditor
    bash
    npm install vditor
  3. 编辑 admin/config.json
    • 修改 postsDir 为目标项目的文章目录,例如 docs/blog
    • 修改 previewUrlpreviewPathPrefix 以匹配 VitePress 路由。
  4. package.json 中添加启动脚本:
    json
    "admin": "node admin-server.mjs"
  5. 启动:
    bash
    npm run admin

环境变量覆盖

如果同一台机器上同时管理多个博客,端口可能冲突,这时无需修改配置文件,直接用环境变量覆盖:

bash
# 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 — 覆盖 postsDir
  • ADMIN_PORT — 覆盖 port
  • ADMIN_PREVIEW_URL — 覆盖 previewUrl
  • ADMIN_PREVIEW_PREFIX — 覆盖 previewPathPrefix

不引入线上部署风险

admin/ 目录和 admin-server.mjs 位于仓库根目录,但 VitePress 构建时只处理 docs/ 目录下的内容,因此:

  • 默认情况下,admin/admin-server.mjs 不会被打包进 docs/.vitepress/dist
  • 上线部署时只发布 docs/.vitepress/dist 即可,管理面板文件不会暴露到公网。
  • 目前管理面板没有鉴权,仍然建议仅在本地开发环境使用。

近期面板还增加了几个实用细节:

  • 未保存标记:当编辑中的文章与上次保存内容不一致时,左侧文件树对应文件后会显示红色 *,顶部文件路径旁也会同步显示,避免误以为自己已经保存。
  • 只读保护:在文章 frontmatter 中设置 readonly: true,或在 admin/config.jsonprotectedFiles 列表中添加路径,可防止文章被保存或删除。只读文章打开后编辑器、元数据表单和保存按钮都会自动禁用。
  • 描述 / 摘要字段:frontmatter 表单增加了 description 输入框,可直接写入文章描述,方便在列表或摘要中展示。

总结

博客管理面板虽然不大,但已经覆盖了日常写作的核心流程:浏览、新建、编辑、保存、删除、移动、预览。 它让写博客的体验从“改文件”变成了“打开网页就能写”,而底层依然是最干净的 Markdown 文件。

后续如果还想继续完善,可以考虑加入:

  • [X] 侧边栏按目录自动生成
  • [X] 文件变化后自动重启 VitePress
  • [X] 拖拽移动文章
  • [X] 只读保护
  • [ ] 图片/附件上传
  • [ ] 文章草稿状态
  • [ ] 更完整的 frontmatter 字段(如标签、封面图)
  • [ ] 部署前的 Git 自动提交钩子

但就目前而言,它已经够用了


📝 工具是为人服务的,不要让工具本身变成负担。能用、好用、够用,就是最好的状态。

💬 评论区

总会有好事覆盖坏记忆!