花了两个晚上把博客彻底重构了一遍:后端 44 个任务逐一落地,测试 41/41 全绿。写代码的快乐,就是把坑一个个填平。这篇把整个过程的思路、取舍和踩过的坑都记下来,给未来的自己留一盏灯。
为什么重构
旧版博客是前后端揉在一起的 Express 单体,用了两年之后问题开始集中爆发:
- 改一处动全身:加一个接口要在三四个文件里同步改,样式和逻辑混在一个目录里;
- 上线靠手感:没有测试,每次部署都要手动点一遍核心流程,改完心里没底;
- 主题无法扩展:CSS 变量只有一层,想加「极简阅读」这类第二主题,牵一发动全身。
重构的目标很朴素:让每个服务能独立启动、独立部署、独立测试,同时给未来的功能(评论通知、影音展示、自动部署)留好接口。
服务拆分
最终拆成了三个互不依赖的服务,各自用最合适的工具:
mblog/
├── backend/ # Hono + Drizzle + better-sqlite3,纯 API
├── site/ # Astro 5 SSR,前台渲染
└── admin/ # Vue 3 SPA,后台管理
| 服务 | 技术栈 | 职责 |
|---|---|---|
| backend | Hono + SQLite | 全部数据接口:文章、评论、友链、统计、RSS |
| site | Astro 5 SSR | 前台页面:首页、文章、归档、影音、项目 |
| admin | Vue 3 SPA | 管理后台:发文、审评论、同步豆瓣、站点设置 |
拆分的核心收益是边界清晰:site 只关心渲染,admin 只关心编辑,backend 只关心数据和规则。任何一个服务可以单独换技术栈,不影响另外两个。
踩坑记录
路由 base 双重前缀
后台部署在 /admin/ 子路径下,Vite 配了 base: '/admin/',结果路由又在组件里拼了一次前缀,首页白屏。排查半小时,最后发现是 API 请求路径变成了 /admin/admin/posts。
教训:路径只拼一次。base 配置好了之后,所有内部跳转都用相对路径或路由方法,不要手动拼前缀。
FTS 中文分词
SQLite 自带的 unicode61 分词器对中文很不友好:它把连续的中文当一个 token,搜「博客」搜不到「重构博客的笔记」。解决方案是逐字分词——写入时把正文拆成单字再进 FTS 表:
-- 写入时:正文内容 -> 正 文 内 容
INSERT INTO posts_fts(title, content_md)
VALUES ('博 客', '重 构 记 录');
配合 quote() 转义查询词,中文子串搜索就通了。代价是 FTS 表体积变大,但个人博客完全可接受。
数据库迁移
better-sqlite3 不支持在线改表结构,前期加字段全靠 drizzle-kit generate 生成增量迁移。有次手改迁移文件顺序错了,本地库和线上库结构不一致,排查了很久。
教训:迁移文件只能追加,不能修改。改已应用的迁移 = 给自己埋雷。
双主题架构
这次顺便做了双主题:正常主题(暗色科技风,首页内容丰富)和极简阅读(暖色、衬线、时间线)。核心是两个机制:
- CSS 变量 + 主题前缀:所有规则都挂在
[data-theme='normal']/[data-theme='reader']下,切换主题只是换html[data-theme],零 JS 重绘; - localStorage 记忆:首帧前用内联脚本恢复用户上次的选择,刷新不闪主题。
两个主题的首页结构完全不同(模块卡片 vs 干支时间线),所以页面做了双渲染,靠 CSS 显隐切换,互不干扰。
测试与质量
后端用了 Vitest,8 个测试文件、63 个用例覆盖核心路径:
- 登录与限流(含 429 边界)
- 文章的增删改查与分页搜索
- 评论的审核、回复、批量操作
- 豆瓣抓取的分页与 TMDB 封面匹配
- markdown 清洗(script / onerror / javascript: 注入)
测试的收益立竿见影:重构最后一天我改了统计接口的聚合逻辑,跑一遍测试立刻发现一个边界 bug。没有测试的改动叫冒险,有测试的改动叫迭代。
后续规划
- 评论的邮件通知,文章被评论时第一时间知道;
- 豆瓣影音的自动同步脚本,定时拉取观影记录;
- 一套自动化部署:
git push后自动构建镜像、滚动更新; - 更多主题变量暴露到后台,让换肤不需要改代码。
重构不是终点,是让下一次改动更轻松的手段。写完这篇,继续填下一个坑。
评论
评论加载中…