t3code:用TypeScript、自动化测试与工程工具链打造前端质量防线
我接手过一个维护了快两年的前端项目代码里 TypeScript 的any散落得到处都是测试文件夹里只躺着两个形同虚设的快照文件每次发版都跟拆盲盒一样。后来我把踩过的坑整理成了一套自己的工程规范给它起了个名字叫t3code。这套东西不是某个开源框架也不是什么新奇语法它是一套把TypeScript 类型约束、自动化测试、工程工具链三条主线拧在一起的前端代码质量保障方案。今天把这份折腾记录完整写出来如果你也在为代码失控、review 靠吼、重构心惊胆战这些问题头疼这篇应该对你有用。t3code 这个名字拆开看就是三条主线第一个T是 TypeScript第二个T是 Testing第三个T是 Tooling最后的code指的是让规范真正落到每一行代码上。它不是让你推翻重来而是给工程逐步装上安全带适合正在用 TypeScript 做中大型前端项目、被隐性 bug 和协作成本折磨的团队也适合想把自己的代码水平往上提一个台阶的个人开发者。1. 项目整体设计与思路拆解1.1 从名字拆解到核心定位我先说说为什么要叫 t3code。市面上关于代码质量的方案不少ESLint 管风格、TypeScript 管类型、Jest 管测试但大部分人都是缺啥补啥很少把它们当成一个整体来设计。t3code 的思路是把三条线串起来类型把住编译期的关口测试把住运行期的关口工具链把住团队协作的关口。这三道关卡相互独立却又彼此咬合缺了任何一块另外两块的效果都会大打折扣。拿我最开始接手的那个项目举例类型定义几乎全是any编译能过但运行时才炸测试快照写了两份就没人再更新代码风格靠人工 review 强撑。这三件事单独拎出来看都不算致命但叠在一起就是灾难类型失守导致 bug 提前暴露不了没有测试导致改代码全靠胆量风格失控导致 review 全在吵格式而不是聊逻辑。t3code 的核心定位就是把这三种散装能力收拢成一套标准化的工程底座让质量不是靠几个人的自觉而是靠机制本身去兜住。1.2 为什么需要一套整体性的编码规范很多人觉得规范就是统一用单引号、统一加不加分号这是把规范想窄了。真正值得投入的规范解决的是沟通成本和试错成本。我在团队里做过一次粗略统计一次普通 code review三到五个人的项目组平均要花十五分钟扯格式问题真正聊业务逻辑的时间不到一半。如果把格式问题全部交给 Prettier 自动处理类型问题全部交给 TypeScript 强制约束人就可以把精力放在那些机器替代不了的事情上——业务边界、数据流、异常路径。t3code 的另一个设计动机是减少隐性知识。很多项目的关键约束不在代码里在某个老员工的脑子里。他走了约束就没了。把约束写进配置、写进测试、写进流水线实际上是在把个人经验固化成团队资产。我整理 t3code 过程中一个很深的体会是写文档教育大家不要在 catch 里吞异常不如直接把规则写进 ESLint 配置写文档教育大家接口字段要校验不如用类型体操把非法状态变成编译错误。凡是能自动化的都不要靠自觉。1.3 三大支柱的选型逻辑为什么偏偏是 TypeScript、Testing、Tooling 这三样而不是别的因为我复盘了几年来的线上事故发现绝大多数问题都能归到这三类类型没约束住导致传入脏数据、测试缺失导致回归没人发现、工具链不规范导致低级错误漏进生产。所以这三根柱子不是我拍脑袋定的是被事故倒逼出来的。选 TypeScript 而不是 Flow选 Vitest 而不是 Jest选 ESLint 而不是 TSLint这些具体选型背后都有更细的理由。简单说TypeScript 的生态和语言服务能力目前没有对手而 Flow 的维护状态已经不适合新项目Vitest 在 Vite 生态下配置几乎为零而且基于 ESM 的原生体验比 Jest 在 ts 场景下各种 mock 折磨要舒服太多ESLint 是事实标准TSLint 已经被官方废弃。工具选型有一条经验选跟着主生态走的那个不要选逆着主生态爬的那个否则所有坑都要自己填。2. 核心细节解析与实操要点2.1 工程目录设计与初始化一个 t3code 规范下的典型项目目录结构不需要花哨但边界要清楚。我的习惯是把代码分成src、tests、scripts三大块src只放业务源码tests放测试与测试工具函数scripts放工程脚本。这样划分的逻辑很简单源码、测试、工具脚本的生命周期和变更频率不一样混在一起会导致 lint 配置、tsconfig 配置都要为不同目的妥协拆开之后每块都可以用最严格的标准去卡。初始化我推荐直接用 Vite 的脚手架起步然后逐步加配置。很多人一上来就上 monorepo其实大多数项目的复杂度和团队规模根本撑不起 monorepo 的收益。t3code 的定位是先让单包工程变扎实等真正出现多包共享代码的需求时再拆不迟。我用一个简单的公式判断要不要拆如果共享代码只有一两个模块拆的维护成本高于收益如果共享代码超过五六个模块且改动频繁再考虑 pnpm workspace 那套。pnpm create vite t3code-demo --template react-ts cd t3code-demo pnpm install2.2 TypeScript 严格模式的几个关键配置严格模式不是strict: true一开就完事的。我见过太多项目开了严格模式结果代码里全是ts-ignore等于把安全门焊死了。t3code 里有几个配置项我认为是必开的开完之后代码质量会有肉眼可见的提升。第一个是noUncheckedIndexedAccess它会让数组下标访问返回的类型变成T | undefined。这个配置初看很烦但真实场景里数组越界访问导致的崩溃不在少数逼着你处理可能不存在的情况长期受益巨大。第二个是exactOptionalPropertyTypes它禁止把undefined显式赋给可选属性很多隐蔽的序列化 bug 就是从这里来的。第三个是noFallthroughCasesInSwitch开关语句漏写 break 的问题不少人都踩过这个能直接编译期报错。{ compilerOptions: { strict: true, noUncheckedIndexedAccess: true, exactOptionalPropertyTypes: true, noFallthroughCasesInSwitch: true, noImplicitOverride: true, noUnusedLocals: true, noUnusedParameters: true, noPropertyAccessFromIndexSignature: true } }noPropertyAccessFromIndexSignature也是容易被忽略的一项。它强制要求通过索引签名访问属性时必须使用obj[key]的语法而不是obj.key。这样做的好处是让这个属性来自动态索引的语义在代码里显式可见避免把可能的 undefined 值当成一定存在来用。开这些严格选项的时候一部分存量代码会开始报错这是好事但也别想着一晚上改完。我的建议是分模块渐进式开先在tsconfig.json里配置好目标对历史代码用// ts-nocheck过渡新代码强制严格每周清理一部分历史文件。硬骨头要一点点啃一次性血流成河的项目基本都会回退。2.3 测试体系怎么搭才算够用测试这块 t3code 的理念是不为覆盖率而测为关键行为而测。很多人一上来就定 80% 覆盖率的指标结果团队为了跑分写出一堆断言毫无意义的测试真正的核心逻辑反而没人保护。我见过最离谱的是给一个 mock 函数写了个被调用三次的测试业务一改测试就碎这种测试不是资产是负资产。我在 t3code 里把测试分层核心业务逻辑和工具函数必须做单元测试组件行为用 Testing Library 做组件测试用户主路径用端到端测试补齐。对应到工具就是 Vitest 加 Testing Library端到端这块我推荐 Playwright。单元测试写逻辑分支组件测试写交互反馈端到端测试只跑最关键的两三条路径比如登录、下单、支付。三层的数量比例我经验里大约是 7:2:1核心逻辑的密度决定了单测占比最大。测试的写法上有一条我认为最重要不要测实现要测行为。比如测一个排序函数不要断言内部调用了Array.prototype.sort而要断言输入乱序数组后输出有序测一个组件不要去找组件内部某个状态变量而要断言用户点击后看到的结果。凡是绑定实现的测试重构时一定碎凡是绑定行为的测试重构时依然能跑这才是安全网的意义。2.4 代码规范与自动化检查流水线t3code 的第三根柱子是 Tooling核心是让规范和检查变成流水线的一部分。我在工程里配置了 ESLint、Prettier、husky、lint-staged 四件套。ESLint 负责语义检查Prettier 负责格式统一husky 加 lint-staged 把检查卡在提交之前形成一个四道闸门代码写出来、格式化、提交钩子检查、CI 流水线复查。配置这套东西有个小细节特别容易踩坑husky 的老版本用husky install新版是husky init或者直接prepare脚本升级之后命令完全不同网上抄的旧配置经常跑不起来。另外 lint-staged 的配置要小心它默认只对 staged 文件跑检查所以命令里一定要把eslint .或者tsc --noEmit这类全量命令改成只针对暂存区文件否则一次改动引爆整个仓库的存量报错钩子就废了。{ scripts: { lint: eslint src tests scripts --max-warnings0, format: prettier --write \src/**/*.{ts,tsx,css}\, typecheck: tsc --noEmit, test: vitest run }, lint-staged: { *.{ts,tsx}: [ eslint --fix, prettier --write ] } }这里的--max-warnings0是最容易被忽略的参数。它把 ESLint 的警告也变成错误防止队友们看到警告无动于衷、越积越多。设了这个之后但凡有新的警告出现提交直接失败逼着每个人当场处理。3. 实操过程与核心环节实现3.1 从零搭建一个 t3code 示范项目空谈配置没意思我实际带着一个新项目完整走了一遍 t3code 的落地过程这里记录一下关键节点的现场步骤。第一步是搭骨架。我用 Vite 的 react-ts 模板建好工程之后第一件事不是写业务代码而是把 tsconfig 的严格选项全部打开这时候模板代码会报出几个类型错误比如模板自带的counter组件里某些可空访问。别急着删顺手把类型改对这就相当于给新工程奠定了从第一行代码开始就是严格模式的基调。第二步是装测试和检查工具。Vitest 的安装比很多人想象中简单因为它和 Vite 共享配置不需要额外写配置文件。Testing Library 需要装testing-library/react和testing-library/jest-dom后者提供了一系列像toBeInTheDocument这样的断言扩展。我习惯把 jest-dom 的扩展在 setup 文件里全局引入否则每个测试文件都要手动 import 一遍。第三步是搭 Git 钩子。这里有一个现阶段的正确姿势直接在package.json里加prepare: husky或者用husky init然后手动创建.husky/pre-commit文件写入pnpm lint-staged再创建.husky/commit-msg写入 commitlint 检查提交信息格式。提交信息格式我推荐用type(scope): subject的写法像fix(auth): 处理登录态过期这样配合 commitlint 一起用。刚开始团队会觉得写提交信息烦但等你要根据 commit 生成 changelog 的时候就知道值了。3.2 关键配置文件的写法与解释先看 eslint 配置。新版本 ESLint 用的是 flat config也就是eslint.config.js和旧版.eslintrc的写法完全不同。一个适配 TypeScript 3.0 的平坦配置大概是这个样子import ts from typescript-eslint/eslint-plugin; import tsParser from typescript-eslint/parser; import reactHooks from eslint-plugin-react-hooks; import prettier from eslint-config-prettier; export default [ { ignores: [dist, coverage] }, { files: [**/*.{ts,tsx}], languageOptions: { parser: tsParser, parserOptions: { ecmaVersion: latest, sourceType: module, }, }, plugins: { typescript-eslint: ts, react-hooks: reactHooks, }, rules: { ...ts.configs.recommended.rules, react-hooks/rules-of-hooks: error, react-hooks/exhaustive-deps: warn, }, }, prettier, ];这段配置最关键的是最后一行prettier。它的作用是关掉 ESLint 里和 Prettier 冲突的格式类规则。很多人没加这一条结果 ESLint 要求代码必须这样写Prettier 又强迫代码那样排两边打架弄得人很崩溃。记住一个原则语义归 ESLint格式归 Prettier两个工具各管一摊千万别让它们抢活干。再看 Vitest 配置实际用下来推荐写成这样import { defineConfig } from vitest/config; export default defineConfig({ test: { environment: jsdom, setupFiles: [./src/testSetup.ts], coverage: { provider: v8, reporter: [text, html], thresholds: { lines: 70, functions: 70, branches: 60, statements: 70, }, }, }, });environment: jsdom是给组件测试用的如果只测纯逻辑其实用 node 环境更快。覆盖率阈值这里我故意设在 70 到 80 之间比很多人拍脑袋定的 90 要现实得多。覆盖率这东西边际成本递增非常快从 70 提到 90 需要的测试量可能翻好几倍收益却没有同步增长。t3code 的态度是覆盖率是敲门砖不是圣旨真正值钱的是测试有没有覆盖到核心路径的异常分支。3.3 流水线接入与实战记录本地检查做完了还要在云端兜底。我在项目里接的 CI 流水线是三段式安装依赖、并行跑检查、构建产物。检查这一步会把 lint、typecheck、test 三条命令设成并行执行任何一个失败就阻断合并请求。name: CI on: pull_request: push: branches: [main] jobs: quality: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: pnpm/action-setupv2 - uses: actions/setup-nodev4 with: node-version: 20 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm typecheck - run: pnpm lint - run: pnpm test -- --coverage - run: pnpm build这套流程跑起来之后的直观感受是合代码不再是心跳局。以前 merge 一个分支前总担心遗漏了什么检查现在所有检查都在云端跑本地钩子再拦截一道双保险之下低级错误基本进不了主干。有一次同事提交的代码里混进了一个无效的 import实时编译根本没报错结果 lint 直接在 PR 上给拦住了这种例子一多大家就心服口服了。还有一个细节值得提pnpm install --frozen-lockfile这个命令保证 CI 里只按锁文件安装不会因为某个人本地pnpm install改了间接依赖版本导致云端和本地行为不一致。之前团队被本地能跑云端挂坑过太多次这个参数是救命级别的好习惯。4. 常见问题与排查技巧实录4.1 严格模式下的类型错误怎么破开了noUncheckedIndexedAccess之后最常见的报错是访问数组元素或索引签名属性时对象可能是undefined。新手第一反应是加非空断言!这其实是在跟类型系统撒谎。正确做法是看场景如果是确定分过支的逻辑可以用显式的 null 检查把类型收窄如果是真的可能不存在就返回默认值或者走异常分支。我有一个排查套路遇到类型报错先问三个问题这个值在上游有没有被校验过这个值的合法范围是什么如果它非法程序应不应该继续跑想清楚这三个问题再动手改类型而不是打开tsconfig把严格选项关掉。关严格选项等于拔掉安全气囊图一时快出事就是大事。4.2 测试写不下去的典型卡点很多初写测试的人卡在不知道测什么。我给出的原则是找三类东西测有输入输出的纯函数、有状态转换的模块、有关键交互的组件。那些 UI 上纯粹展示没有逻辑的组件坦白说不值得写单测交给端到端测试去覆盖就好。还有一类人卡在 mock 上mock 太多太深测出来的东西跟真实行为已经没关系了。t3code 的理念是少 mock 固有的多 mock 边界的。网络请求、时间、随机数这些是不稳定的边界该 mock 就 mock业务逻辑是自己的核心资产mock 掉了等于没测。举个例子测一个请求用户列表的 hook不要 mock 整个 fetch而是封装出一个apiClient模块测试时 mock 这个模块的返回值。组件里真正复杂的 state 更新逻辑依然会真实执行测试才有意义。如果直接把组件内部的所有依赖全部 mock 光测试跑过跟没跑一样。4.3 工具链冲突的典型根源ESLint 和 Prettier 的冲突是我被问得最多的问题根源一句话ESLint 认为它是老大格式也想管Prettier 也认为它是老大格式必须按它来。解法不是去一条条调规则而是迎进eslint-config-prettier把所有格式类规则全部关掉让 Prettier 专心管格式。还有一个冲突源是编辑器自动保存的格式化插件跟 lint-staged 的--fix打架本质上是保存时没有按照工程配置来。解决方法是统一团队编辑器配置在项目根目录放一份.editorconfig和.vscode/settings.json设置保存时用 Prettier 格式化然后 lint-staged 里的格式化命令就不会有太多实际动作。4.4 团队推广不了的五个原因与解法t3code 这套东西单独用没问题但要带团队一起上阻力从来不在技术上而在人上。我观察到一个规律推行失败大多是五个原因。一是上来就要全员全量整改压力太大。正确做法是圈定一个模块试点跑两个迭代再扩大范围。二是不解释为什么只发配置让大家照做。每个人都需要知道这么干能少踩哪些坑而不是被迫服从。三是不留过渡期存量代码瞬间被检查工具刷红挫败感爆棚。给历史代码开豁免通道新代码必须合规每周定期清理存量。四是工具链不稳定今天格式变一下明天规则改一下大家就不信任了。工具链要锁定版本、少做破坏性变更。五是只管检查不管修报错了没人教怎么处理。要注意培养一两个配置户专门答疑让其他人尝到甜头而不是苦头。我自己的经验是推规范最好的时机是项目出事之后。线上出一次和数据、状态相关的 bug复盘时顺带引导出如果类型和测试在这里就不会出这个事大家的接受度比平时高十倍。5. 从个人项目到团队规范的实践扩展t3code 不只适用于 React 项目换成 Vue 或者 Node 服务端也一样成立核心原则是不变的类型把住数据边界、测试守住行为边界、工具链规范流程边界。我在几个不同类型的项目里都套过这套框架差异只是具体工具链的微调。比如 Vue 项目把eslint-plugin-react-hooks换成eslint-plugin-vue的推荐规则集Node 服务端项目把 Testing Library 换成supertest来做接口测试。还有一个更现实的应用在你无力推动整个团队改革的时候可以先在自己的模块里把标准立起来。比如说你负责的这块代码严格类型、有核心单测、提交时能过 CI其他人乱归乱至少你这块是稳的。等大家看到你模块的 bug 率明显低于平均再顺势提议推广就有了实打实的说服力。我在团队里就是这么干的从自己负责的权限模块开始一个季度之后覆盖到了全部业务模块。规范化程度可以做一个简单的体检打分类型上有没有系统性消灭any测试上关键模块有没有行为级用例流程上提交代码有没有自动检查三块各占三十分再补十分给团队是否有人能长期维护这套配置。低于六十分的项目未来三到六个月大概率会在某个凌晨被线上事故上一课。我个人的体会是搞工程规范最累的不是写配置而是把为什么要这么做讲清楚以及陪着团队把习惯养起来。配置是死的人是活的工具链只是把一部分人的好习惯固化成了机制。等机制转起来之后你会有一种明显的感觉代码 review 的时间短了、回归的 bug 少了、新同事上手的成本低了这三样才是 t3code 真正值钱的地方。最后分享一个小技巧把 tsconfig、eslint、lint-staged 这套配置提取成一个共享包比如your-scope/tscode以后新项目开箱即用。版本升级只改一处所有项目同步受益。我就靠这一手省下了每个项目重复配置的时间也避免了不同项目之间配置漂移的隐患。这套东西运行了两年多给我最大的回报不是代码多漂亮而是夜里睡觉踏实了——这大概就是我愿意把它写成一篇文章分享出来的原因。