Maltose:15 天重构博客,先写了 29 份 ADR 才敢写代码
本文发布于20天前,最后更新于 11 天前,其中的信息可能已经有所发展或是发生改变。

我本来只想给博客换个皮肤。
结果 15 天后,我有了一个 enterprise 级博客、29 份架构决策文档,和一颗想回到“只改改样式”那天的心。


🤯 开场:这博客到底经历了什么

事情是这样的——我有个基于 WordPress 的老博客,跑着一个叫 Argon 的主题。它很好,但我手痒了。

于是我决定“重构”一下。就是那种“我就改改样式”的重构。

15 天之后:

  • 前端的 WordPress 主题没了,变成了 Astro 7 + React 19 的 headless 架构
  • 项目名字从一开始就是 Maltose(麦芽糖)——没什么宏大叙事,就是觉得可爱又好听,以后我养的猫也要叫这个名字
  • 包管理器从 npm 换成了 pnpm(npm 的 node_modules 像盘根错节的树根)
  • 代码没写几行,架构决策文档先写了 29 份

29 份是什么概念?就是你还没开始写 Hello World,已经写了一篇硕士论文的“研究背景”。

但这套“先立字据再动手”的 ADR(Architecture Decision Record,架构决策记录)工作流,后来救了我不止一次。这个系列就是想把这 15 天的坑、顿悟和翻车现场,全部交代一遍。

今天是第 1 篇:为什么要重构、ADR 文化怎么形成的、以及第一个大坑——浏览量计数。


🐒 踩坑 01:浏览量计数——当你的计数器和数据库各说各话

先说第一个大坑,因为它直接决定了后面整个缓存架构的走向。

老博客的浏览量用的是 WP-PostViews 插件——经典的老牌插件,能用,但有个问题:它和主题深度耦合,而且它的数据格式(存在 postmeta 里)有点像“文档说 1.0,代码用 1.1”的祖传协议。

我的需求很简单:浏览量要实时(刷新就要最新),要能自己控制(不想被插件绑架)。

于是第一版方案(记录在 ADR-0002/0003):

  1. 在 Maltose 主题里注册 WPGraphQL 字段 viewCount,自己读写 postmeta(WP 的文章附属数据,相当于文章的便签墙)
  2. 为了“兼容老数据”,刻意复用 WP-PostViews 的 views——这样老文章的浏览量不会清零

结果第一个坑就来了:

// 主题里注册 viewCount 字段
register_graphql_field('Post', 'viewCount', [
    'type' => 'Int',
    'resolve' => function ($post) {
        // 这里的 $post 是 WPGraphQL 的 Model 对象,不是原生 WP_Post!
        // 我第一次直接当 WP_Post 用,喜提 500 错误
        $views = get_post_meta($post->databaseId, 'views', true);
        return (int) $views;
    },
]);

报错现场是这样的:

Cannot return null for non-nullable field "Post.databaseId"

翻译成人话:你给我的对象不对,我拿不到它的身份证。

WPGraphQL 的 Post 类型解析器要求的是 WPGraphQLModelPost(它的模型对象),不是 WordPress 原生的 WP_Post。这俩长得像,但不是一个人——就像你给前台递了张身份证复印件,她只认原件。

修复其实一行的事(用 array_map 包一层 Model),但教训很深刻:

GraphQL 字段的 resolve 函数,签名里的“Post”未必是你以为的那个 Post。

这个坑后来在第 2 篇的“WPGraphQL 静默 100 条上限”那里又升级了——GraphQL 不仅对象会骗你,连数量都会骗你。


💡 灵光 01:ADR 工作流是怎么长出来的

很多人问:写个博客而已,为什么要整 ADR 这种“大厂流程”?

答案:因为我踩的坑,80% 都是“上次改的时候没想到”。

拿浏览量的例子来说。第一版方案里,我为了让浏览量实时,设计了三层缓存 + 主动失效

浏览器 → nginx → Astro SSR(Apollo InMemoryCache + cache-first)
              ↘ WordPress(postmeta 实时读写)

