如何在 grunt 中实现 staged 文件仅 lint 的 pre-commit 钩子
解读
面试官真正想看的是:
- 你能否把「只处理 Git 暂存区文件」这一常见需求落地到 Grunt 体系;
- 你能否把 Grunt 任务与 Git Hooks 打通,并保证跨平台、跨组员一致性;
- 你能否在「不改全局依赖、不污染 commit 历史」的前提下给出可维护方案。
国内团队普遍使用 Husky + lint-staged,但题目限定「用 Grunt 实现」,因此必须给出纯 Grunt 流或Grunt 与 Husky 混合流两种可行路径,并解释优劣。回答时要体现对 Grunt 插件生态、Git diff 命令、跨平台路径处理、错误码传递的完整理解。
知识点
- Grunt 任务注册与多任务(multi-task)机制:如何用
grunt.registerTask组合子任务。 - Grunt 插件:
grunt-contrib-jshint、grunt-eslint、grunt-git等官方插件的filter与src动态注入技巧。 - Git 暂存区文件列表获取:
git diff --cached --name-only --diff-filter=ACM命令及跨平台换行处理。 - Grunt 动态配置:在任务运行前通过
grunt.config.set动态改写src,避免全量 lint。 - pre-commit 钩子安装:使用
husky或yorkie(Vue 系)自动注入.git/hooks,或手写scripts/install-hooks.js保证 CI/新成员无感。 - 错误码传递:lint 失败需返回非 0 退出码,阻断 commit;Gruntfile 中需显式
grunt.fail.fatal。 - 性能优化:缓存、并发、忽略删除文件(
--diff-filter=ACM)与二进制文件过滤。 - 团队协作:把
grunt githooks任务加入npm postinstall,确保新人拉完代码即生效,无需手动配置。
答案
方案一:纯 Grunt 流(无 Husky)
- 安装依赖
npm i -D grunt grunt-contrib-jshint grunt-git
- 在 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']);
};
- 安装 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 混合流(推荐,国内主流)
- 安装
npm i -D husky grunt-eslint
- 在
package.json中声明钩子
"husky": {
"hooks": {
"pre-commit": "grunt pre-commit"
}
}
- Gruntfile.js 只需保留「动态注入 staged 文件」逻辑,其余同方案一;Husky 会自动接管
.git/hooks,Windows/Mac/Linux 均无需额外脚本,更适合国内多人协作场景。
拓展思考
- 如果项目同时存在 ESLint + Stylelint,可再注册一个
collect-staged-style任务,把src按扩展名拆成两组,分别注入eslint.staged与stylelint.staged,最后并行运行,整体耗时从 8s 降到 3s。 - 对大型仓库可引入
grunt-newer或自建.cache/grunt-staged.json,记录上次已校验文件哈希,二次 commit 只 diff 变更,lint 时间再降 60%。 - 若需支持「部分 commit」(
git add -p),务必保留--diff-filter=ACM并排除D,否则被拆分的区块可能对应已删除文件,导致 lint 报错。 - 在 CI 环境可暴露
grunt pre-commit --dry-run,把待检查文件列表输出成 JSON,供 SonarQube 或阿里云的云效代码扫描作为增量范围,实现本地与云端规则一致。