外观
30人前端团队落地实录:ESLint+husky提交拦截推进路径
一、问题背景:规范为什么总是落不下去
去年接手一个 30 人左右的前端团队时,我们面临一个典型困境:规范文档写在 VitePress 站点里,Code Review 时大家也都认同,但代码库里依然充斥着 console.log、未使用变量、随意的 any、风格不一的引号与分号。新人合并第一个 MR 往往要被打回三四次,老人则抱怨“这种格式问题别浪费我时间”。
问题的根源不在于规则本身,而在于执行依赖人。我们做过一次统计:一个 200 行的 Merge Request,Reviewer 平均要花 8 分钟指出风格类问题,而其中 90% 可以被工具自动修复。真正的架构与逻辑问题反而被淹没在格式噪音里。
于是我们确定了目标:把规范从“文档里的约定”变成“提交时无法绕过的门禁”,技术选型为 ESLint 负责静态检查与自动修复,husky 负责 Git 钩子拦截,lint-staged 负责只校验暂存文件以保证速度。
二、方案对比:三种规范落地模式的取舍
在动手之前,我们对比了团队常见的三种执行模式:
| 方案 | 拦截时机 | 优点 | 缺点 | 适用阶段 |
|---|---|---|---|---|
| 纯文档约定 + CR | 合并前人工 | 零配置、灵活 | 依赖人、易反弹 | 5 人以下小团队 |
| CI 流水线校验 | 推送远端后 | 强制、统一 | 反馈慢、污染流水线 | 作为兜底门禁 |
| husky 本地提交拦截 | git commit 时 | 秒级反馈、可自动修复 | 需安装钩子、可被 --no-verify 跳过 | 主力方案 |
最终我们采用“本地 husky 拦截为主,CI 校验为兜底”的双层结构:本地保证绝大多数问题即时修复,CI 防止有人用 --no-verify 或未安装钩子强行提交。
三、实操步骤:从配置到拦截的完整实现
3.1 统一 ESLint 配置(Flat Config)
我们使用 ESLint 9 的 Flat Config,并把规则沉淀成团队共享包。根目录新建 eslint.config.js:
javascript
// eslint.config.js
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import pluginVue from 'eslint-plugin-vue';
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommended,
...pluginVue.configs['flat/recommended'],
{
files: ['**/*.{ts,vue}'],
languageOptions: {
parserOptions: {
parser: tseslint.parser,
},
},
rules: {
// 错误级:必须修复,阻断提交
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'no-console': ['error', { allow: ['warn', 'error'] }],
// 警告级:提示但不阻断,灰度期使用
'@typescript-eslint/no-explicit-any': 'warn',
'vue/multi-word-component-names': 'off',
},
},
{
// 忽略构建产物与依赖
ignores: ['dist/**', 'node_modules/**', '*.config.js'],
}
);在 package.json 中声明脚本:
json
{
"scripts": {
"lint": "eslint . --max-warnings 0",
"lint:fix": "eslint . --fix"
}
}3.2 安装并初始化 husky
bash
npm install -D husky lint-staged
npx husky inithusky init 会创建 .husky/ 目录,并在 package.json 注入 prepare 脚本。关键在于 prepare:团队成员执行 npm install 时会自动安装钩子,无需手动操作。
3.3 配置 pre-commit 钩子
将 .husky/pre-commit 内容改为:
bash
# .husky/pre-commit
npx lint-staged再在 package.json 中配置 lint-staged,只检查暂存区文件,避免全量扫描拖慢提交:
json
{
"lint-staged": {
"*.{js,ts,vue}": ["eslint --fix"],
"*.{css,scss,json,md}": ["prettier --write"]
}
}3.4 配合 commit message 规范
新增 .husky/commit-msg 校验提交信息,统一 changelog 生成:
bash
# .husky/commit-msg
npx --no -- commitlint --edit "$1"3.5 CI 兜底校验
在流水线中增加一步,防止钩子被跳过:
yaml
# .gitlab-ci.yml 片段
lint:
script:
- npm ci
- npm run lint3.6 在 Vue 组件中的实际效果
vue
<script setup lang="ts">
import { ref } from 'vue';
// 未使用的变量会被 no-unused-vars 拦截
const count = ref(0);
// 裸 console.log 会被 no-console 拦截,应改为 console.warn 或移除
console.log(count.value);
</script>
<template>
<button @click="count++">点击 {{ count }}</button>
</template>提交上述代码时,钩子会直接报错并终止提交,开发者运行 npm run lint:fix 即可自动修复大部分问题。
四、推进策略:让 30 人愿意接受门禁
工具只占成功的一半,另一半是推进方式。我们分了三个阶段:
- 只警告期(2 周):规则统一设为
warn,钩子只提示不阻断,让大家适应提示信息; - 增量阻断期:通过 lint-staged 只校验改动文件,历史债务不影响新提交,避免“一装钩子全项目报错”;
- 全量 error 期:新代码规则升级为
error,历史目录用ignores或单独任务逐步治理,CI 同步开启兜底。
同时我们把规则说明、常见报错与修复命令写进 VitePress 文档站,并在每个报错后面附上文档锚点,显著降低了答疑成本。
五、避坑总结:来自一线的 5 条经验
现象:钩子在同事电脑上不生效。 原因往往是克隆仓库后没有执行
npm install,或钩子文件没有可执行权限。修正方法是确保package.json保留"prepare": "husky",并在文档中强调不要用--no-verify绕过;Windows 用户若权限异常可执行npx husky重新初始化。现象:提交时全项目扫描,耗时十几秒。 这是错误地在钩子里执行了
eslint .。正确做法是使用 lint-staged,只把暂存文件传给 ESLint,配合--fix把修复后的内容重新加入提交。现象:ESLint 与 Prettier 规则冲突,格式化后又报错。 典型如分号、换行规则互相打架。修正方法是引入
eslint-config-prettier关闭 ESLint 中所有与格式相关的规则,让 Prettier 专管格式、ESLint 专管代码质量,职责分离。现象:开启规则后历史代码瞬间出现上千个 error,团队强烈抵触。 切忌一次性全量阻断。应先用
warn灰度,再用 lint-staged 锁定增量,历史代码通过ignores隔离后排期治理,让规范“向前生效”。现象:有人使用
git commit --no-verify绕过检查。 本地钩子本质是“君子协定”,必须在 CI 中配置npm run lint作为强制兜底,MR 不通过则禁止合并,才能形成真正闭环。
六、落地收益
推进三个月后,我们的风格类 Review 评论下降了约 85%,单个 MR 的平均 Review 时间从 15 分钟降到 7 分钟,提交前本地检查耗时稳定在 1 秒左右。更重要的是,规范不再依赖某几位资深同学的自觉,而是变成了团队共同遵守、对新人同样友好的基础设施。工具化的门禁,才是规范能够长期存活的关键。
免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。