Remix 决策文档(ADR)撰写指南:用decisions/记录架构选择的原因
【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix
在remix仓库根目录下有一个decisions/目录,存放着一系列编号的决策文档(Architecture Decision Record,ADR)——例如decisions/001-route-pattern-vs-url-pattern.md、decisions/005-trie-based-matching.md。它们记录的并非"功能是什么",而是"我们为什么这样选"。本文基于仓库内 make-decision-doc 技能文档 与现存 6 份决策文档实例,完整讲解这套决策文档的编写规范、工作流、结构与内容规则,帮助你理解"为什么"类的架构决策如何在开源项目中沉淀、传播与自我检验。
决策文档是什么:记录"为什么",而非"是什么"
决策文档存放在仓库根目录的decisions/NNN-kebab-name.md,例如decisions/001-route-pattern-vs-url-pattern.md。它们解释一个选择之所以成立的原因——权衡点(tradeoff)、实际考量过的备选方案(alternatives),以及在什么条件下值得重新审视(revisit)。
需要特别界定边界:决策文档不是功能规格说明书(feature spec)、不是操作指南(how-to guide)、也不是变更日志条目(changelog entry)。它回答的核心问题是:"未来的贡献者看到这段代码时,为什么会问'我们为什么不用 X 方案?'"——并用文档给出令人信服的答案。
从现存文档可以直观看到这一点:
decisions/001-route-pattern-vs-url-pattern.md解释"为什么自研RoutePattern而不是使用 Web 内置的URLPattern";decisions/005-trie-based-matching.md解释"为什么@remix-run/route-pattern只保留基于 Trie 的Matcher,移除ArrayMatcher";decisions/004-v8-vs-istanbul-instrumentation.md解释"为什么@remix-run/test采用 V8 原生覆盖率而非 Istanbul 插桩"。
这些主题都属于"不读文档就会被反复质疑"的非显然架构决策,正是决策文档的目标场景。
何时该写、何时该跳过
技能文档给出了明确的触发条件,满足其一即可编写:
- 做出了非显然的架构选择,未来的贡献者会合理地问"我们为什么不换成 X?"。例如
decisions/006-sql-migrations.md记录的"迁移改用.sql文件而非 TypeScript 文件",初看反直觉(TS 生态更常见),但文档解释了其稳定性动机。 - 选择本身带有容易被事后质疑的权衡,例如"性能 vs. 简洁""锁定 vs. 灵活""人体工学 vs. 正确性"。
decisions/005正是"性能 vs. API 简洁"的权衡记录。 - 偏离了常见默认或行业标准方案,且理由重要。
decisions/001偏离了 Web 平台内置的URLPattern标准,decisions/004偏离了 Istanbul 插桩这一测试覆盖率的主流路线。
同时应跳过决策文档的场景:选择显而易见、备选方案并未被认真考虑、或者理由已经存在于代码注释、变更文件(change file)或 PR 描述中。刻意编写多余的决策文档同样是噪声。
编写工作流:五步走
技能文档定义了编写一份决策文档的标准流程:
- 先读现存文档对齐风格:阅读
decisions/下的既有文件(如decisions/001-*.md),匹配语气与结构,而非套用僵硬模板。 - 确定下一个编号:通过统计现存文件数量(例如
ls decisions/ | wc -l)得到 N,加 1 即下一个编号。文件名格式为NNN-kebab-slug.md,前缀为三位零填充数字(001、002……006)。 - 选择措辞聚焦"被决策的事物":slug 命名的是"被决定的东西"而不是动作,例如
single-matcher而非pick-single-matcher。对照仓库实例:005-trie-based-matching命名的是"trie 匹配"这一事物,而非"我们决定用 trie 匹配"。 - 按结构起草:保持务实,不要虚构备选方案,不要使用浮夸语言。
- 关联其他决策:如果本决策与其他决策相关,以脚注形式链接(
[NNN]: ./NNN-other.md)。
仓库中decisions/004-v8-vs-istanbul-instrumentation.md末尾正是这种脚注链接的实例:
[003]: ./003-coverage-transform-determinism.md它指向同目录下的 003 决策文档,因为 V8 原生覆盖率的可行性建立在"确定性 TS 变换"这一前提之上。注意这类文档内相对链接需要以仓库根目录为基准解析,即decisions/003-coverage-transform-determinism.md。
文档结构:不设僵硬模板,但有惯用骨架
技能文档明确"没有僵硬模板——匹配现存文档的风格",但给出一个"往往奏效"的形态,自上而下包括:
- H1 标题:用一句简短陈述或问句,让扫视目录的读者一眼抓住决策核心。例如
decisions/001的标题RoutePattern vs. URLPattern是双方对峙的陈述;decisions/006的标题Use .sql files for migrations则直接给出结论。 - 开头铺垫(1~3 个短段落):说明做了什么选择、为什么会出现这个选择、实际考虑过的备选方案是什么。要以具体上下文开头,不要以抽象论述开头。
decisions/001第一段即提问"Web 有内置的 URL 匹配器URLPattern,为什么我们不用它而是自建RoutePattern?"——读者瞬间进入问题语境。 - 权衡小节(H2):通常按维度展开,如"什么变得更简单 / 更困难""为什么我们选 X",或按性能、复杂度、人体工学等维度各设小节。
decisions/004就是范例:What gets simpler、What gets harder、Why we're staying on V8-native三个 H2 完整呈现双向权衡。 - 何时重新审视(可选但鼓励):以编号列表列出会使决策翻转的条件,让文档保持诚实,也给未来贡献者一个明确的"出口"。
decisions/004的When to revisit列出了三条:需要 Firefox/WebKit 覆盖率、支持任意用户自定义 transformer、V8 覆盖率运行时成本不再"基本为零"。decisions/005也说明"如果出现受益于数组匹配器的真实场景,可重新审视此决策"。 - 脚注链接:指向正文中引用的相关决策或外部资料。
内容规则:每一条论断都要落地
技能文档对内容提出若干硬性规则,这是决策文档区别于随笔的核心:
- 为每一条论断提供依据:引用基准测试、博客文章、代码引用或实测行为。如果某个决定就是"感觉更好",那就直说"这是感觉层面的选择",而不是包装成技术理由。
decisions/005引用基准数据"trie 匹配器在长驻服务基准中大幅超越数组匹配器;在 lambda 基准中数组匹配器略快(不到 2 倍),但两者在最大约 2500 条模式的基准(benchmark 模式集)下设置与匹配都在 10ms 以内"——每个数字都有出处。 - 只列出真正斟酌过的备选方案:虚构选项再打倒它只会削弱文档说服力。
decisions/006讨论的备选方案是"TS 迁移文件"与"引入 schema 导入"的失败模式,都是真实发生过的。 - 时态规范:决策本身用过去时("我们选择了 X"),决策带来的持续影响用现在时("选择 X 意味着我们不需要 Y")。
- 行文紧凑:决策文档奖励信息密度——读者是带着具体问题来的。避免营销腔与填充词(如"值得一提的是……""在许多情况下……"),直接陈述结论。
- 代码引用遵循仓库标准路径风格:如
path/to/file.ts;展示具体代码片段时用反引号围栏并可给出行范围。
以decisions/001中的代码对比为例,它用成对的RoutePattern与URLPattern片段说明"可选组"语义的直观性差异:
// URLPattern 的可选组::id 可选?还是 /:id 可选?仅凭单个 ? 无从判断 let pattern = new URLPattern('/books/:id?', 'https://example.com') pattern.test('https://example.com/books/123') // true pattern.test('https://example.com/books') // true pattern.test('https://example.com/books/') // false // RoutePattern 的可选组:() 包围可选部分,起止一目了然 let pattern = new RoutePattern('/books(/:id)') pattern.test('https://example.com/books/123') // true pattern.test('https://example.com/books') // true pattern.test('https://example.com/books/') // false结合仓库实例:从源码到决策的闭环
决策文档的价值最终要落在可验证的实现上。以decisions/005为例,文档声称"@remix-run/route-pattern只提供基于 Trie 的Matcher",这一声明可以在 route-pattern 包的源码 中得到印证:src/lib/match/目录下仅存在 trie.ts 等匹配实现文件,而数组匹配器的实现被移入 benchmark 子包以持续监测性能特征。同理,001 决策 中描述的RoutePattern语法、href 构建、可选组括号语法等能力,均能在 parse.ts、href.ts 等源码文件中找到对应实现与测试(如 route-pattern.test.ts)。
在@remix-run/test的覆盖率决策上,文档与测试形成了更严格的契约:decisions/004声称"三方一致性测试断言服务端、浏览器与 e2e 运行器对同一 fixture 产生字节级一致的 Istanbul 记录",该测试确实存在于 coverage-parity.test.ts;decisions/003则说明"想更换 transformer?修改transformTypeScript并用该一致性测试验证,若百分比或未覆盖行列表发生变化则变更不安全"。这展示了决策文档的终极形态——决策不仅是历史记录,还是代码变更的守门契约。
提交前检查清单
技能文档以一份清单收尾,用于自检决策文档是否达标:
- 是否至少阅读过一份现存决策文档以对齐风格?
- 文件名是否为
NNN-kebab-slug.md,且编号是下一个可用数字? - H1 是否用一行说清决策内容?
- 列出的备选方案是否确实是实际斟酌过的?
- 是否用具体证据(基准、来源、代码)支撑论断,而非空口断言?
- 若决策存在被推翻的合理可能,是否写了"何时重新审视"一节?
对照清单逐项核查后,一份合格的决策文档即可提交。它记录的不只是"我们选了 X",更是"为什么是 X、为什么不选 Y、什么情况下要改"。对维护者而言,这是防止架构决策被时间湮没的低成本保险;对贡献者而言,这是进入代码库深层意图的最短路径。
【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考