
一句话:规范工具链的分工是「Prettier 管格式、ESLint 管正确性与坏味道、TypeScript 管类型」——其中
eslint-plugin-react-hooks的两条规则最关键(rules-of-hooks是 error、exhaustive-deps至少 warn)。
一、工具的职责划分
| 工具 | 管什么 | 例子 |
|---|---|---|
| Prettier | 纯格式(缩进、引号、分号、换行) | 空格/引号/尾随逗号 |
| ESLint | 正确性与坏味道(潜在 bug、不一致写法) | 未使用的变量、Hook 规则、依赖数组 |
| TypeScript | 类型(接口契约) | props 类型、可空性 |
| Stylelint | CSS 规范 | 属性顺序、未知属性 |
| commitlint | 提交信息格式 | feat: xxx |
| EditorConfig | 编辑器基础约定 | 缩进风格、行尾 |
⭐ 关键原则:不要用 ESLint 管格式(那会与 Prettier 打架)
· 用 `eslint-config-prettier` 关掉「与 Prettier 冲突的格式规则」
· 格式统一交给 Prettier(`prettier --check` 在 CI 里跑)
二、eslint-plugin-react-hooks(最重要)
npm i -D eslint eslint-plugin-react-hooks eslint-plugin-react-refresh
// eslint.config.js(扁平配置)
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{ ignores: ['dist', 'coverage'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2022,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
// ⭐ 两条核心规则
// 'react-hooks/rules-of-hooks': 'error', (recommended 已包含)
// 'react-hooks/exhaustive-deps': 'warn', (recommended 已包含)
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
},
}
);
⭐ 两条规则为什么最关键:
① `rules-of-hooks`(error)
检查「Hook 是否在顶层调用、是否在组件/自定义 Hook 里」
→ 违反会导致「状态错乱」,是【必须】拦住的错误
② `exhaustive-deps`(warn)
检查「Effect/useMemo/useCallback 的依赖数组是否完整」
→ 漏依赖 = 闭包陷阱(见 2.13);它的警告「几乎总是对的」
→ 不要习惯性 disable;要 disable 时写清原因
⚠️ 第三类(现代项目也该有):`react-refresh/only-export-components`
保证「组件文件只导出组件」(否则热更新会失效/整页刷新)
// ✅ 需要 disable 时要写清原因
useEffect(() => {
initThirdPartyLibrary(); // 只初始化一次
// eslint-disable-next-line react-hooks/exhaustive-deps -- 第三方库只需初始化一次,参数不会变
}, []);
三、TS 严格模式与推荐规则
// tsconfig.json
{
"compilerOptions": {
"strict": true, // ⭐ 开启全部严格检查(含 strictNullChecks)
"noUnusedLocals": true, // 未使用的局部变量报错
"noUnusedParameters": true, // 未使用的参数报错
"noFallthroughCasesInSwitch": true, // switch 穿透报错
"noUncheckedIndexedAccess": true, // ⭐ 下标访问返回「可能 undefined」
"exactOptionalPropertyTypes": true, // 可选属性不接受显式 undefined(较严格)
"verbatimModuleSyntax": true, // 强制 import type 写法
"jsx": "react-jsx",
"moduleResolution": "bundler",
"skipLibCheck": true
}
}
⭐ 三个最有价值的选项:
① `strict: true` —— 包含 strictNullChecks(让「可能为 null」被编译器发现)
② `noUncheckedIndexedAccess` —— `arr[0]` 的类型从 `T` 变成 `T | undefined`
(能发现大量「数组越界」隐患,代价是要多写判空)
③ `noUnusedLocals/Parameters` —— 清理死代码
⚠️ 老项目开启严格模式会有大量报错 →
建议「逐文件/逐目录」渐进开启,或先用 `// @ts-expect-error` 标注待修处
// 推荐的 ESLint 规则(针对 React)
{
"rules": {
// 组件与 Hook
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"react-refresh/only-export-components": "warn",
// 常见坏味道
"no-console": ["warn", { "allow": ["warn", "error"] }],
"no-debugger": "error",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/consistent-type-imports": "error", // 强制 import type
// 一致性
"eqeqeq": ["error", "smart"],
"prefer-const": "error",
"no-var": "error"
}
}
四、团队约定(值得写进规范文档)
① 命名
· 组件:PascalCase(UserCard)
· Hook:use 开头 + camelCase(useUserList)
· 工具函数:camelCase(formatDate)
· 常量:UPPER_SNAKE_CASE(MAX_RETRY)
· 布尔值:is/has/should 前缀(isLoading、hasError)
· 事件处理:handleXxx(handleSubmit)/ 回调 props:onXxx(onSubmit)
· 类型:PascalCase(UserCardProps),不加 I 前缀
· CSS Module 类名:camelCase 或 kebab-case(统一即可)
· 文件名:组件文件与组件同名(UserCard.tsx)
② 组件写法
· 统一用函数组件 + Hooks(不用类组件,除 Error Boundary)
· props 用「解构 + 显式类型」,导出 props 类型
· 不用 React.FC
· 一个文件一个主组件(小组件可同文件,但别超过 2~3 个)
· 组件内不定义组件(见 4.4)
③ 导入顺序
1. React 相关(react / react-dom)
2. 第三方库
3. 内部绝对路径(@/...)
4. 相对路径(./ ../)
5. 样式
6. 类型(import type 放最后)
→ 用 eslint-plugin-import / simple-import-sort 自动排序
④ 注释
· 「为什么」比「是什么」更重要
· 对「反直觉的写法」必须说明原因(如 disable lint 的原因)
· TODO/FIXME 要带「人名 或 工单号」
⑤ 禁止事项
· 禁止 any(用 unknown + 收窄)
· 禁止 index 作 key(列表会变动时)
· 禁止直接改 props/state
· 禁止在渲染中做副作用
· 禁止 require(用 import)
五、提交规范与自动化
npm i -D husky lint-staged @commitlint/cli @commitlint/config-conventional
npx husky init
// package.json
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{css,scss}": ["stylelint --fix", "prettier --write"],
"*.{json,md}": ["prettier --write"]
},
"scripts": {
"lint": "eslint .",
"format": "prettier --write .",
"typecheck": "tsc --noEmit"
}
}
// commitlint.config.js —— 约定式提交
export default {
extends: ['@commitlint/config-conventional'],
rules: {
'scope-enum': [2, 'always', ['ui', 'api', 'auth', 'deps', 'config']],
'subject-max-length': [2, 'always', 72],
},
};
# .husky/pre-commit —— 提交前只检查「暂存的文件」(快)
npx lint-staged
# .husky/commit-msg —— 校验提交信息
npx --no -- commitlint --edit $1
# .husky/pre-push —— 推送前做完整类型检查(可选,稍慢)
npm run typecheck
⭐ 约定式提交(Conventional Commits)的格式:
<type>(<scope>): <subject>
feat: 新功能
fix: 修 bug
docs: 文档
refactor: 重构(不改行为)
perf: 性能优化
test: 测试
chore: 杂项(构建、依赖、配置)
style: 格式(不影响逻辑)
ci: CI 配置
revert: 回滚
✅ feat(auth): 支持微信登录
✅ fix(ui): 修复 Modal 在移动端的滚动锁定失效
❌ 修改了一下
❌ update code
价值:
① 自动生成 CHANGELOG
② 自动决定「语义化版本号」(feat → minor,BREAKING CHANGE → major)
③ 「按类型筛选提交」便于排查(哪个提交引入了 bug)
六、CI 中的检查
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm run lint # ① 规范
- run: npm run typecheck # ② 类型(⭐ 别漏,tsc 与构建是两件事)
- run: npm run test:run # ③ 测试
- run: npm run build # ④ 构建(也能发现部分类型问题)
# ⑤ 产物分析 / 体积门禁(可选)
- run: npx bundlesize
⭐ CI 的四道门禁(顺序从快到慢):
① lint(秒级)
② typecheck(几十秒)—— ⚠️ 很多项目漏了这步:
因为「构建工具(esbuild/swc)不做类型检查」,所以 tsc 必须单独跑
③ test(分钟级)
④ build(分钟级)
⚠️ 常见错误:
· 只跑构建不跑 typecheck → 类型错误漏到线上(因为 esbuild 不检查类型)
· 只在本地跑 lint(没进 CI)→ 有人不跑就提交了
· 把 lint 全放进 pre-commit(提交变慢)→ 应该「pre-commit 只检查暂存文件 + CI 做全量」
七、常见坑
// ① 用 ESLint 管格式(与 Prettier 打架、报错刷屏)
// ✅ 加 eslint-config-prettier 关掉格式规则
// ② 关掉 react-hooks 规则(最危险的做法)
"react-hooks/rules-of-hooks": "off"; // ❌ 会放过「Hook 在条件里」的 bug
"react-hooks/exhaustive-deps": "off"; // ❌ 会放过闭包陷阱
// ③ 习惯性 disable exhaustive-deps(且不写原因)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // ⚠️ 没写原因 → 后人不敢改
// ④ 只跑 lint 不跑 tsc(类型错误漏到线上)
// ⑤ 严格模式没开(strictNullChecks 的价值最大)
// ⑥ noUncheckedIndexedAccess 没开 → 数组越界/字典取值为 undefined 的 bug 漏掉
// ⑦ 用 any 抹类型(丢失检查)→ 用 unknown + 收窄
// ⑧ 提交信息不规范(无法自动生成 changelog/版本号)
// ⑨ pre-commit 里跑「全量 lint + 全量测试」(提交极慢 → 有人用 --no-verify)
// ⑩ 没有「禁止 --no-verify」的约定(husky 可被绕过)
// ⑪ 用「npm install」而不是「npm ci」在 CI 里(依赖版本可能漂移)
// ⑫ 没有「锁文件提交」(依赖不可复现)
// ⑬ 忽略「lint 规则升级带来的破坏性变更」(升级 eslint 大版本时要看 changelog)
// ⑭ 规则太多、太严但没有「自动修复」(开发者体验差、开始绕过)
// ⑮ 只在本地配 lint(新人克隆后没配好就提交了脏代码)
// ⑯ 没有「分环境」的规则(如测试文件里允许 any、允许 console)
// ⑰ 用「.eslintrc」(旧格式)而不是「扁平配置」(eslint 9 的默认)
// ⑱ 忘记「忽略目录」(dist、coverage 被 lint → 慢且报错多)
// ⑯ 的分环境配置(测试文件放宽)
{
"files": ["**/*.test.{ts,tsx}", "**/test/**"],
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"no-console": "off"
}
}
八、面试延伸
- 「你们项目的代码规范是怎么配的?」
工具分工是「Prettier 管格式、ESLint 管正确性、TypeScript 管类型」(再加 Stylelint 管 CSS、commitlint 管提交信息)。关键配置有四点:①eslint-plugin-react-hooks的rules-of-hooks(error)与exhaustive-deps(warn)——这两条是 React 项目里最重要的;②eslint-config-prettier关掉与 Prettier 冲突的格式规则;③ TS 严格模式(strict+noUncheckedIndexedAccess);④ husky + lint-staged(提交前只检查暂存文件)+ CI 里跑全量检查。 -
「为什么
eslint-plugin-react-hooks这么重要?」
因为它检查的是「React 里最难靠肉眼发现、后果最严重的两类问题」:①rules-of-hooks——「Hook 是否在顶层调用、是否在组件/自定义 Hook 里」。违反会导致「Hook 调用顺序错乱 → 状态错乱甚至崩溃」;②exhaustive-deps——「依赖数组是否完整」。漏依赖会产生闭包陷阱(读到旧值),而且症状是「偶尔不对、难以复现」。前者必须是error,后者至少是warn(不要习惯性 disable,要 disable 时写清原因)。 -
「Prettier 和 ESLint 有什么区别?怎么配合?」
职责不同:① Prettier 管「格式」(缩进、引号、分号、换行)——它不做逻辑检查,目标是「消除格式争论」;② ESLint 管「正确性与坏味道」(未使用变量、Hook 规则、any、潜在 bug)。配合方式是:① 用eslint-config-prettier关掉 ESLint 里「所有与格式相关的规则」(否则两者会互相打架);② 格式统一交给prettier --check(在 CI 里跑);③ 编辑器里开启「保存时格式化」。 -
「TypeScript 的严格模式为什么重要?」
因为strict: true里最有价值的strictNullChecks能「把「可能为 null/undefined」变成编译错误」——而「对undefined取属性」是前端最常见的线上崩溃来源(Error Boundary 的主要捕获对象)。此外noUncheckedIndexedAccess让arr[0]的类型从T变成T | undefined(发现大量「数组越界/字典取不到」的隐患)。代价是要多写判空,但换来的是「线上崩溃大幅减少」。老项目建议渐进开启(逐目录/逐文件)。 -
「为什么 CI 里必须单独跑
tsc --noEmit?」
因为「构建工具不做类型检查」——Vite 用 esbuild、Next.js 用 SWC/Turbopack,它们只做「语法转换」(把 TS 类型擦除),不会检查类型。所以npm run build成功不代表类型正确(any到处跑也能构建成功)。必须单独跑tsc --noEmit(只做类型检查、不产出文件)才能拦住类型错误。这是很多项目的漏洞——类型错误一路漏到线上。 -
「提交规范有什么用?」
约定式提交(feat:/fix:/docs:等)的价值有三:① 自动生成 CHANGELOG;② 自动决定语义化版本号(feat→ minor、fix→ patch、BREAKING CHANGE→ major)——这对「发布多包/组件库」尤其重要;③ 便于排查与筛选(「这个 bug 是哪个提交引入的」「本次发版包含哪些新功能」)。配套工具是 commitlint(校验格式)+ husky(在commit-msg钩子里运行)。 -
「husky 和 lint-staged 分别做什么?」
husky 是「Git 钩子管理器」——它让你在pre-commit、commit-msg、pre-push等时机运行脚本;lint-staged 是「只对「暂存区文件」运行命令」的工具(如eslint --fix、prettier --write)。配合的价值:只在提交时检查「你这次改动的文件」——快(不会因为一个老文件的历史问题阻塞提交),同时保证「新提交的代码是干净的」。注意:husky 可以被--no-verify绕过,所以「CI 里的全量检查」才是最终防线。 -
「规范太严会不会影响开发效率?」
会——所以规范设计要遵循三条:① 能自动修复的规则优先(eslint --fix、prettier --write让开发者「不用手改」);② 分级——「必须拦」的用error(如 Hook 规则、类型错误),「建议」的用warn(如no-explicit-any);③ 分环境——测试文件可以放宽(允许any、console),构建产物目录要忽略。此外「pre-commit 只检查暂存文件」也是为了让提交不慢。目标是「让规范成为助力而不是阻力」——如果团队都在绕过它,说明配置有问题。
一句话速记
分工是「Prettier 管格式、ESLint 管正确性、TS 管类型」(用
eslint-config-prettier关掉冲突规则);最关键的两条规则是react-hooks/rules-of-hooks(error)与exhaustive-deps(warn),加react-refresh/only-export-components;TS 要开strict+noUncheckedIndexedAccess(把「可能 undefined」变成编译错误);CI 四道门禁是 lint →tsc --noEmit(⚠️ 必跑,因为 esbuild/SWC 不做类型检查)→ test → build;husky + lint-staged 在 pre-commit 只检查暂存文件,CI 做全量(husky 可被--no-verify绕过);commitlint + 约定式提交能自动生成 CHANGELOG 与版本号;规范要「能自动修复的优先、分级(error/warn)、分环境、别拖慢提交」。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!