Published on

把"工具放宽"误读成"标准降低":前端质量门禁背后的阈值语义

Authors
  • avatar
    Name
    Jack Qin
    Twitter

前端质量这件事,最容易出问题的不是工具不够,而是工具被读错了。一套工具链摆在那里,它给出的每一个数字、每一处例外,都带着一个隐含的语义;而质量的松动,几乎总是从某个人把这个语义读反开始的。

这篇不打算罗列工具配置——配置项谁都查得到。我想拆的是两个最常被误读的阈值:--max-warnings 500 到底是"额度"还是"上限",测试文件放宽的类型规则到底是"例外"还是"标准"。这两个误读看起来无关痛痒,但它们是质量从边缘开始腐烂的起点。把阈值的语义讲清楚,比再加十条规则都管用。

工具链各司其职:先分清谁管什么

要避免误读,第一步是知道每个工具的职责边界,不要让它们的能力互相混淆:

  • ESLint —— 前端策略和大部分代码质量规则
  • Biome —— 格式化,外加一小部分 lint 规则
  • Vitest —— 单元/组件测试
  • Playwright —— E2E 覆盖
  • CI 工作流 —— lint、typecheck、build、契约漂移、密钥扫描

Lint 与格式化:遵循 apps/web/eslint.config.js 的前端规则,遵循 biome.json 的格式化和 Biome 规则子集;一致地用 type 导入/导出;保持 a11y 安全的标记和有效的 ARIA 用法。

测试风格:优先用户向断言和可访问查询;单元测试与被测代码就近放置;用 Playwright 覆盖完整浏览器流程。

CI 期望——当前前端相关检查包括 web-lint、web-test、web-build、web-contract、secret-scan 这几个工作流。

测试配置与阈值:数字是有方向的

Vitest 用 jsdom + src/test/setup.ts,覆盖率阈值 80 行 / 80 函数 / 80 语句 / 75 分支。模式:按 role/label/可见行为查询 UI;需要时在模块边界 mock 依赖;测试贴着源文件放。

Playwright 用 apps/web/playwright.config.ts,通过 e2e/global-setup.ts 引导认证,跑 Chromium 和 Firefox,失败或重试时保留截图/视频/trace。E2E 用于登录、重定向、浏览器行为和多页流程。

这些数字看起来只是配置,但每个都有一个方向——它是地板还是天花板?读错方向,整套门禁的意义就反了。下面这两条就是最容易读反的。

两个必须读对方向的阈值

这两条单独拎出来,因为它们最容易被误读成"降标准的许可"。

1. --max-warnings 500 是上限,不是目标。 apps/web/package.json 允许 ESLint 警告至多 500 个。误读是这样产生的:有人看到"能容 500 个",就推出"那我加几个无所谓"。但这个数字的语义恰恰相反——它是给历史债务留的缓冲天花板,是"现存的警告还没清完,先别让 CI 一直红"的妥协,不是"每个人可以新增警告的额度"。把上限读成额度,等于把一个用来限制债务的护栏,当成了制造债务的许可证。新增代码的正确默认是零警告

2. 测试文件放宽类型规则,是测试的例外,不是生产代码的标准。 eslint.config.jsbiome.json 里测试文件故意放宽了几条不安全类型规则。这个放宽有它的道理:测试里构造 mock 和断言时,偶尔需要绕过一些类型严格性,强行类型化反而让测试更难读。但这个例外的作用域严格限定在测试文件里。误读是把它当成"这个项目对 any 比较宽松"的信号,然后在应用代码里也开始随手 any。应用代码仍应避免 any——测试的宽松不该外溢一寸。

两条误读的共同结构是一样的:把一个有明确作用域的例外,扩大解释成全局的标准下调。 护栏放宽的地方,往往正是最需要自律的地方。

还有一个真实存在的 a11y/测试缺口值得提:apps/web/e2e/pages/EmailSchedulesPage.ts 里有一段显式注释,标注了某个 modal 缺少 dialog role。这种缺口的正确处理方式是被看见、被记录,而不是被默默接受为常态。一条写明的"这里有缺口"注释,和一个无人知晓的缺口,在质量账本上是两回事。

代码评审清单

评审者应检查:

  • 改动是否遵循功能化结构,而不是新增一套并行模式?
  • API 调用是否走了共享客户端和功能 hook?
  • 类型是否干净传播,没有不必要的 any 或断言?
  • 交互组件是否可访问、可按 role/label 测试?
  • 测试是否对齐既有 Vitest 和 Playwright 模式?
  • 如果前端契约变了,有没有考虑 schema/客户端漂移?

反例

// 反例 1:把 max-warnings 当额度,随手引入新警告
// "反正上限 500,加几个无所谓" ❌ —— 警告上限是债务缓冲,不是新增额度
// 反例 2:把测试的宽松搬进应用代码
// 应用代码里:
function parse(input: any) {
  return input.data.items
} // ❌ 测试可放宽,生产不行
// 正确:用类型化边界
function parse(input: ApiResponse): Item[] {
  return input.data.items
}
// 反例 3:既有 API 客户端 + React Query 适用,却另起一套 ad hoc fetch
useEffect(() => {
  fetch('/api/v1/foo').then(/* ... */)
}, []) // ❌
// 正确:走共享客户端 + 功能 hook
const { data } = useFooQuery()
// 反例 4:交互 UI 缺可访问名称,测试只能靠脆弱选择器
<div onClick={open}></div> // ❌
// 正确
<button aria-label="Open settings" onClick={open}></button> // ✅

其它要避免的:不要加违背当前 apps/web 架构的通用代码;不要跳过交互 UI 的可访问 label 和 role;不要在应有合适 role/label 时,在测试里依赖脆弱 DOM 遍历;不要把 React Query 的原始 DTO 直接从 hook 抛出(仓库已为 UI 做映射);不要在产品本该暴露可访问钩子时,在 E2E 用脆弱选择器;别忘了 apps/web/tsconfig.json 把一些文件排除在 TypeScript 编译覆盖外,要确认重要应用代码仍在类型化路径内。

落地建议

  1. 阈值读对方向max-warnings 500 是债务上限不是新增额度;新增代码默认零警告。
  2. 测试宽松不外溢:测试文件放宽的类型规则只属于测试,应用代码继续避免 any
  3. a11y 缺口要可见:像 EmailSchedulesPage.ts 那样,把已知缺口写成显式注释,而不是默默接受。
  4. 走既有数据路径:API 调用一律走共享客户端 + 功能 hook,别另起 ad hoc fetch。
  5. 评审照清单逐条过:结构、数据路径、类型传播、可访问性、测试对齐、契约漂移——六问一个不漏。

可迁移的那一层

抛开 ESLint 和 Vitest 的具体配置,这件事真正可迁移的认知是:质量门禁里的每一处"放宽",都携带着一个隐含的作用域,而腐烂总是从有人把这个作用域悄悄扩大开始的。 上限被读成额度、测试例外被读成全局标准——错的不是工具配置,是阅读它的人丢掉了"这条放宽到底是为谁、为什么场景而设"这个前提。

面对任何一处"工具允许我这么做"时,与其问"那我能不能这么做",不如先问:这条放宽当初是为了解决什么具体问题、它的作用域有多大、我现在是不是正在把它用到作用域之外? 护栏存在的地方都好守;真正考验团队的,是护栏被有意拆掉的那几处。