拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

前端代码规范自动化:ESLint、Prettier与Git钩子让团队零摩擦

前端代码规范自动化:ESLint、Prettier与Git钩子让团队零摩擦 有一次评审会上前端组为了一行代码该用双引号还是单引号吵了二十分钟。我坐在旁边看着两个都挺资深的工程师为这个毫无生产力的问题争得面红耳赤。不夸张地说几乎所有没做代码规范自动化的团队都会在某个深夜为这种问题消耗掉本应该留给业务逻辑的热情。后来我在团队里做了一轮代码规范自动化的改造把 ESLint、Prettier、Husky、lint-staged、commitlint 这些工具串成一条流水线让机器在代码进入评审之前就把风格问题和低级错误全部拦掉。效果比预想中好很多——不是检查出来的错误数量有多惊人而是评审群里再也没有出现过“这里应该用单引号”这种对话。团队协作的摩擦就这样被无声无息地消掉了。这篇内容就围绕这套“规范自动化”体系来聊。我会说清楚为什么要自动化、每个工具扮演什么角色、从零怎么配、以及真正跑起来之后那些文档里不会写的问题。适合正准备给团队做规范建设、或者自己想在项目里引入这套流程的开发者。1. 靠人提醒的规范注定失败从评审拉锯战说起1.1 一次典型的两小时评审拉锯战我给你还原一个场景。老张提交了 500 行代码里面用了 4 空格缩进、单引号、if不加花括号还有两个var声明的变量。小李负责评审他习惯 2 空格、双引号要求所有if必须带{}。于是评审会议的画风就变成了“这里为什么用单引号项目规范里写的是双引号。” “单引号不是更省事吗” “规范就是规范。” “那这个缩进为什么是 4 空格” “你上次不是也用了 4 空格吗” “那是你让我改的。”两小时过去代码逻辑本身有没有问题没人来得及关心。这种场景只要出现几次团队里就会形成两派一派觉得评审浪费时间一派觉得写代码的人不守规矩。无论哪一派赢了最后输的都是产品和代码质量。我当时的判断很简单只要规范需要靠“人”来提醒它就一定会在疲劳、赶进度、换新人这些压力下崩溃。人肉执行规范这件事本身就不符合工程化的逻辑。1.2 为什么“自觉”和“文档”都靠不住有的团队会说我们写一份很详细的规范文档不就行了我见过不少这样的情况文档洋洋洒洒几十页从命名、注释到缩进全部写清楚但实际效果基本等于零。原因不复杂。第一文档违背了人的惰性。赶版本到凌晨两点的时候没人会去翻规范文档确认函数命名是不是动宾结构。人在疲劳状态下的唯一目标就是让代码“跑得通”。第二规范必然有歧义。什么叫“语义化命名”什么叫“合理拆分组件”写文档的人觉得明确执行的人觉得模糊。于是文档本身又成为新的争吵来源。第三人不是机器记不住那么多规则。哪怕今天记住了下个月又忘了一半。更不用提不同框架、不同历史项目的差异化规则靠脑子记根本不可能。所以关键结论是规则必须从“人的记忆”里搬到“代码库的配置”里。机器能稳定、可重复地执行规则人只需要在规则本身需要调整的时候参与讨论。1.3 规范自动化的三层目标在我眼里规范自动化不是一个工具而是一套分层目标第一层格式统一。代码长什么样缩进、引号、分号、换行这些交给格式化工具处理。第二层静态检查。代码写得好不好有没有未使用的变量、明显 bug、违反团队约定这交给 Linter 处理。第三层提交规范化。每个提交携带什么信息、临时调试代码是否被带入主干这交给提交钩子处理。三层全部跑通之后代码评审这个环节就只剩下一件事评审业务逻辑和架构。评审者不需要再说“这边缩进不对”“那个变量名改一下”这类声音会从团队里彻底消失。2. 工具链拆解每个工具到底解决了什么问题2.1 ESLint给 JavaScript 代码做静态诊断ESLint 是整个体系里最核心的一块。它的工作方式是先把代码解析成一棵抽象语法树AST然后在这棵树上执行各种规则检查。你可以把 AST 理解为代码的“骨架结构图”比如一个if语句在树上是哪个节点、一个变量声明挂在哪里ESLint 都看得清清楚楚。规则就是在这些节点上做判断发现不符合预期的就报出来。我举几个最常见的规则实例no-unused-vars声明了但从未使用的变量。eqeqeq要求必须使用而不是。no-console禁止在生产代码里留console.log。no-await-in-loop循环里不允许使用await。这些规则里有相当一部分自带自动修复能力。执行eslint --fix的时候机器会直接改写代码来满足规则比如把换成、删掉多余的空格。注意ESLint 的自动修复是保守的它只修复那些“改完不会改变语义”的问题其他问题仍然需要人工介入。ESLint 还有一个庞大的插件生态。TypeScript 项目要用typescript-eslintVue 项目要用eslint-plugin-vueReact 项目则用eslint-plugin-react和eslint-plugin-react-hooks。这些插件就是为了让 ESLint 理解对应框架的“语义”告诉你哪段代码是死代码、哪个 Hook 依赖写错了。2.2 Prettier格式化问题的另一种解题思路很多人刚开始会把 Prettier 和 ESLint 搞混以为它们做的事差不多。实际定位差异很大ESLint 关心的是“代码写得对不对”Prettier 关心的是“代码长得整不整齐”。拿一条 120 个字符的语句为例。ESLint 可以告诉你“有一行太长了建议拆开”但怎么拆它不管Prettier 则会直接把它重写成统一的换行格式。换句话说ESLint 负责质量和隐患Prettier 负责风格和外观。Prettier 是一个高度“固执”的工具官方设计理念就是不给你多少自定义空间选项加起来也不到二十个。这一点恰恰是它最大的优点不管谁来格式化结果都一样。团队里就算有一百名成员只要统一用 Prettier产出的代码样式就完全是同一个模板印出来的。这里顺便解决一个历史遗留问题ESLint 自身也有不少格式化类规则比如控制缩进的indent、控制引号的quotes。如果同时又用 Prettier两边的规则很容易打架。业界的通行做法是格式化一律交给 PrettierESLint 的格式化规则全部关闭。后面我会讲具体怎么关这是配置环节最容易踩坑的地方。2.3 lint-staged 与 Husky把检查拦在提交前规范检查再强大如果只在 CI持续集成上跑开发者提交代码之后还是要等几分钟甚至几十分钟才能发现问题。更靠谱的做法是在git commit这个动作发生前就自动执行检查把有问题的代码直接挡在本地。实现这个靠的是 Git 本身的 hooks 机制。Git 在提交的不同阶段会执行特定脚本pre-commit发生在提交之前commit-msg发生在提交信息输入之后。你可以在项目目录下的.git/hooks/里手动写这些脚本但问题在于.git目录不会被提交到仓库每个新成员 clone 之后都要手动配一遍太反人类。Husky 就是来解决这个痛点的。它允许你把 hook 脚本放在项目仓库里通常是.husky/目录别人安装依赖后自动激活。新版 Husky v9 的做法非常简洁执行npx husky init会自动生成对应的 hook 文件。lint-staged 是另一个关键拼图。它的作用非常直观只检查“已经被git add暂存”的文件。为什么这个能力重要因为在大项目里直接跑全量eslint可能要二十秒甚至更久每次提交都等二十秒团队会把钩子禁用掉。lint-staged 会让检查变得轻量只有你本次提交的文件才会被检查几千个历史文件根本不在这个批次里。2.4 commitlint把提交信息也纳入规范如果说前面几个工具管的是“代码内容”commitlint 管的就是“代码历史”。它检查的是提交信息是否符合一种约定格式最常见的是 Conventional Commits 规范feat: 新增用户登录功能 fix: 修复订单金额计算错误 docs: 更新部署文档这种格式的提交信息不是形式主义。有了稳定的分类前缀团队可以一键自动生成 Changelog可以按照fix、feat筛选历史记录可以快速定位某个需求是什么时候合入的。没有规范和自动化提交信息就会因为成员当天的心情而千奇百怪比如“改bug”“update”“aaa”三个月之后你自己都看不懂。commitlint 配置在commit-msghook 上提交信息不符合约定时提交直接在本地被终止。到这里五个工具的角色已经清晰了工具管什么在哪一层生效ESLint代码质量和隐患编辑器、pre-commit、CIPrettier代码格式和外观编辑器、pre-commitHuskyGit hooks 管理本地提交时lint-staged只处理本次提交文件pre-commitcommitlint提交信息格式commit-msg3. 落地实操一套最小可用的前端规范自动化配置3.1 准备环境与安装依赖下面这套配置我以 Vue3 Vite TypeScript 前端项目为背景用 npm 做包管理。这套思路完全可以平移到 React、Node.js 后端等场景只是插件和规则包换一换。安装依赖npm install -D eslint eslint/js typescript-eslint eslint-plugin-vue \ prettier eslint-config-prettier husky lint-staged \ commitlint/cli commitlint/config-conventional初始化 Huskynpx husky initv9 的 Husky 初始化之后会自动创建.husky/pre-commit文件里面的默认内容是npm test我们待会儿要把它替换成 lint-staged。同时它会在package.json里加上一条prepare: husky脚本这个脚本是保证其他人安装依赖时能自动激活 hooks 的关键不要删掉。3.2 ESLint 的 flat config 配置ESLint v9 开始默认使用 flat config配置文件叫eslint.config.js取代了老版本的.eslintrc系列。对于 Vite 工程项目里一般已经设置了type: module所以可以直接用 ESM 语法写配置import js from eslint/js; import tseslint from typescript-eslint; import vue from eslint-plugin-vue; export default tseslint.config( { ignores: [dist/**, node_modules/**, coverage/**], }, js.configs.recommended, ...tseslint.configs.recommended, ...vue.configs[flat/recommended], { rules: { no-console: warn, vue/multi-word-component-name: off, }, }, );逐段解释一下。ignores是 flat config 里替代.eslintignore的写法把构建产物和依赖目录排除在外。js.configs.recommended是 ESLint 官方推荐规则集。tseslint.configs.recommended是 TypeScript 项目的推荐规则集注意这里要用展开运算符展开成数组。vue.configs[flat/recommended]是 Vue3 插件的推荐规则它要求使用 Vue 插件的 flat config 版本。最后一段rules是用来覆盖团队自定义规则的。我故意把no-console设成warn而不是error是考虑到很多团队在开发期还希望保留日志输出直接报错会逼疯所有人的。vue/multi-word-component-name则关掉了 Vue 官方推荐里要求组件名必须多个单词的规则因为这个约束对很多内部小项目来说过于严格。3.3 Prettier 配置与冲突处理在项目根目录建.prettierrc.json{ semi: true, singleQuote: true, printWidth: 100, trailingComma: all }这套参数是很多团队的主流选择行尾加分号、单引号、单行 100 个字符以内、对象和数组尾随逗号。Prettier 的选项不多够用就好。接下来是关键的衔接环节。ESLint 和 Prettier 确实有规则重叠比如都管引号、缩进。解决办法是安装eslint-config-prettier并在 ESLint 配置的最末尾加载它import prettier from eslint-config-prettier; export default tseslint.config( // ...前面所有配置 { rules: { no-console: warn, vue/multi-word-component-name: off, }, }, prettier, // 必须放在最后 );eslint-config-prettier的作用是关闭所有和 Prettier 冲突的格式化类规则。这么配置之后整个体系的职责边界就非常干净了ESLint 判断质量Prettier 统一格式两者不再互相打架。3.4 Husky 钩子与 lint-staged 配置修改.husky/pre-commit把内容替换为npx lint-staged然后在package.json里增加 lint-staged 配置{ lint-staged: { *.{js,ts,vue}: [eslint --fix, prettier --write], *.{css,scss,less}: [prettier --write], *.{json,md,yml}: [prettier --write] } }lint-staged 匹配到相关文件后会按顺序执行对应的命令。这里有一个细节值得强调在 lint-staged 配置里我直接写eslint --fix而不是npx eslint --fix。因为 lint-staged 会主动把node_modules/.bin加进环境变量直接调用项目本地装好的 ESLint 最干净。如果写npx eslint某些环境下 npx 可能会去远程拉一个版本反而导致行为不一致。再创建一个.husky/commit-msg文件内容npx --no -- commitlint --edit $1同时创建commitlint.config.cjsmodule.exports { extends: [commitlint/config-conventional], };接着在package.json里编排好统一的 npm scripts{ scripts: { lint: eslint ., lint:fix: eslint --fix ., format: prettier --write . } }到这里本地提交的拦截链路已经完整串起来了开发者在git commit时pre-commit钩子会先触发 lint-staged只对本次暂存文件做 ESLint 修复和 Prettier 格式化提交信息输入之后commit-msg钩子会校验提交信息格式。任何一环不通过提交都会失败。3.5 编辑器配合保存时自动修复钩子只是最后一关真正让开发者感受到“零摩擦”的是编辑器在写代码的时候就默默把事情做了。强烈建议把以下配置随仓库提交到.vscode/settings.json{ editor.defaultFormatter: esbenp.prettier-vscode, editor.formatOnSave: true, editor.codeActionsOnSave: { source.fixAll.eslint: explicit } }同时把扩展名写进.vscode/extensions.json{ recommendations: [ dbaeumer.vscode-eslint, esbenp.prettier-vscode ] }这两个文件的妙处在于新成员 clone 项目后VS Code 会弹出推荐安装扩展的提示装完即获得和团队完全一致的编辑器行为。保存文件时Prettier 负责格式化ESLint 负责自动修复质量类问题。大多数 trivial 问题在按下 CtrlS 的那一瞬间就消失了developer 甚至感知不到规范检查的存在。4. 踩坑实录多数团队自动化失败的真实原因4.1 Husky 钩子不生效完整排查链路我先说一个最常见的翻车现场规则配好钩子也写了但当开发者执行git commit时lint-staged 纹丝不动提交直接通过了。为什么看起来没效果排查链路是有固定路线的。第一步确认 Git 的 hooks 路径配置git config core.hooksPath当这个值被全局配置指向了某个别的目录比如一些 GUI 工具、或以前装过的旧版 husky项目自己的.husky目录就不会生效。如果发现输出不为空且不是项目内的.husky执行git config --unset core.hooksPath第二步检查.husky/pre-commit文件是否有执行权限。在 Linux/macOS 上文件权限丢失也可能导致钩子被静默跳过给文件加执行权限即可chmod x .husky/pre-commit .husky/commit-msg第三步确认npm install执行过且prepare脚本跑通了。Husky 的激活依赖安装依赖时触发的脚本。如果团队里有人在 CI 环境特殊处理了prepare或者某个成员 install 的时候加了--ignore-scriptshooks 就不会刷新。第四步检查命令本身。pre-commit里如果写的是npm run lint而lint脚本执行eslint . --max-warnings 0那这个检查是全量的不是 lint-staged 增量模式。很多团队说“lint-staged 没生效”其实是因为 pre-commit 脚本里根本没有调用 lint-staged只是调用了全量 lint。这种情况要么换回 lint-staged要么接受每次提交都等十几秒。4.2 lint-staged 误伤文件暂存区与工作区的认知误区有同事反馈“我明明只git add了 A 文件为什么报错里出现了 B 文件”第一次听到这话我也愣了一下后来发现是对 Git 机制理解有偏差。Git 有三个区工作区、暂存区index、本地仓库。git add把改动从工作区带进暂存区git commit提交的是暂存区的快照。lint-staged 判断“本次提交了哪些文件”看的是git diff --name-only --cached也就是暂存区里的文件清单。所以真相是如果 B 文件之前已经git add过、现在还在暂存区里那么它就会出现在 lint-staged 的检查清单里。这不算误伤它是规则本身的正常行为。另一个容易导致“误伤”的场景是git commit -a。这个命令会把所有已追踪文件的改动自动加入暂存区包括那些你只想留在工作区、暂时不想提交的文件。一旦用了-alint-staged 会把所有被加入暂存区的文件全部检查一遍。我的建议很直接在团队里统一使用“显式git add再git commit”的流程不要用git commit -a这种含混命令。还有一个细节是 lint-staged 修复后的动作。它执行完eslint --fix和prettier --write之后会用git add把修复后的文件重新加回暂存区。如果不加回提交的内容就是修复前的版本等于修复了个寂寞。这一点 lint-staged 默认帮我们做掉了但如果你自定义 lint-staged 的 command 时写了额外的操作要留意不能破坏暂存区状态。4.3 规则冲突导致“修复完又报错”的循环我见过一个项目开发者保存文件时 ESLint 把单引号改成双引号紧接着 Prettier 又改回单引号光标还没反应过来编辑器右下角就开始报错。这就是典型的两套工具规则冲突。ESLint 自己的quotes规则和 Prettier 的singleQuote配置都在管引号两个工具互相修改对方的输出形成死循环。根治方案我在前面提过安装eslint-config-prettier并放在 ESLint 配置最末尾。这个包会把 ESLint 所有和 Prettier 重叠的样式规则一次性关闭。团队在引入这套体系的时候最好在一开始就达成一条默契格式问题永远以 Prettier 为准ESLint 只负责真正的质量隐患。不要试图在 ESLint 里精细控制格式那样只会让配置越来越复杂、冲突越来越多。4.4 存量项目的兼容难题几千个历史错误怎么办存量老项目第一次接 ESLint 的时候跑一次eslint .出来几千个报错是常有的事。很多团队在这里就放弃了觉得历史债太重。我的方案是分四步走不追求一次性清零。第一步先忽略短期内处理不了的目录。在eslint.config.js的ignores里加上历史遗留的大型目录先把新增代码的检查跑起来。第二步把一些常见争议规则设为warn而不是error。比如no-console、typescript-eslint/no-explicit-any这些规则直接设成 error 会让存量代码完全无法提交。设成 warn 之后质量问题能被看见但不会阻塞提交流程。第三步依靠 lint-staged 的增量保护。因为 lint-staged 只检查本次修改的文件老文件不在检查范围内所以团队可以从第一天开始严格约束“新改的代码必须干净”而不需要去处理历史文件。第四步在 CI持续集成上逐步收紧。等增量代码的报错量降下来之后把--max-warnings 0加进 CI 的 lint 命令让新增的 warning 直接导致构建失败。这一步意味着团队正式从“提醒”模式切换到“强制”模式。寿命长期的项目的演进节奏通常是这样第一周大部分成员会对报错感到烦躁第二周开始习惯第三周发现提交前自动修复的东西越来越多一个月后几乎没人再手动关注格式问题。历史代码也不用特意去清理随着迭代自然会被新的干净代码替换掉。5. 从个人工具到团队规范让自动化成为协作契约5.1 规范的第一身份是配置文件文档只是解释很多团队会把大量精力花在撰写华丽的规范文档上但真正的规范主体应该是仓库里这些可执行、可验证的配置文件。文档的作用只是给配置文件做解释告诉成员每一条规则在解决什么问题。我建议把规则变更纳入和代码变更一样的评审流程。提出者提交一个 PR把rules加一条并附上理由其他人评审后合并。这样每一条规范都有据可查有变更历史而不是某个人在会议上口头拍板定下的。规则变成了团队的集体决策而不是少数人的命令。5.2 CI 上的最后一道防线本地钩子可以被禁用这是个基础事实。有的人会因为钩子影响效率而执行git commit --no-verify绕过检查这拦不住也没必要硬拦。所以 CI 上的 lint job 是绝对不能省的最后一道防线。下面是 GitHub Actions 的最小配置放在.github/workflows/lint.ymlname: lint on: pull_request: types: [opened, synchronize] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: 20 - run: npm ci - run: npm run lint这套流程保证即使有人在本地--no-verify代码合入主干之前依然会被强制检查一次。本地钩子和 CI 的关系可以理解为钩子负责优化体验CI 负责兜底安全。5.3 团队文化上的建议回到标题里那个“零摩擦”。技术配置只是表真正决定这套体系能不能长期运转的是团队对规则的态度。有几条建议是从我踩过的坑里得来的第一不要把工具检查结果当 KPI。有的管理者会用“ESLint 报错数”考核成员这个做法会让团队走向极端——有人为了过检查在代码里乱加注释有人用 disable 注释掩盖问题。工具的定位是帮助不是刑具。第二允许规则有例外但例外必须走项目级 override。遇到某个确实需要any类型的地方团队应该讨论后在配置文件里加一条针对性例外而不是某个成员在代码里写个// eslint-disable-line悄悄绕过。例外有记录、有理由规范才有生命力。第三新人引导靠工具链不靠口口相传。新成员入职之后看到的应该是“装好依赖、系统自动检查”的顺畅体验而不是老成员转发的规范文档链接。让工具去教育比让人去教育稳定得多。最后分享一个我的个人体会。搭建这套体系真正难的不是技术而是让所有人接受“自己的习惯不一定是团队的规范”这个事实。我第一次把 Prettier 引入团队时反对声最大的反而是平时写代码最随性的那位而最支持的人居然是对格式最挑剔的那位。等到跑了一个月后连当初反对的人都开始说能自动搞定的都不算事。如果你团队正处在“为风格吵架”的阶段别想着靠一份规范文档和培训来解决问题。直接把工具链配好然后用一周时间让所有人感受“提交时自动修正”的顺畅。机器能管的永远别让人来扛。
返回列表