指南 / 2026-09-08
为什么这套模板的每个模块都能删干净
一个「需要做减法」的启动模板,比一个「需要做加法」的更有用——前提是删除得是一条配方,而不是一场考古。

大多数 SaaS 启动模板是按「包含了什么」来评判的。这个角度是错的。任何模板你大概只会用到一半功能,而剩下那一半并不免费:它是你要迁移的表结构、要过类型检查的代码、要跟着打补丁的依赖,以及你在改动附近代码之前必须先读懂的一片区域。
所以这里的原则是反过来的:每个可选模块都必须能在几分钟内按配方删除,并且不留痕迹。
这条原则倒逼出什么
事实证明「可删除」没法事后再加上去,它从第一个提交起就在约束架构:
- 模块是垂直切片。
src/features/<name>/自带 schema、服务端函数、查询和组件。模块之间从不互相 import,公共部分只放在src/core/。 - 注册只有一行。 一个模块不会从十个地方接进系统。它导出一个处理器,然后往注册表里加一行 import:schema 汇总、支付处理器、统计数据源、后台用户详情区块、定时任务。删模块就是删掉这几行。
- 注册表是惰性的,而且允许为空。 没有注册任何 sink 时,
audit()是空操作而不是崩溃。正因如此,你才能在代码库里几十处调用点仍在调用它的情况下,把审计模块整个删掉。 - 凡是做不到一行的,就用注释围起来。 当一个模块确实必须碰共享 UI(侧边栏一行、首页一张卡片)时,那段代码会被夹在
[feature: name] start和end之间,让配方能精确地剪掉它。
让这件事持续为真的部分
写在文档里的配方,在别人第一次重命名文件时就会失效。所以这里的配方是可执行的:bun run verify:deletion 会复制一份仓库,真刀真枪地执行每一条删除,然后要求类型检查和构建全部通过。它在每个 PR 上都会跑。
这个任务不止一次抓到过配方失效——某个 import 被挪了位置,某个新增的注册点没人记得写进文档。而这正是重点:首页上那句承诺之所以敢写,是因为每次代码变动都有机器替你重新验证一遍。
ShipKit
