CW Logo

博客减重记:从 8200 行代码到架构反思

周末整理博客代码,顺手跑了个统计:84 篇内容,8200 行 TypeScript,46 个运行时依赖。这个数字让我愣了一下——一个个人博客,怎么搞出了小型 SaaS 的体量?

从 PDF 功能开始动手

最早装 @react-pdf/renderer 的时候,想法很朴素:每篇文章应该有一个"导出 PDF"按钮,读者可以下载保存。为了这个按钮,我陆续加了:

  • lib/markdown-to-pdf.tsx(478 行),手写了每个 Markdown token 类型到 React-PDF 组件的映射
  • lib/pdf-document.tsx(202 行),定义 A4 页面模板
  • components/PdfButton.tsx,前端按钮组件
  • app/api/pdf/[slug]/route.ts,动态 PDF 生成 API
  • public/fonts/ 下放了两个中文字体文件(合计 34MB)

六个文件,外加一个重量级依赖。而实际上,我从来没有在任何一篇文章的 frontmatter 里填过 pdfUrl

这大概就是过度设计的典型症状:为了一个没人用的功能,搭了一整套管道。

于是第一个 commit 就是把这些全删了。git diff --stat 显示 +48 -1081,净减少一千多行。删代码的快感,懂的都懂。

更深层的问题:抽象与规模不匹配

PDF 只是冰山一角。继续审视代码,发现更隐蔽的问题。

lib/content/ 目录下有三个文件——module.tsrepository.tstypes.ts——用了工厂模式 + 仓储模式 + 缓存层 + 字段选择器,把"读目录下的 Markdown 文件"这件事封装成了一个泛型内容模块系统:

TypeScripttypescript
export function createContentModule<TIndex, TFull>(config) {
  // buildIndex → createIndexGetter → createContentCache
  // → createGetBySlug → createGetAll → selectFields → toFieldsKey
  // 返回 { get, getFull, list, listFull, adjacent, tags, byTag, totalCount }
}

看起来很漂亮。问题是,这个 450 行的基础设施只服务了 3 个内容类型——posts、essays、albums。每个类型本质上只需要 fs.readdirSync + matter() 两行。

类似的还有 Feed 渲染管道:feed.tsmdx-render.tsxfeed-cache.ts,用 MDX 检测 + React prerender + SHA-256 缓存来为 RSS feed 生成 HTML 内容。而大多数博客直接用 marked 一行搞定。

工程中的"规模意识"

这件事让我想起一个老生常谈但总被忽略的原则:解决方案的复杂度应该与问题的规模成正比。

三个内容类型 → 不需要泛型工厂。八十篇 Markdown → 不需要分层仓储。个人博客的 RSS → 不需要 React prerender 管道。

我们写代码的时候,很容易陷入一种心态——"万一以后扩展呢"、"这样写更规范"——然后把企业级模式套在一个小项目上。但"万一"很少发生,"规范"的代价却是实实在在的维护负担。

不是说这些模式不好。工厂模式、仓储模式、缓存层——在几百个 API 端点、几十个数据源的项目里,它们是必需品。但在一个从文件系统读 Markdown 的个人博客里,它们只是噪音。

后续打算

PDF 删完只是第一步。接下来准备:

  1. lib/content/ 的泛型工厂拆成直接函数
  2. 用自己写的轻量请求替换 @aws-sdk/client-s3(上传脚本里已经手动实现了 Signature V4,列文件无非一个 GET 请求)
  3. Feed 渲染直接用 marked,砍掉 MDX→React→HTML 的转换链
  4. 清理那些给 AI Coding 工具配的实验性配置文件

目标是代码量降到 5000 行以内,依赖数降到 35 个以下。

这些数字本身不是目的,但它们是一面镜子:如果你的代码比你的内容还重,那一定是哪里搞错了。