前端代码规范自动化:ESLint+Prettier+Husky+lint-staged完整工作流

发布时间:2026/10/6 9:14:36
前端代码规范自动化:ESLint+Prettier+Husky+lint-staged完整工作流 前端项目里最容易被忽略但又最值得花时间投入的就是代码规范与自动化检查这一环。很多人觉得“代码能跑就行”等团队大了、接手的人多了才发现每个人的缩进风格、引号习惯、分号取舍都不一样review 的时候满屏都是格式化改动真正要看的逻辑反而被淹没。这篇文章我就围绕我一直在用的这套组合——ESLint、Prettier、Husky、lint-staged——把从前端代码规范到 Git 提交前自动检查的完整工作流讲清楚包含具体配置、踩过的坑和推荐的最佳实践想要建立或优化前端工程化流水线的朋友可以参考。1. 整体设计思路与工具分工先说为什么要用这一套组合而不是单纯装一个 ESLint 完事。前端工程化走到今天代码检查和代码格式化实际上是两件事ESLint 管的是代码质量和潜在错误比如未使用变量、隐式类型转换、循环依赖等它关注的是“代码写得对不对”Prettier 管的是代码风格和排版比如缩进、单双引号、行宽、分号它关注的是“代码长得整不整齐”。两个工具职责不同但配合使用效果最好。Husky 的作用是接管 Git Hooks。Git 本身提供了 pre-commit、commit-msg 等钩子但这个机制在团队协作中存在一个很大的痛点钩子脚本默认存放在.git/hooks目录下这个目录不会被提交到 Git 仓库意味着每个人 clone 项目后都要手动去设置钩子完全不可控。Husky 通过修改 Git 配置中的 core.hooksPath把钩子统一到项目根目录下的.husky文件夹而这个文件夹是跟随仓库一起提交的团队成员 clone 下来之后直接生效彻底解决了钩子管理分散的问题。lint-staged 解决的是另一个性能问题。如果直接在 pre-commit 钩子里跑npm run lint那么它会对整个项目所有文件进行检查。项目初期还好等到项目文件数量上千甚至上万之后全量 lint 的时间会从几秒膨胀到几十秒非常影响开发体验。lint-staged 的核心机制是先通过git diff --name-only拿到本次暂存区中变更的文件列表然后只对这些文件执行配置好的检查命令。这就把检查范围精确限制在“这次要提交的东西”上速度快、干扰小。四个工具的协作流程简单说就是开发者在编辑器里写代码保存时由 Prettier通过 VS Code 插件自动格式化提交代码时git add把文件加入暂存区git commit触发 Husky 注册的 pre-commit 钩子钩子里执行 lint-stagedlint-staged 再对暂存区内的文件依次执行 ESLint 检查与自动修复如果有问题就阻断提交并报错修复后才能继续。这里有一个关键点值得展开讲ESLint 和 Prettier 之间的规则冲突。ESLint 里也有一部分与代码风格相关的规则比如quotes引号风格、semi分号、indent缩进这些规则和 Prettier 的格式化规则会存在重叠甚至矛盾。如果两者同时生效会出现“Prettier 格式化完ESLint 又报错”的死循环。主流解法是安装eslint-config-prettier它的作用就是把 ESLint 中所有与格式化重叠的规则全部关闭把格式化这件事完全交给 PrettierESLint 只专注代码质量本身。我前后在多个项目里踩过这个坑最初不懂两者重叠时会互相打架后来统一采用这个方案后再也没出现过格式化冲突。2. 基础依赖安装与初始化配置这套工作流的第一步是安装依赖。这里必须区分两个包的定位差异ESLint 负责代码质量需要和项目的 JavaScript/TypeScript 解析器、插件配套使用Prettier 是一个独立的格式化程序不但格式化 JavaScript还能格式化 JSON、CSS、Markdown、YAML 等文件类型。我一般在项目里把它们作为 devDependencies 安装因为它们在构建和发布阶段都不需要。npm install -D eslint prettier husky lint-staged如果你用的是 TypeScript还需要安装typescript-eslint这个包它提供了基于 TypeScript AST 的解析器和一套推荐规则集。如果是 React 项目还要额外安装eslint-plugin-react、eslint-plugin-react-hooks等插件。Vue 项目则需要eslint-plugin-vue并且要配合vue-eslint-parser。这里建议不要一次性把所有插件装齐而是按项目实际技术栈按需安装依赖越精简维护成本越低。初始化 ESLint 配置我推荐走官方交互式命令因为它会根据你选择的框架、模块类型、是否使用 TypeScript 等条件生成一份基础配置比自己手写要准确得多。在 ESLint 9 及以上版本中默认配置方式已经从.eslintrc迁移到eslint.config.js也就是所谓的 flat config 方案。初始化命令如下npm init eslint/config交互过程会询问项目使用的是 ES Modules 还是 CommonJS、用什么框架、是否用 TypeScript、代码运行环境是浏览器还是 Node回答完就会生成eslint.config.js。以最常见的 React TypeScript 项目为例生成的配置大致长这样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] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: [**/*.{ts,tsx}], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { react-hooks: reactHooks, react-refresh: reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, react-refresh/only-export-components: [ warn, { allowConstantExport: true }, ], }, } );这里我补充一个容易被忽略的细节tseslint.config()这个函数其实是个组合器它能接收多个配置对象或配置数组再把它们合并成一份最终的 flat config 数组。上面的写法中{ ignores: [dist] }是第一个配置对象用来声明不需要检查的目录第二个对象则是主体配置。注意files字段限定了这份规则只对.ts和.tsx文件生效JavaScript 文件不会套用 React 相关规则这种细分方式在 flat config 里非常灵活。Prettier 的配置相对简单它本身默认规则已经比较合理大多数项目只需要少量自定义。可以通过在项目根目录创建prettier.config.js或直接在package.json里增加prettier字段来配置。我更推荐独立配置文件因为后续如果需要为不同文件类型定制规则比如 JSON、Markdown独立文件的表达力更强。一份常用的配置如下export default { semi: true, singleQuote: true, printWidth: 100, tabWidth: 2, trailingComma: es5, endOfLine: lf, };这里每个配置项背后都有实际考量。singleQuote设为 true 在 JavaScript 社区接受度最广字符串里出现双引号也不需要转义。printWidth默认值是 80这对现代大屏显示器来说偏窄每行代码很容易折行我习惯设为 100既保持行宽紧凑又减少换行频率。trailingComma设为es5意味着在对象和数组等 ES5 合法的位置保留尾逗号函数参数尾部不加逗号这是兼容性比较好的折中方案。endOfLine设为lf是为了统一不同操作系统上的换行符避免在 Windows 上 checkout 代码后出现整文件 diff 的问题。依赖安装和基础配置完成之后建议先在终端里手动验证一遍两个工具是否正常工作。ESLint 跑一遍npx eslint src/或者指定文件看是否有报错Prettier 跑npx prettier --check src/检查格式化状态或者用npx prettier --write src/直接重写文件。确保底层工具独立可用再接入 Git Hooks否则多个环节叠加在一起出问题时很难定位是哪一层的故障。3. Husky 初始化与 Git Hooks 注册Husky 从 v9 开始初始化方式发生了明显变化。早期版本是在package.json里配置husky.hooks再通过husky install生成.husky目录。v9 之后更简洁直接执行npx husky init这个命令会做三件事在项目根目录创建.husky文件夹、生成一个默认的pre-commit钩子文件、在package.json中写入prepare脚本。prepare脚本的用途是确保任何开发者 clone 项目并运行npm install后Husky 自动被安装并启用细节我不多展开但这是团队协作中极其关键的一环务必确认你的package.json里已经包含它{ scripts: { prepare: husky } }初始生成的.husky/pre-commit文件内容比较简单一般只有一句npm test。我们需要把它改成调用 lint-stagednpx lint-staged关于 pre-commit 钩子文件的执行权限有几个容易踩坑的点值得单独说。在 macOS 和 Linux 环境下.husky/pre-commit文件必须拥有可执行权限否则钩子不会运行。husky init命令会默认设置好权限但如果通过 Windows 环境 clone 代码后文件权限可能会丢失这时可以在项目根目录执行chmod x .husky/pre-commit修复。另外钩子文件最顶上必须保留#!/usr/bin/env sh这一行 shebang它是钩子脚本能运行的先决条件很多奇怪的不生效问题都源于此。Husky 支持多种钩子除了 pre-commit 之外commit-msg也是团队协作中常用的一类。配合 commitlint 可以对提交信息做规范化检查强制要求提交信息符合 Conventional Commits 规范例如feat: xxx、fix: xxx。如果你要启用在.husky目录下新建commit-msg文件内容为npx --no -- commitlint --edit $1这里用npx --no是为了避免 npx 在没有安装 commitlint 时自动下载远程包保证用的是项目本地安装的版本。提交信息规范化之后有两个直观好处一是 Git 历史清晰可读将来做版本回退或查看变更原因时非常舒服二是可以无缝对接后续的自动生成 CHANGELOG、语义化版本控制等流程。我还遇到过一个问题在不同项目中切换时Husky 的钩子可能被上一个项目的钩子覆盖。这其实不是 Husky 的 bug而是因为公司的前端脚手架或同事的初始化脚本重新执行了git config core.hooksPath把钩子指向了错误的目录。遇到这种情况用git config --get core.hooksPath查看当前值如果发现指向不明执行git config core.hooksPath .husky修复即可。4. lint-staged 配置与提交流程实测lint-staged 的核心价值前面已经提过——只对暂存区里即将提交的文件执行检查。这意味着就算项目里有几千个历史遗留的问题文件只要你不改动它们提交时完全不会受影响。这是它相比“pre-commit 里跑全量 lint”最大的优势也是保证检查速度、减少挫败感的关键。在package.json中配置 lint-staged 最常见用一段 JSON 映射即可{ lint-staged: { *.{js,jsx,ts,tsx}: [ eslint --fix, prettier --write ], *.{json,css,md}: [ prettier --write ] } }这段配置的含义很直白对所有 JS/TS 相关文件先跑 ESLint 自动修复再跑 Prettier 重新格式化对 JSON、CSS、Markdown 等文件只跑 Prettier。ESLint 放前面是因为它的自动修复会做结构性调整比如删除未使用的 import、调整函数参数等改动之后 Prettier 再统一排版两者的处理顺序符合逻辑。执行顺序的细节值得注意。eslint --fix修改了文件之后文件内容会与暂存区中的原始版本不同。此时如果只进行到这一步就提交提交内容会是修复后的版本吗答案是会的因为 lint-staged 内部会在命令执行结束之后把所有被命令修改过的文件重新git add到暂存区。这个机制相当贴心但前提是命令执行成功退出码为 0。如果 ESLint 检查出无法自动修复的错误命令会以非零退出码结束lint-staged 会中止整个流程并打印错误信息此时需要开发者手动修复后再重新提交。我再补充一个处理 lint 错误时的实际建议当eslint --fix执行完发现仍有 error 级别的报错时会输出错误明细通常是一个文件一个错误列表。此时不要试图绕过钩子强行提交--no-verify虽然能跳过但不建议因为钩子的目的就是守住底线。正确的做法是根据报错信息逐个修复绝大多数规则错误修复成本都很低比如补上缺失的依赖、删除未使用的变量、加上必要的类型标注等。修复后重新git add再git commit即可。下面用一个实际的提交流程把整条链路串联起来。假设我在一个 React 项目里修改了src/components/Button.tsx和src/utils/format.ts两个文件然后依次执行git add src/components/Button.tsx src/utils/format.ts git commit -m feat: 优化按钮组件样式与文案格式化工具执行git commit的瞬间Husky 的 pre-commit 钩子被触发.husky/pre-commit文件中的npx lint-staged开始运行。lint-staged 检查暂存区发现两个待提交文件按配置先对它们执行eslint --fix。如果Button.tsx里有一个未使用的 importeslint --fix会自动把它删除如果format.ts中有一处类型错误无法自动修复ESLint 会退出并报错提交被中断控制台会展示具体的文件和规则。此时我手动修复这个问题重新执行git add和git commit第二次提交顺利通过随后prettier --write会把格式化后的最终内容写入暂存区并完成提交。这个流程实测下来整个 lint 检查加上格式化通常在一两秒内完成对提交体验几乎没有感知损耗。而早期我用全量 lint 的时候在这个体量的项目上每次提交前都要等十几秒虽然能忍但确实磨人。相比之下lint-staged 通过精确限定范围换来的速度提升是在团队里推广这套工具时最有说服力的卖点。5. 编辑器集成与 VS Code 保存时格式化命令行工作流解决了提交时的检查问题但开发体验的改善还得靠编辑器配合。VS Code 里通过插件市场安装 ESLint 和 Prettier 两个扩展扩展 ID 分别是dbaeumer.vscode-eslint和esbenp.prettier-vscode然后在项目根目录创建.vscode/settings.json写入以下配置{ editor.defaultFormatter: esbenp.prettier-vscode, editor.formatOnSave: true, editor.codeActionsOnSave: { source.fixAll.eslint: explicit } }这里有三点要解释清楚。第一editor.defaultFormatter设为 Prettier 后保存文件时所有支持的文件类型JS、TS、CSS、JSON 等都会按 Prettier 规则格式化。第二editor.formatOnSave设为 true 触发保存时格式化如果项目里有些文件不想被格式化比如第三方生成的类型声明文件可以在该目录下创建.prettierignore文件把路径加进去。第三source.fixAll.eslint会在保存时执行 ESLint 的自动修复处理那些 Prettier 管不了的代码质量问题——比如自动排序 import、移除未使用变量等。这个配置本质上是把前面提到的 lint-staged 检查流程提前到了“保存文件”这个时点早发现问题早解决。一个经常出现的困扰是保存文件后代码没有按 Prettier 格式化也没看到任何提示。大多数情况下都是因为没有选定默认格式化器。此时可以在 VS Code 的编辑器右下角看到一个“选择默认格式化程序”的入口点击后选择 Prettier或者用命令面板CtrlShiftP输入 “Format Document With” 手动指定。还有另一种情况是项目根目录下的.prettierrc文件没有被 VS Code 识别到这通常是因为配置文件名为.prettierrc且没有扩展名的识别问题建议直接命名为.prettierrc.json或prettier.config.js这两种格式 VS Code 的 Prettier 插件识别最稳定。关于编辑器集成还有一个团队协作层面的建议将.vscode/settings.json和.vscode/extensions.json都提交到 Git 仓库。extensions.json可以声明项目推荐的扩展列表当团队新成员首次打开项目时VS Code 会自动提示安装缺失的扩展同时统一了所有人的编辑器行为。这是防止“在我电脑上没问题但你的格式化结果和我完全不一样”这类问题最有效的手段。前者同步的是配置后者同步的是工具链两者缺一不可。下面是.vscode/extensions.json的推荐内容{ recommendations: [ esbenp.prettier-vscode, dbaeumer.vscode-eslint ] }6. 常见问题与排查技巧实录这套工作流整体不难搭但我在多个项目里落地时还是遇到了不少看似奇怪、实则原因很常见的问题。下面整理成速查表并补充详细的排查思路。问题表现可能原因解决方案git commit 时没有触发任何钩子Husky 未安装或 core.hooksPath 指向不对检查.husky/pre-commit是否存在且可执行执行git config --get core.hooksPath确认指向lint-staged 不执行任何命令package.json 中 lint-staged 配置缺失确认lint-staged字段已配置且暂存区中存在匹配规则的文件ESLint 和 Prettier 规则冲突导致循环报错缺少eslint-config-prettier关闭重叠规则安装并添加到extends末尾或 flat config 中最后引入Prettier 对 JSX/TS 文件不生效默认格式化器没设为 Prettier在.vscode/settings.json里设置editor.defaultFormatterWindows 环境下 hook 不执行文件权限或 shebang 问题确认.husky/pre-commit第一行有 shebanggit bash 环境下执行 chmod 修复权限提交时eslint --fix提示无法修的错误代码存在需要手动处理的质量问题查看具体规则和文件位置手动修复后重新 add 和 commit这里挑两个高频问题展开说一下排查路径。第一个是“Husky 钩子不生效”。如果项目刚 clone 下来首先确认npm install是否完整执行因为 Husky 的启用依赖prepare脚本该脚本默认由npm install触发。如果你用了npm ci --ignore-scripts或某些包管理器如 pnpm默认禁用脚本那么 Husky 就不会被安装。此时只需要手动执行npx husky或者重新完整安装依赖即可。如果钩子目录存在但依然不触发大概率是 core.hooksPath 被其他工具改写了查看git config --get core.hooksPath的值正常应等于项目的.husky目录。第二个高频问题是 ESLint 和 Prettier 的规则打架。最典型的场景是设置了 Prettier 使用单引号、不带分号但 ESLint 有一个quotes规则默认要求双引号。保存文件时 Prettier 把双引号改为单引号紧接着 ESLint 报错说需要双引号两者进入拉锯战。解决方式就是安装eslint-config-prettier在 ESLint 配置中把它作为最后一个扩展引入关闭所有与格式化相关的冲突规则。以 flat config 为例可以这样引入import prettier from eslint-config-prettier/flat; export default tseslint.config( // 其他配置... prettier, );这里的核心原则是格式化的归 Prettier质量的归 ESLint互不越权。如果你发现在某个规则上两者仍然冲突优先检查是不是没有把eslint-config-prettier放到最末尾因为它的作用就是覆盖override之前可能开启的样式类规则。还有一个值得记录的教训是 lint-staged 在 Windows 环境下的兼容性。早期版本在 Windows 下执行 shell 命令时可能会因为 shell 解析方式不同而导致通配符失效表现为“暂存区里的 .js 文件没有被处理”。新版 lint-staged 已经内置了跨平台支持基本不会出现这个问题但如果你还在用旧版本建议升级到最新版。另外在 Windows 的 PowerShell 环境下执行git commitHusky 默认通过sh运行钩子所以需要确保系统里有可用的shGit for Windows 自带的 git bash 已经包含这通常不是问题。7. 团队协作与进阶扩展这套自动化工作流搭建到能跑大概一两个小时但真正让它发挥长期价值的是团队层面的一致性和持续推进。我的经验是引入这套工具后至少要同步做三件事统一的编辑器配置模板、规范文档沉淀、以及一个“问题快速通道”。编辑器配置模板上一节已经提到了.vscode目录提交到仓库并且配合.editorconfig文件把缩进、编码、换行这些最基础的底层约定固定下来。.editorconfig的好处是可以被绝大多数编辑器和 IDE 原生识别即便团队有人不用 VS Code也能保证最基础的缩进风格一致。一个标准的.editorconfig长这样root true [*] charset utf-8 indent_style space indent_size 2 end_of_line lf insert_final_newline true trim_trailing_whitespace true再进一步如果团队规模达到 5 人以上建议在 CI/CD 流水线里再加一道不可绕过的检查。本地 hook 可以被开发者用--no-verify跳过虽然我们不建议但总有人会这么做CI 阶段的检查相当于最后的兜底。例如利用 GitHub Actions在 push 和 PR 时统一执行name: Lint Check on: push: pull_request: 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这里仍然推荐npm ci而不是npm install因为npm ci严格按package-lock.json安装依赖能保证 CI 环境与本地依赖一致避免出现“本地没问题、CI 报错”的随机性问题。如果 lint 脚本本身是全量检查这个阶段慢一点也可以接受毕竟它主要发生在后台。另一个值得展开的扩展方向是与提交信息规范的深度整合。利用 commitlint 配合 Husky 的 commit-msg 钩子从代码质量检查延伸到提交信息规范性。提交信息一旦标准化便可以通过工具自动生成 CHANGELOG、联动语义化版本、甚至实现自动化的代码评审路由。这些在我的实践里都是顺理成章的下一步前期的钩子基础设施搭好之后加一个 commitlint 的成本非常低但长期收益非常可观。根据我个人多次从一个空项目搭建到多团队推广这套工作流的体会最核心的心得是工具链的价值不在于“有没有配置”而在于“团队是否真的在依赖它”。如果只是一个人在自己电脑上装了 ESLint、加了 Husky其他人完全不知道不理睬那这套方案就是一纸空文。真正有效的落地方式是把配置、编辑器设置、安装说明写进 README并在项目初始阶段强制使用一两个迭代让团队成员先感受到“被自动发现问题”的价值之后就算你想拆掉大家也会拦着你。这套组合拳真正让人舒服的地方是它把“靠人自觉”的代码规范变成了“由系统守门”的流程约束少了很多无谓的争论也让 review 回归到逻辑、设计和业务本身。