保存文章时 → WP save_post 钩子 → 签名 POST → Astro 清缓存

这个方案(ADR-0003)当时觉得天衣无缝。结果一周内被自己打了三次脸:

  • 评论编辑后浏览量不刷新(缓存清的是文章,不是评论)
  • 前端多了一个“渐进更新”的 Provider,每次首页加载多一次请求
  • 排查陈旧数据时,签名代理 + Apollo 缓存 + WP transient 三层叠加,头都大了

最后干脆推倒重来:全局 network-only(每次真实打 WP),缓存什么的不要了

那一刻我悟了:

与其设计一个“聪明的缓存失效机制”,不如想清楚谁才配拥有缓存

这套“设计 → 翻车 → 推翻 → 再设计”的过程,如果每次都用脑子记,第 5 个坑就记混了。所以从第 2 天起,每个决策我都写成 ADR:背景、决策、备选方案、为什么没选备选。

29 份 ADR 就是这么攒出来的。它不是什么流程合规,它是我给自己留的防失忆保险


🐒 踩坑 02:安全审查四连——你以为没人看你博客,但坏人会

重构的第 3 天,我做了一次“如果我是坏人,我会怎么打这个博客”的头脑风暴。收获很丰富,都是血泪:

坑 A:CORS 开的是 *

// 重构前的“豪放”写法
const headers = {
  "Access-Control-Allow-Origin": "*", // 任何网站都能调我的 API
};

* 意味着:任何恶意网站都能在浏览器里向你的 API 发请求。等于你家大门没锁,还挂了块“欢迎光临”的牌子。

修复:白名单 + 只对允许的来源回 CORS 头。

坑 B:评论内容 XSS——让用户的浏览器替你打工

评论是 dangerouslySetInnerHTML(React 直接插 HTML 的方式)渲染的,如果不消毒,用户发一条 <script>偷cookie</script> 的评论,全站访客的 cookie 就没了。

修复:DOMPurify 消毒(HTML 消毒库,等于给用户输入装了个安检机)。

坑 C:JWT 里的用户 ID,是字符串还是数字?

鉴权 JWT(JSON Web Token)里存了用户 ID。判断“这条评论是不是我发的”时,1 === "1"false(严格相等),导致你自己发的评论,系统不认账——一个类型不对就让你连自己都删不掉自己的评论。

修复:统一 String() 归一化。类型要严谨,不然生活(和评论权限)会教做人。

坑 D:信任了浏览器的 User-Agent

记录评论来源时,我直接用了请求头的 User-Agent(浏览器自报家门的字符串)。但 UA 是客户端随便填的——坏人可以伪装成任何设备。

修复:服务端解析 + 只存“设备类型”这种低敏信息,不存原始 UA。

这四连的教训浓缩成一句话:

你的博客可能没多少人看,但一定有坏人扫描。安全审查不是 KPI,是底线。


🎉 中场:评论系统的“8 连击马拉松”

主体架构搞定后,我花了一整天打磨评论系统——不是功能,是UI 细节的 8 个 commit

  1. 气泡样式对齐 shadcn 的 Message 架构
  2. 头像顶部对齐(评论气泡和头像谁高谁低,强迫症大战)
  3. 配色映射到主题色板
  4. 头像加载失败的回退
  5. 嵌套回复的弹窗导航
  6. 提及(@)颜色可读性
  7. 提及自动注入到第一条评论
  8. ……以及记不清的第 8 个

为什么这么较真?因为评论区是一个博客最像“社区”的地方。技术上它只是 CRUD(增删改查),体验上它是你的“客厅”。

没有人因为评论框圆角多 2px 而关注你,但所有人会因为评论框用着别扭而离开。

这 8 个 commit 的恩怨情仇,如果反响好我考虑单开一篇。


🔮 下篇预告

第 1 篇到这里。今天讲了:为什么重构、ADR 文化怎么来的、浏览量的坑、安全四连、评论马拉松。

