首页 / 资讯中心 / 文章详情

ESLint+Prettier+Husky:终结代码风格争议的团队规范工具链

ESLint+Prettier+Husky:终结代码风格争议的团队规范工具链 ★ FEATURED ARTICLE
“代码洁癖”这个说法放在个人代码上是一种认真放进团队仓库里就可能变成一场持久战。我见过太多项目commit 历史里一半是“格式化”提交今天有人把单引号统一成双引号明天又有人改回去后天再来一次“reformat”。这不是段子是真实发生过的评审现场。问题的核心不是谁的习惯更好而是这个团队的代码风格根本没有一个“说了算的机制”。ESLint、Prettier、Husky 这套组合就是用来终结这类内耗的。这篇文章写给谁写给那些正在带团队、或者经历过“代码评审一小时半小时在吵缩进”的开发者。如果你是一个人的项目想怎么写都行但只要超过两个人协作风格分歧带来的沟通损耗就会指数级上升。我会从工具分工、完整搭建流程、冲突排查、团队落地几个角度把这套东西讲透。文章里的配置和命令都是可以直接拿去用的。1. 先看清楚规范问题到底烂在哪里1.1 评审现场的“格式之争”比想象中更贵先还原一个典型场景。某次代码评审功能逻辑没有任何争议但评论区被刷了十几条“这里能不能换行一行太长了”“为什么这个文件是4个空格缩进其他文件是2个”“变量名缩写我看不懂改个名吧”“这个对象为什么最后一个逗号加了前面几个没加”这些注释单独看都有道理但它们根本不值得出现在评审上。代码评审的价值在于讨论逻辑漏洞、设计取舍、边界情况而不是当活体格式化工具。每次这种争论一出现实际上就说明团队缺少一个前置的、自动化的“格式契约”。而且这种争论是有累积成本的。一次评审1小时其中20分钟在讲格式三次评审下来就浪费了一小时。如果这是两个人那就是双倍时间。放到整个团队的维度一年下来浪费掉的开发工时是相当可观的。更麻烦的是这种争论会消耗人的情绪——没人愿意被反复纠正缩进风格也没人愿意做一个“每次都挑格式毛病”的讨厌鬼。于是最后的结果往往是大家不再在评审里提格式了仓库越来越乱。1.2 风格分歧的本质是“没有统一裁决机制”每个人在写代码的时候都有自己的肌肉记忆。有人习惯先写函数再声明变量有人喜欢先推导类型再写实现有人喜欢把条件判断里多个布尔值拆成多行有人觉得一屏看完更爽。这些偏好没有对错它们只是不同。如果没有一个工具来裁决“谁的声音大就听谁的”就成了事实上的规则。今天小组长喜欢双引号全组改双引号下周组长换人了新组长喜欢单引号又改一轮。这种反复除了让代码历史变得难以追踪还会让团队失去对代码规范的敬畏——反正风格改来改去认真写规范有什么意义我个人的观点是代码风格问题的本质不是审美问题而是沟通损耗问题。如果仓库里只有一个人写某个模块他的风格再奇怪也只会折磨他自己如果这个模块要被三个人长期维护统一的格式就是最低成本的协作基础。1.3 不治理的隐性成本diff混乱与审查疲劳还有一个经常被忽略的问题——代码审查的 diff 质量。我见过一个业务组件功能改动其实只有三行但因为有人顺手把整个文件的引号从双引号改成单引号diff 页面显示几百行的变更。评审人看到这种 diff第一反应是“这改了什么没法看跳过吧”。真正埋藏在三行改动里的逻辑问题反而被格式化噪声盖过去了。Diff 混乱、审查疲劳、git blame 失真这三者会相互强化。当审查者开始用“跳过”来逃避噪声团队的代码质量防线就从这里破了一个口。这也是我觉得必须用工具链把格式问题完全自动化的原因——不让“人的注意力”去处理“机器能做的事”。2. 工具分工ESLint、Prettier、Husky 各司其职2.1 ESLint管的是“代码质量与正确性风险”很多人把 ESLint 和 Prettier 混在一起觉得都是“检查代码的”。但它们的侧重点完全不同。ESLint 盯的是逻辑层面的风险有没有声明了变量但没使用no-unused-vars、有没有用了而不是eqeqeq、有没有定义了某个变量又被重新赋值prefer-const这类问题。ESLint 的好处是规则非常丰富社区生态也成熟比如eslint-plugin-react、eslint-plugin-vue、typescript-eslint/eslint-plugin这类针对特定技术栈的插件。它更像是一位“戴着显微镜的代码律师”逐条对照规则清单把潜在问题揪出来。它的能力边界也很明确ESLint 不会管你的代码好不好看。例如“一行代码要不要在 80 个字符处换行”、 “对象属性后面要不要加尾逗号”这些格式细节 ESLint 虽然也有一堆规则能管但强行用它来管格式化会非常别扭——规则之间的优先级和冲突会让配置变成一团乱麻。2.2 Prettier管的是“格式风格的绝对统一”Prettier 的定位简单粗暴我是格式化工具我不跟你讨论审美我有我自己的固定风格。它接受一个代码片段输出一种格式化后的结果。你不需要在配置里声明“每个数组元素单独一行”还是“允许全部在一行”Prettier 会用它的算法基于行宽自动决定怎么换行。它解决的是“人的品味”问题。“我喜欢 Allman 花括号风格你喜欢 1TBS 风格”这种争论在 Prettier 面前没有讨论空间因为它只会输出一种预设风格。对团队来说这反而是优势——你要做的不是开会讨论哪种风格更好而是决定“我们统一用 Prettier 默认值还是只改两个参数”。注意Prettier 只管格式不管正确性。它不会告诉你“你这个函数有未使用参数”但会把你的缩进、引号、分号全部统一。2.3 Husky把规范卡在“提交之前”前面两个工具解决的是“怎么管”的问题Husky 解决的是“什么时候管”的问题。它负责接入 Git Hooks最常见的是pre-commit钩子——在开发者执行git commit之前自动执行一段脚本比如运行 ESLint 和 Prettier。如果一个人只在本地写了代码但没跑 lint他照样能把代码 push 到远端。这个时候代码评审人又要当人肉扫描仪。而 Husky 加 lint-staged 的机制可以在本地的git commit这扇门之前就拦下不合规的代码。配合 lint-staged 只检查暂存区文件效率也高不用每次对全仓库做检查。2.4 为什么“一个人自律”替代不了“一套机制”我经常听到一种说法“我们团队的人都很有责任心靠自觉就行。”这种说法我完全不同意。责任心是会波动的人在赶需求、熬夜、被产品经理催进度的时候注意力会放在功能实现上格式和规范这种事必然会被挤到边缘。这不是人品问题是人性问题。而且“自律”无法跨设备复用。开发者在 A 电脑上改了代码回家用 B 笔记本电脑提交B 电脑上没有装 lint 插件、没有配置编辑器格式化他根本不知道自己的代码格式已经跑偏了。这种场景靠“自觉”怎么兜住只有机制能兜住。工具链的价值就是把“必须记住并遵守的规则”变成“机器自动执行的动作”。3. 从零搭建一套可以照抄的完整配置3.1 版本选择与依赖安装先说我推荐的工具版本思路ESLint 8/9、Prettier 3、Husky 9新项目建议直接用目前主流的稳定版本具体以官方最新为准。如果你在维护老项目尽量选与现有环境兼容的版本组合不要在版本升级和业务重构同时进行否则排查问题时根本分不清是哪个环节出的错。在项目根目录执行npm install --save-dev eslint prettier husky lint-staged eslint-config-prettier如果你的项目用了 TypeScript还需要npm install --save-dev typescript-eslint/parser typescript-eslint/eslint-pluginReact 项目可以再加npm install --save-dev eslint-plugin-react我习惯用--save-dev而不是全局安装。全局安装的问题在于不同项目的 ESLint 版本可能互相打架而且团队成员不一定都装了全局工具。装在项目里node_modules/.bin下的版本是确定的跑起来也稳定。3.2 ESLint 配置文件的写法ESLint 8 之后支持多种配置文件格式我推荐使用.eslintrc.cjs因为 CommonJS 模块在大多数项目里兼容性最好。一个比较通用的 JS 项目配置可以长这样module.exports { root: true, env: { browser: true, es2022: true, node: true }, parserOptions: { ecmaVersion: latest, sourceType: module }, extends: [eslint:recommended, prettier], rules: { no-unused-vars: [error, { argsIgnorePattern: ^_ }], eqeqeq: [error, always], prefer-const: error, no-console: process.env.NODE_ENV production ? warn : off } };几个关键点root: true很重要。它的作用是告诉 ESLint“这个文件是项目的根配置不用再往上级目录一级级找配置文件了”。在 monorepo 或者嵌套目录结构里不设置root容易跑到父目录的配置导致规则不一致。extends最后一项放了prettier后面会细说它的作用是关掉 ESLint 里所有和 Prettier 冲突的格式化规则。no-unused-vars打开了。很多团队觉得未使用变量“无所谓就是多了个变量而已”但实际它往往意味着“删了旧的实现忘了清理残留”是代码腐化的信号。我加了process.env.NODE_ENV production ? warn : off这种写法意思是本地开发时可以随便留console.log生产构建时给出警告提醒你清理调试日志。这个策略比“全关”或者“全开”都实用。3.3 Prettier 配置与编辑器联动Prettier 的配置文件是.prettierrc.json也可以写在package.json里的prettier字段中。我习惯单独文件这样编辑器插件和命令行工具都能识别。{ semi: true, singleQuote: true, printWidth: 100, tabWidth: 2, trailingComma: es5, arrowParens: always }这套配置的含义semi: true统一保留分号。我知道很多新项目喜欢无分号风格但分号之争没必要定一个就好。singleQuote: true字符串用单引号。默认会在字符串内部有单引号时自动改成双引号。printWidth: 100一行最长 100 个字符。默认值是 80偏保守和很多公司现有代码的宽度不匹配。100 对现代显示器很友好也不会让代码太碎。trailingComma: es5对象和数组的尾逗号保留函数参数不加。这个设置能减少 diff 噪声又能避开部分浏览器的兼容问题。arrowParens: always箭头函数参数永远加括号即使只有一个参数也写(x) x 1。配置好了之后建议在编辑器里做到“保存即格式化”。以 VSCode 为例先安装 Prettier 扩展然后在settings.json里设置{ editor.formatOnSave: true, editor.defaultFormatter: esbenp.prettier-vscode, editor.codeActionsOnSave: { source.fixAll.eslint: explicit } }这样每次保存编辑器会先用 Prettier 格式化再顺手执行 ESLint 的自动修复。两个动作在保存的一瞬间完成开发者几乎感知不到代码就已经是规范状态了。3.4 如何让 ESLint 和 Prettier 不打架这是一个非常经典的坑。ESLint 里有一些规则本身也是管格式的比如quotes引号、semi分号、comma-dangle尾逗号。你启用了 Prettier它把单引号改成双引号ESLint 又跳出来说“这个该用单引号”——两个工具互相打脸。解决办法不是手动去改一堆规则而是引入一个官方给出的适配包npm install --save-dev eslint-config-prettier然后在 ESLint 配置的extends数组里把prettier放在最后一位。它的作用是把 ESLint 中那些会与 Prettier 冲突的格式化类规则全部关掉。规则顺序不能乱因为extends数组后面的配置会覆盖前面的同名配置。注意不要用eslint-plugin-prettier把 Prettier 当成 ESLint 的 rule 跑。那个方案不是不能用但每次 lint 都要同时跑 ESLint 和 Prettier会明显变慢。更合理的方式是ESLint 管逻辑Prettier 管格式两个工具各跑各的互不干扰。3.5 Husky lint-staged提交前自动整顿lint-staged 的作用可以用一句话概括只检查你准备提交的文件。它会把git diff --cached里的暂存文件列表拿过来匹配到你配置的 glob 模式上然后执行对应的命令。第一步初始化 Husky。以 Husky 9 为例npx husky init这个命令会在项目里生成.husky/目录并默认创建一个pre-commit钩子文件。如果你用的版本生成的内容略有不同也没关系手动改这个文件就行。第二步在package.json里配置 lint-staged{ lint-staged: { *.{js,jsx}: [eslint --fix, prettier --write], *.{ts,tsx}: [eslint --fix, prettier --write], *.{json,css,scss,md,html}: [prettier --write] }, scripts: { prepare: husky } }第三步修改.husky/pre-commitnpx lint-staged原理其实很简单在git commit之前pre-commit钩子触发npx lint-stagedlint-staged 拿到暂存区文件列表找到匹配*.{js,jsx,ts,tsx}的文件依次执行eslint --fix和prettier --write两条命令都会修复代码如果修复后仍有不可自动修复的错误命令会以非零状态退出git commit会被阻止。注意eslint --fix前面最好放在prettier --write之前这样 ESLint 修完逻辑问题后Prettier 再做最终排版兜底确保提交的代码格式一定经过统一校准。3.6 新增脚本命令与联调验证配置完上面这些我会在package.json的scripts里补上几个常用命令{ scripts: { lint: eslint . --ext .js,.jsx,.ts,.tsx, lint:fix: eslint . --ext .js,.jsx,.ts,.tsx --fix, format: prettier --write ., format:check: prettier --check . } }这四个命令分别对应不同场景npm run lint开发过程中随时检查全部源码。npm run lint:fix自动修复能修的问题。npm run format全仓库格式化适合刚接入工具链时清理存量代码用。npm run format:checkCI 里拿来做格式校验确保所有文件符合 Prettier 风格。最后别忘了手动验证一下整条链路。随便改一个文件把缩进改成乱序然后git add .再git commit观察输出里有没有 pre-commit 钩子的执行日志。如果命令没报错但代码没被格式化多半是 lint-staged 配置的 glob 模式没匹配上文件后缀。4. 接入过程中踩过的坑与排查心得4.1 冲突排查ESLint 和 Prettier 互相干架的现场第一类冲突是“引号之争”。配置了 Prettier 用单引号但 ESLint 里quotes规则默认要求双引号保存文件的时候光标瞬间被拉回两下然后编辑器右下角弹一个 ESLint 报错。解决方式就是前面说的eslint-config-prettier不多解释。第二类冲突是“换行之争”。比如max-len规则限制一行 80 个字符但 Prettier 的printWidth是 100于是 Prettier 把长函数参数重新排版之后ESLint 立刻报“这一行太长”。这个配置坑我也踩过。应对办法是在 ESLint 的rules里显式关掉不符合 Prettier 宽度的规则rules: { max-len: off, comma-dangle: off, quotes: off, semi: off }实际上eslint-config-prettier会自动处理这些规则的开关但如果你自己又手动开启了某条规则它就不会被自动关闭。所以排查冲突的第一原则是先检查这些格式类规则有没有被extends: [prettier]覆盖再检查自己手动加的规则和 Prettier 是否矛盾。4.2 Husky 不生效的常见原因Husky 接入后最常听到的抱怨是“我配置了 pre-commit但 commit 的时候它就是不跑。”遇到这种情况我建议按下面的顺序排查首先确认.git/hooks/pre-commit这个文件的内容。Husky 新版本是通过core.hooksPath把 Git 钩子指向.husky目录的。如果你的core.hooksPath没有正确指向项目里的.husky文件夹Git 根本不会执行它。git config core.hooksPath如果输出结果不是项目里的.husky路径可以执行git config core.hooksPath .husky其次确认package.json里是否在scripts中配置了prepare: husky。这个 prepare 脚本非常关键——它会在执行npm install的时候自动初始化 Husky。很多团队成员 Clone 仓库后没有重新跑npm install或者环境变量里跳过了 prepare 脚本就导致本地根本没有有效钩子。最后检查钩子文件是否具备可执行权限。在 Windows 和 macOS 上对权限的处理机制不一样如果你在用跨平台环境可以给.husky/pre-commit加一下可执行权限chmod x .husky/pre-commit4.3 lint-staged 明明配了但提交时还是没有格式化这个问题大概率出在“文件没被匹配”。lint-staged 的 glob 模式是相对项目根目录来的如果项目里文件后缀是.tsx但配置里只写了*.{js,jsx}那自然匹配不到。还有一种情况开发者先把文件git add了之后又对文件做了进一步修改但忘记再git add一次。lint-staged 检查的是暂存区里那一版内容工作区里的最新修改不在检查范围内。这不是工具的 bug而是操作习惯的问题。遇到这种“怎么没生效”的困惑先把改动重新git add再 commit 试一次。4.4 老项目接入的存量告警淹没问题老项目接入 ESLint 最头疼的不是配置而是那一堆历史代码刷出来的几千条 warning。我第一次给一个实训项目接这套工具时跑完 lint 直接出来两千多条报错团队瞬间就没有继续的欲望了。这里我推荐一个渐进式策略第一周只把 ESLint 跑起来但所有规则先设为warn让开发者看到但不强制阻断。第二周用eslint --fix修一遍能自动修复的问题手动修复高频的几十条常见错误。第三周把对项目最有价值的规则比如eqeqeq、no-unused-vars、react-hooks/rules-of-hooks提升到error。第四周打开 Husky开始阻断不合规提交。另外一个很实用的技巧在 ESLint 配置里用overrides为一些历史遗留目录单独降级。比如src/vendor、src/deprecated里的代码暂时先只给warning等后续重构再收紧。overrides: [ { files: [src/vendor/**/*.js, src/deprecated/**/*.js], rules: { no-unused-vars: off, no-console: off } } ]这样能保住新代码的严格检查同时照顾到老代码的现实情况。5. 让团队真正跑起来我的落地经验与建议5.1 别把工具链做成“制度压人”我知道有些团队引入这套工具链时会走味——技术负责人直接把配置一写丢到群里说“以后用这个不符合就不准合并。” 我不推荐这么做。更好的方式是开一个简短的分享会把“为什么”讲清楚。可以现场演示一个没有工具链的 demo故意提交一份代码到分支然后让大家在评审里假装讨论 5 分钟格式问题再演示加了 Husky 之后commit 瞬间被拦截格式化自动完成。这种对比比任何口头宣讲都有说服力。和团队约定的时候我会把原则说清楚工具的职责是把一切可自动化的规范自动化人的职责是解决工具解决不了的问题比如逻辑设计、边界判断和可维护性决策。一旦这个共识建立起来就不会有人觉得“lint 是公司派来管我的监工”。5.2 渐进式推广并不指望一步到位我在工作室和团队里实践这套流程的经验是先从“核心仓库”和“核心模块”开始不要第一次就要求全仓库所有文件零 error。核心模块的严格检查跑顺了再慢慢铺开。具体来说第一个接入的模块不要选老代码最多的模块而是选一个“正在新开发”的中等模块。新开发没有历史包袱跑起来干净团队成员也能第一时间感受到“这套工具帮我把问题在提交前止住了”。先在核心模块上验证出价值再去推进别的模块阻力会小很多。另外在最初两周如果发现某条规则频繁被误报不要硬撑着该关就关。规则是为代码服务的代码不是为规则服务的。宁可保留一个“团队真正认可并且能坚持执行”的规则集合也不要一次性叠加过多规则让成员觉得寸步难行。5.3 在 CI 上也要加一道兜底最后说一个很多团队会漏掉的环节本地 Husky 做得再完善也无法保证永不绕过。团队成员可能用git commit --no-verify跳过钩子或者直接在网页端修改文件不经过本地 git 流程。所以 CI 上的检查不能少。把下面这段放到流水线里npm run lint npm run format:check这两条命令没有--fix只做“检查”。一旦代码不合规构建就会失败。这样一来即使本地钩子被跳过了远端还有一道门。而且 CI 上的失败记录是长期留存的比“某人私下跳过钩子”这种口头问题要有说服力得多。5.4 关于“代码洁癖”的最终理解我一直觉得“代码洁癖”这个词是被误解的。真正的洁癖不是“我看不惯别人的风格就忍不住要改”而是“我不需要把精力花在风格上因为工具已经替我保证了”。人眼和手动调整永远赶不上工具的确定性。与其培养一个追求完美格式的评审人不如把这份执念放进配置文件和钩子脚本里。我个人在实际项目里的体验是接入这套工具链之后的第一个月代码评审的平均时长下降了将近一半剩下的时间基本都花在了真正的逻辑讨论上。这种“格式再也不需要被评审”的安心感一旦用上就回不去了。这套东西的价值不在“完美主义”而在“把人的注意力还给逻辑本身”。如果你现在还在跟团队里为缩进和引号争论这周就可以先把 Prettier 和 ESLint 接上。先从两个人的项目中跑起来让工具替你说话。
阅读完成 · 觉得有帮助?
咨询建站