WEBKT

Monorepo 提效指南:如何配置差异化 pre-commit 增量校验?

5 0 0 0

在 Monorepo(单仓多包)架构中,随着项目数量的增加,开发者往往会面临一个尴尬的问题:每次提交代码时,Git Hooks 触发的 lint 或测试脚本会对整个仓库进行扫描。即使你只改动了 packages/user-api 的一个小函数,系统却在疯狂校验 packages/admin-dashboard

这种“全量校验”不仅浪费时间,更挑战开发者的耐心。本文将分享如何通过 Husky + lint-staged 实现精准的差异化校验,确保“谁变动,校验谁”。

一、 核心架构思路

在一个标准的 Monorepo 中,.git 文件夹位于根目录。因此,所有的 Git Hooks(pre-commit 等)本质上都是在根目录上下文执行的。

要实现差异化校验,核心逻辑不是在每个子包里装一个 Husky,而是在根目录统一调度,通过**文件路径匹配(Glob Patterns)**将校验任务分发给对应的子包。

二、 基础环境搭建

首先,我们需要在根目录安装必要工具(以 pnpm 为例):

pnpm add -w husky lint-staged

初始化 Husky:

npx husky init

这会在根目录生成 .husky/pre-commit 文件。我们需要将其内容修改为:

# .husky/pre-commit
pnpm lint-staged

三、 实现差异化校验的两种策略

方案 A:根目录统一分发(推荐,维护成本低)

在根目录创建 .lintstagedrc.js。利用 lint-staged 的路径匹配能力,为不同目录指定不同的指令。

// .lintstagedrc.js
module.exports = {
  // 针对 apps/web 目录下的 TypeScript 文件
  'apps/web/**/*.{ts,tsx}': (filenames) => {
    return `pnpm --filter ./apps/web exec eslint ${filenames.join(' ')} --fix`;
  },

  // 针对 packages/ui 库的样式文件
  'packages/ui/**/*.scss': (filenames) => {
    return `pnpm --filter ./packages/ui exec stylelint ${filenames.join(' ')} --fix`;
  },

  // 全局性的 Prettier 格式化(可选)
  '**/*.{json,md}': [
    'prettier --write'
  ]
};

关键点:

  • 路径前缀:通过 apps/web/** 锁定了校验范围。
  • pnpm --filter:使用过滤器确保指令在正确的子包上下文中执行,且能利用子包局部的 eslint 配置。
  • 函数式配置:当需要处理大量文件路径时,使用函数形式返回字符串指令比数组更灵活。

方案 B:级联配置文件(适用于超大型仓库)

如果你的 Monorepo 有几十个项目,根目录配置会变得非常臃肿。此时可以将逻辑拆分。

  1. 根目录 .lintstagedrc.js

    module.exports = {
      'apps/app-a/**/*': 'pnpm --filter app-a lint-staged',
      'apps/app-b/**/*': 'pnpm --filter app-b lint-staged',
    };
    
  2. 子项目 apps/app-a/package.json

    {
      "lint-staged": {
        "*.ts": "eslint --fix"
      }
    }
    

这种方案实现了配置解耦,每个子项目只需要关心自己的 lint-staged 规则。

四、 避坑与进阶技巧

1. 解决路径溢出问题

在 Windows 等系统下,如果一次性提交的文件过多,filenames.join(' ') 生成的命令行字符串可能会超过系统限制。
对策:可以使用 lint-staged 的默认行为,或者在函数中分批处理文件名。

2. 与 Turborepo/Nx 结合

如果你的 Monorepo 使用了 Turborepo,你可以更进一步,不使用 lint-staged 逐个文件校验,而是执行:

# 只校验受改动影响的包
turbo lint --filter=[HEAD^1]

但注意,lint-staged 的优势在于它能只校验暂存区(Staged)的代码片段,而 Turborepo 的 Task 通常是针对整个包的。对于大型项目,推荐两者结合:lint-staged 做快速语法检查,turbo 做受影响范围的集成测试。

3. 处理配置文件依赖

确保根目录执行指令时能找到子包的 node_modules。使用 pnpm execpnpm --filter <pkg> exec 是最稳妥的方式,它会自动处理环境变量中的 PATH

五、 总结

在 Monorepo 中配置 pre-commit 并不是要在每个角落都塞入脚本,而是要收敛入口,分散执行

  • 小规模仓库:直接在根目录 .lintstagedrc.json 用 Glob 区分路径。
  • 中大规模仓库:根目录 lint-staged 负责分发,子包 lint-staged 负责具体执行。
  • 极致性能:结合 pnpm filter 或构建工具的缓存机制。

通过这种配置,你可以将提交代码时的等待时间从分钟级降低到秒级,极大地提升团队的开发体验。

码农架构师 MonorepoHusky前端工程化

评论点评