干过几年前端带过项目的朋友,大概率都有过这种体验:Code Review 的时候群里热热闹闹,结果点开评论区一看,一半以上都在说“这里多了个空格”“引号改成单引号”“这个缩进不对”。真正该被盯住的业务逻辑、边界条件、异常处理,反而被淹没在格式琐事里。我们项目组前段时间把这个问题彻底解决掉了——用一套自动化前端工程化方案,把 ESLint、Prettier、Stylelint、husky、lint-staged、commitlint 串起来,从源头拦截不规范代码,让 Vue 项目真正实现“提交即合规”。
这套东西落地之后,CR 的讨论质量肉眼可见地上了一个台阶,大家开始认真聊状态管理怎么拆、接口异常怎么兜底、路由权限有没有漏洞。本文就把这套方案的选型思路、配置细节、踩坑记录完整摊开,希望能给被 Code Review 格式问题折磨的团队一个可抄的作业。
1. 为什么 Code Review 不该管格式,以及自动化能带来什么
1.1 先算一笔“人盯格式”的成本账
很多团队不是不想管格式,是真的管不过来。一个 20 人左右的前端组,每周 merge request 数量大概在 60 到 100 个之间。假设每个 MR 里有 3 到 5 处格式问题,评审人平均要花 5 到 10 分钟去指出这些琐碎错误。算下来,每周光浪费在“纠格式”上的人力就是 5 到 15 个小时,一年就是几百个小时。这笔时间放在任何业务上,都是不小的成本。
更麻烦的是人的状态。评审人反复说“这里格式不对”,时间长了会形成惯性疲惫,看见不规范的代码甚至懒得说了。写代码的人则觉得“能用就行,你咋这么较真”,两边都不愉快。格式检查这种 100% 可以机器化、确定性的规则,压根不应该占用人的注意力。
那为什么不干脆只靠 Lint 插件在编辑器里提示?因为编辑器提示依赖个人自觉,很多同事根本没装插件,或者装了也不看黄色波浪线,还有一批人用 WebStorm、VS Code 混着来,配置都不一样。只要“提交”这个动作不设卡,问题代码必然流到 CR 阶段。
1.2 自动化检查的三个层次与“提交即合规”的含义
所谓“提交即合规”,不是口号,而是把检查点前移到 Git 提交之前,形成三道防线:
- 第一道防线是编辑器实时提示,这个靠 EditorConfig + VS Code 插件,体验友好但不是强制手段。
- 第二道防线是 Git 提交钩子,用 husky 在 pre-commit 阶段跑 lint-staged,只检查本次改动的文件。
- 第三道防线是 CI 流水线里的全量检查,防止本地钩子被绕过,也保证合并到主干之前代码始终是干净的。
这三道防线里,最关键的是第二道。它把“提交”这个动作变成了一道闸门,代码不合规根本提交不上去,自然流不到 CR。第三道是保险,CI 里再跑一遍全量检查,就能覆盖那些本地跳过钩子的情况,以及合并后出现的交叉污染。
我个人的体会是,“提交即合规”最妙的地方在于它把责任前置了。以前是评审人发现问题打回,开发者再改一版,一来一回经常隔上一天。现在是写代码的人提交前机器就告诉他哪里不行,改完再提交,整个过程可能就多花 30 秒。团队的摩擦成本低了一大截。
2. 方案选型:为什么是 husky + lint-staged + ESLint + Prettier 这套组合
2.1 各工具职责边界与组合优势
很多人刚开始搞前端工程化的时候,容易把 ESLint 和 Prettier 搞混,觉得它们都是检查代码格式的,随便挑一个就行。这是本方案里最大的认知误区。ESLint 管的是“代码质量规则”,比如未使用的变量、隐式类型转换、危险的全局变量、Vue 组件里的命名规范;Prettier 管的是“代码排版风格”,比如行宽、缩进、分号、引号。二者有重叠,但定位完全不同。
可以用一个类比来理解:ESLint 是交警,查的是违章行为;Prettier 是洗车行,管的是外观整洁。违章和外观都重要,但不是一个部门的事。
工具链的具体分工如下:
| 工具 | 定位 | 负责内容 | 典型规则示例 |
|---|---|---|---|
| EditorConfig | 编辑器基础风格 | 缩进、换行符、字符集 | indent_style = space |
| ESLint | 代码质量检查 | 语法错误、未使用变量、Vue 规范 | no-unused-vars, vue/multi-word-component-names |
| Prettier | 代码统一排版 | 单双引号、行宽、分号、括号换行 | semi: false, singleQuote: true |
| Stylelint | 样式规范检查 | CSS/SCSS 语法、属性顺序、颜色值 | color-no-invalid-hex |
| husky | Git 钩子管理 | 在 commit 前自动触发脚本 | pre-commit, commit-msg |
| lint-staged | 增量检查工具 | 只对暂存区文件执行命令 | *.vue, *.js, *.css |
| commitlint | 提交信息规范 | 校验 commit message 格式 | type-enum, subject-min-length |
这套组合里最难替代的是 lint-staged。如果不用它,在 pre-commit 钩子里直接跑全量 ESLint,20 人团队的项目光扫描一遍就可能要两三分钟,开发者等得不耐烦,就会开始想各种办法绕过钩子。lint-staged 只取git diff --name-only列出的暂存文件,再按文件名后缀匹配出对应的检查命令,整个过程通常在一两秒内完成,这是方案能被团队接受的关键。
2.2 为什么坚持在本地跑,而不是只靠 CI
我见过不少团队为了省事,不在本地做任何钩子,只在 CI 里跑检查。这种方案执行起来确实简单,但有个致命问题:CI 的反馈周期太长了。一次 pipeline 跑完可能要五到十分钟,开发者提交后还得等着看结果,不合格就打回重提,一个上午可能只能提交两三轮。长期下来,团队会形成“先提交再说”的心理,所有质量压力全部堆积到 CI 那里,反而拖慢了整体节奏。
本地钩子的优势在于反馈即时。代码一提交,半秒钟内检查结果就出来了,有问题当场改当场重提,整个闭环非常顺畅。而 CI 检查作为第二道保险,覆盖的场景是有人用git commit --no-verify强行跳过钩子,或者合并请求把多个分支的代码混在一起产生的组合问题。一道防个人,一道防系统,两道都不可少。
2.3 渐进式落地的策略考量
方案落地的顺序也非常重要,我们吃过一次亏。最早的时候,我把 ESLint 和 Prettier 的规则一次性全部打开,结果老项目几百个文件瞬间冒出上千条错误,pre-commit 变成了一堵绝望的高墙,谁提交谁碰壁,最后大家联合起来要求去掉钩子。后来改成渐进式落地,才真正跑通。
渐进式分三步:第一步先跑 Prettier,因为排版类的规则可以全量一键格式化,改动量大但几乎零风险;第二步跑 ESLint,但先用--quiet只看 error 级别的严重问题,warn 级别的规则等团队习惯了再逐步放开;第三步接入 Stylelint 和 commitlint。每个步骤之间留出一到两周的缓冲期,让团队把手写的习惯慢慢改过来。
3. 配置实操:从零搭建 Vue 项目的提交检查链路
3.1 初始化工程配置与依赖安装
先说明一下,我这里的示例基于 Vue 3 + Vite + JavaScript 技术栈,用 npm 做包管理。如果你用的是 pnpm 或 yarn,命令上稍微变化,但配置思路完全一样。
在项目根目录下安装所有依赖:
npm install -D eslint prettier eslint-plugin-vue eslint-config-prettier stylelint stylelint-config-standard @commitlint/cli @commitlint/config-conventional husky lint-staged这里有一个比较容易被忽略的依赖:eslint-config-prettier。它专门用来关闭 ESLint 中与 Prettier 冲突的代码风格类规则。如果不装这个包,ESLint 和 Prettier 会同时管“引号用单引还是双引”这类规则,两边配置不一致时 lint-staged 会来回报错,出现“eslint --fix 改完,prettier --write 又改回去”的死循环。注意,Vue 项目里eslint-plugin-vue也有部分格式化规则,同样要配合关闭。
3.2 配置文件的具体写法
在项目根目录创建.eslintrc.cjs,内容如下:
module.exports = { root: true, env: { browser: true, es2022: true, node: true }, extends: [ 'eslint:recommended', 'plugin:vue/vue3-recommended', 'prettier' ], parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, rules: { 'vue/multi-word-component-names': 'off', 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn', 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn' } }重点说一下vue/multi-word-component-names这条规则。eslint-plugin-vue 默认要求组件名必须是多单词组合,防止和 HTML 原生标签重名。但实际开发中,很多页面组件比如Home.vue、About.vue都是单单词命名,这条规则会疯狂报警。我们项目里选择关闭它,另外用团队的命名约定来管。
再创建.prettierrc文件:
{ "semi": false, "singleQuote": true, "printWidth": 100, "trailingComma": "none", "endOfLine": "auto" }这是一套非常主流的“无分号 + 单引号”风格配置,如果你团队更喜欢分号风格,把semi改成true就行。endOfLine设置成auto是为了避免 Windows 和 macOS 用户之间因为 CRLF/LF 差异产生无尽的文件变更。
Stylelint 的配置.stylelintrc.json:
{ "extends": "stylelint-config-standard", "rules": { "declaration-empty-line-before": null, "no-descending-specificity": null } }3.3 husky 和 lint-staged 的配置细节
husky 的安装方式在版本迭代中变过几次。V8 之前的版本用的是husky install,V9 之后推荐直接在package.json中声明prepare脚本。以下是当前比较稳健的做法:
npm pkg set scripts.prepare="husky" npx husky init执行完成后,项目根目录会自动生成.husky/目录,并创建pre-commit文件。接着在package.json中配置 lint-staged:
{ "lint-staged": { "*.{js,vue}": [ "eslint --fix", "prettier --write" ], "*.{css,scss,vue}": [ "stylelint --fix" ], "*.{json,md}": [ "prettier --write" ] } }然后在.husky/pre-commit文件里写入:
npx lint-staged这里有几个关键细节值得展开。第一,eslint --fix和prettier --write的顺序有讲究。ESLint 有些规则比如sort-imports会重排 import 顺序,Prettier 不干这个事;但如果反过来先跑 Prettier 再跑 ESLint,ESLint 修完后可能引入新的排版问题,导致最终输出不一致。我们测试下来,先 ESLint 后 Prettier 的组合最稳定,最终文件一定是 Prettier 的格式。第二,stylelint --fix也带了--fix,它不仅能修格式,还能自动补全一些标准的属性顺序。
接着配置 commit-msg 钩子,创建.husky/commit-msg:
npx --no -- commitlint --edit "$1"再创建commitlint.config.cjs:
module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore']], 'subject-min-length': [2, 'always', 5], 'subject-empty': [2, 'never'] } }这套配置强制提交信息遵循 Conventional Commits 规范,格式为type(scope): subject,比如feat(login): 增加手机号登录。这个规范的好处不只是好看,它让后续自动生成 changelog、语义化版本管理、按模块筛选提交记录都变成了可能。
3.4 CI 流水线里的全量检查脚本
本地钩子配置好之后,CI 也不能空着。在项目的根目录增加一个独立脚本文件scripts/ci-check.sh:
#!/bin/bash echo "Running ESLint..." npx eslint . --ext .js,.vue echo "Running Stylelint..." npx stylelint "**/*.{css,scss}" echo "Checking commit messages..." npx commitlint --from "$CI_MERGE_REQUEST_DIFF_BASE_SHA" --to "$CI_MERGE_REQUEST_TARGET_BRANCH_SHA"CI 配置因平台而异,核心就是在合并请求流水线里加一个 stage 执行这个脚本。本地钩子是增量的,CI 是全量的,两者配合下来,理论上任何不合规的代码都很难有机会合入主干。
4. 常见问题与排查技巧实录
4.1 老项目存量代码报错太多怎么办
这是问得最多的一个问题,也是团队落地时最容易翻车的点。有一个同事的团队直接把 lint-staged 接到一个维护了三年的 Vue2 项目上,头一个提交瞬间炸出 300 多个错误,差点原地解散这套方案。
处理存量代码的正确姿势是“新代码严、老代码宽”。具体操作是给 ESLint 增加 overrides 配置:
overrides: [ { files: ['old-module/**/*.{js,vue}'], rules: { 'no-unused-vars': 'warn', 'vue/max-attributes-per-line': 'off' } }, { files: ['src/**/*.{js,vue}'], rules: { 'no-unused-vars': 'error' } } ]这样老模块的代码允许 warn 级别的警告,新模块严格要求 error。CI 里也对应加白名单,等团队哪天有空重构老模块,再把规则逐步收紧。我的经验是,不要指望一次重构到位,但一定要明确新代码的高压线,这样至少不会让存量债务继续扩大。
4.2 ESLint 和 Prettier 冲突导致死循环
这个坑非常经典。症状是 lint-staged 执行后文件被来回修改,状态一直停留在暂存区,甚至出现“刚才改好的代码又变回去了”的诡异情况。原因就是两者规则的职责边界没有划清。
解决办法有两个层面。第一,检查是否引入了eslint-config-prettier,并且它必须是extends数组里的最后一个。第二,ESLint 规则中不要手动配置那些 Prettier 已经管了的格式类规则,比如quotes、semi、indent,这些交给 Prettier 就行。记住分工:ESLint 管代码质量,Prettier 管代码颜值。
4.3 Windows 环境下钩子不执行或闪退
这个问题也出现过不少次。husky 生成的钩子文件依赖 shell 环境,Windows 上如果安装 Git 时没有选“Use Git from the command line and also from 3rd-party software”,钩子会静默失败或者弹一个黑框闪退。另外一个常见坑是钩子文件里的路径问题,比如直接写了node_modules/.bin/lint-staged,但 Windows 上实际路径不同。
稳妥的做法是钩子文件里统一用npx调用,因为它会从node_modules/.bin里自动查找命令。另外检查core.hooksPath的值,执行git config core.hooksPath,如果返回的不是.husky/_而是别的路径,要手动修正。
4.4 commit 信息规范引发的团队抵抗
commitlint 开得太狠,连type配置都严格限制,团队很容易产生“这浪费时间”的抱怨。我在实际推行中逐渐总结出一条折中路线:type保留常见的feat、fix、docs、style、refactor、test、chore就够了,别搞一堆perf、build、ci、revert要求大家背;scope允许为空,不强制;subject只要求不能为空且不少于 5 个字。规则能防住“update file”这种毫无信息的提交,就达到了 80% 的目的,剩下的自由度给开发者自己留。
5. 这套方案还能怎么往外扩展
5.1 从格式合规延伸到静态安全扫描与单元测试
lint-staged 的机制可以扩展到任何你想在提交前快速确认的东西。比如引入eslint-plugin-security检测代码中的危险写法,或者直接加一个单元测试的命令到 lint-staged 的 handlers 里。很多人担心“提交前跑单测太慢”,但配合 lint-staged 只跑变更文件的测试,通常也能控制在几秒内。
对于 Vue 项目,还可以接入vitest,在 pre-commit 阶段对本次改动涉及的组件、composable、store 跑一轮轻量级单元测试。默认推荐的 Vitest 并行模式速度非常快,业务不太复杂的项目基本能控制在 3 到 5 秒。
5.2 把配置沉淀成团队脚手架模板
最理想的状态是,这套配置不是每个项目各搞一套,而是沉淀成一个脚手架模板。新项目初始化的时候直接拉取模板,所有规则、钩子、CI 脚本开箱即用。团队里的 eslint、prettier、stylelint 配置保持一致,意味着切换项目的时候不需要重新适应不同的代码风格。
我们内部就是把.eslintrc.cjs、.prettierrc、.stylelintrc.json、commitlint.config.cjs、lint-staged配置全部收进一个私有脚手架仓库。新项目启动时执行一条命令就能初始化好全部工程化能力,老项目迁移时也只多花半天时间。从这个角度看,提交即合规不只是一次技术升级,更是一套团队协作方式的升级。
5.3 规则治理文化的长期维护
最后说一个很多人忽略的点:自动化工具不是万能钥匙,规则本身需要持续治理。如果团队里没有任何人维护 eslint 配置,两年下来规则会变得又臭又长,甚至出现互相矛盾的条目,最终大家又开始手动跳过检查。
合理的做法是每个季度花半天时间过一遍规则配置,看看有没有因为团队技术栈升级而过时的规则,有没有因为业务场景变化应该调整的开关。这个事应该指定专人负责,或者至少在技术分享会上留出讨论机制。工具负责执行规则,人负责定期校准规则,这样才能让整套方案真正持久地运转下去。