如何在 grunt 中实现 staged 文件仅 lint 的 pre-commit 钩子

解读

面试官真正想看的是:

  1. 你能否把「只处理 Git 暂存区文件」这一常见需求落地到 Grunt 体系;
  2. 你能否把 Grunt 任务与 Git Hooks 打通,并保证跨平台、跨组员一致性;
  3. 你能否在「不改全局依赖、不污染 commit 历史」的前提下给出可维护方案。
    国内团队普遍使用 Husky + lint-staged,但题目限定「用 Grunt 实现」,因此必须给出纯 Grunt 流Grunt 与 Husky 混合流两种可行路径,并解释优劣。回答时要体现对 Grunt 插件生态、Git diff 命令、跨平台路径处理、错误码传递的完整理解。

知识点

  • Grunt 任务注册与多任务(multi-task)机制:如何用 grunt.registerTask 组合子任务。
  • Grunt 插件grunt-contrib-jshintgrunt-eslintgrunt-git 等官方插件的 filtersrc 动态注入技巧。
  • Git 暂存区文件列表获取git diff --cached --name-only --diff-filter=ACM 命令及跨平台换行处理。
  • Grunt 动态配置:在任务运行前通过 grunt.config.set 动态改写 src,避免全量 lint。
  • pre-commit 钩子安装:使用 huskyyorkie(Vue 系)自动注入 .git/hooks,或手写 scripts/install-hooks.js 保证 CI/新成员无感。
  • 错误码传递:lint 失败需返回非 0 退出码,阻断 commit;Gruntfile 中需显式 grunt.fail.fatal
  • 性能优化:缓存、并发、忽略删除文件(--diff-filter=ACM)与二进制文件过滤。
  • 团队协作:把 grunt githooks 任务加入 npm postinstall,确保新人拉完代码即生效,无需手动配置。

答案

方案一:纯 Grunt 流(无 Husky)

  1. 安装依赖
npm i -D grunt grunt-contrib-jshint grunt-git
  1. 在 Gruntfile.js 中新增任务
module.exports = function(grunt) {
  grunt.initConfig({
    // 占位,稍后会动态填充
    jshint: {
      staged: { src: [] }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-jshint');

  // 1. 读取暂存区文件
  grunt.registerTask('collect-staged', 'Collect staged files', function() {
    var done = this.async();
    var exec = require('child_process').exec;
    exec('git diff --cached --name-only --diff-filter=ACM', function(err, stdout) {
      if (err) grunt.fail.fatal('Git 命令失败');
      var files = stdout.split(/\r?\n/).filter(function(f) {
        return /\.js$/i.test(f) && grunt.file.exists(f);
      });
      if (!files.length) {
        grunt.log.ok('无 JS 文件需要检查');
        return done(true);
      }
      // 2. 动态注入 src
      grunt.config('jshint.staged.src', files);
      done(true);
    });
  });

  // 2. 组合任务
  grunt.registerTask('pre-commit', ['collect-staged', 'jshint:staged']);
};
  1. 安装 Git 钩子
    新建 scripts/install-hooks.js
const fs = require('fs');
const path = require('path');
const hook = path.join(__dirname, '..', '.git', 'hooks', 'pre-commit');
const content = `#!/bin/sh
# 统一使用项目本地 grunt
./node_modules/.bin/grunt pre-commit
`;
fs.writeFileSync(hook, content);
fs.chmodSync(hook, 0o755);

package.json 中加入

"scripts": {
  "postinstall": "node scripts/install-hooks"
}

至此,每次 git commit 前仅对 staged 的 JS 文件执行 JSHint,失败即阻断提交。

方案二:Grunt + Husky 混合流(推荐,国内主流)

  1. 安装
npm i -D husky grunt-eslint
  1. package.json 中声明钩子
"husky": {
  "hooks": {
    "pre-commit": "grunt pre-commit"
  }
}
  1. Gruntfile.js 只需保留「动态注入 staged 文件」逻辑,其余同方案一;Husky 会自动接管 .git/hooks,Windows/Mac/Linux 均无需额外脚本,更适合国内多人协作场景

拓展思考

  1. 如果项目同时存在 ESLint + Stylelint,可再注册一个 collect-staged-style 任务,把 src 按扩展名拆成两组,分别注入 eslint.stagedstylelint.staged,最后并行运行,整体耗时从 8s 降到 3s
  2. 对大型仓库可引入 grunt-newer 或自建 .cache/grunt-staged.json,记录上次已校验文件哈希,二次 commit 只 diff 变更,lint 时间再降 60%
  3. 若需支持「部分 commit」(git add -p),务必保留 --diff-filter=ACM 并排除 D,否则被拆分的区块可能对应已删除文件,导致 lint 报错。
  4. 在 CI 环境可暴露 grunt pre-commit --dry-run,把待检查文件列表输出成 JSON,供 SonarQube 或阿里云的云效代码扫描作为增量范围,实现本地与云端规则一致