但真正的重头戏还没开始——下一篇,我要给博客装一个“会自我修复的缓存大脑”(LruLink),然后让三个 feature 分支像三条龙一样并行开发、合并——还失败了一次。

缓存这东西,你以为是帮手,其实是前任:你以为ta走了,其实ta一直住在你家客厅,还把你的现任吓跑了。

下一篇:《我的缓存会自我修复:LruLink 与三条并行分支的合并舞步》

评论

  1. Windows Edge 151.0.0.0
    3 周前
    2026-8-21 11:06:34

    这咋很强的deepseek味道,我重构博客,让他自己填充几篇文章,deepseek也是这样给我写的,博主用的dp吗

    • 博主
      Terrece
      Macintosh Edge 151.0.0.0
      3 周前
      2026-8-21 11:11:53

      哈哈哈行家呀,没错就是 deepseek,不过最近涨价了成本翻了三倍,又想回归 openai 了。主要是忙着重构,所以文章就让 AI 帮忙写了,然后我再审稿发布,确实省很多时间,插图再用 gpt image 生成,完美。反正技术博客嘛,我只要保证展现的技术是正确有效的就行。
      不过我不是 deepseek 直接生成的,是先给了一个人设提示词,然后用 grill-with-docs skill 去写的,这样子上下文统一一点,专业词也不会在系列文章里有多个别名。

  2. Android Chrome 151.0.0.0
    已编辑
    3 周前
    2026-8-21 1:48:57

    不知道你在弄的,搜索用的什么。pagefind,还是其他方式。我很头疼这个,想要网站速度快就要用pagefind这种,但是这种对中文搜索很差劲,orama首次搜索要下载数据,最终自建的meilisearch,但是这玩意依赖服务器,我的服务器很慢 ̄﹃ ̄

    • 博主
      Macintosh Edge 151.0.0.0
      3 周前
      2026-8-21 11:16:48

      我现在初步搜索还是用的 graphql 自带的搜索,有些 graphql 没有的 api 我会按 wp 的文档给 graphql 加查询节点,毕竟我做的 headless wordpress,不是纯静态站,那该用到后端的地方还是要用到后端的

  3. Windows Edge 151.0.0.0
    3 周前
    2026-8-21 1:08:05

    我的博客做的时候想到什么弄什么,来来回回搞了好几天。

    • 博主
      Macintosh Edge 151.0.0.0
      3 周前
      2026-8-21 1:23:35

      哈哈哈, 你博客主题配色什么的看着很舒服啊,个人博客嘛,想怎么改怎么改,我看你好像也是用 Astro 重构的

    • 博主
      Macintosh Edge 151.0.0.0
      3 周前
      2026-8-21 1:24:44

      我目前还在本地运行, 打算这两天把部署功能测试验收了, 然后挂在 dev.styunlen.cn 域名下

      • Styunlen
        Android Chrome 151.0.0.0
        已编辑
        3 周前
        2026-8-21 1:49:40

        直接拉取 Astro 的空模板,一点一点的搞了,想到啥搞啥。我图 Astro 能直接托管到 CF 不用服务器。😂不想额外花钱啦哈哈

  4. Macintosh Chrome 151.0.0.0
    3 周前
    2026-8-20 17:00:52

    哈哈哈,是这样的,AI总是出其不意

    • 博主
      Dylan Li
      Macintosh Edge 151.0.0.0
      3 周前
      2026-8-20 22:01:50

      不过这样子确实省下很多和 AI 反复核对需求扯皮的时间。之前在给 AI 需求后,因为双方需求和上下文不一致,AI 总是在决策分叉口不询问我的意见就直接自己决策乱写代码。有了 ADR 相当于提前把每一个决策分叉口要采取的措施提前定下来了,不至于之后遇到类似的情况 AI 又自己乱来

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
Source: https://github.com/zhaoolee/ChineseBQB
Source: https://github.com/zhaoolee/ChineseBQB
Source: https://github.com/zhaoolee/ChineseBQB
颜文字
Emoji
小恐龙
花!
滑稽大佬
演奏
程序员专属
上一篇
下一篇