嚴肅的研發團隊都會使用 Git 之類的版本管理系統來管理代碼,隨著 GitHub 的廣受歡迎,相信大家對 Git 并不陌生。Git 在代碼版本管理之外,也提供了類似 npm script 里 `pre`、`post` 的鉤子機制,叫做 [Git Hooks](https://git-scm.com/book/gr/v2/Customizing-Git-Git-Hooks),鉤子機制能讓我們在代碼 commit、push 之前(后)做自己想做的事情。
Git Hooks 能給我們的開發工作流帶來哪些可能呢?我帶的團隊中,大部分項目通過 npm script 為本地倉庫配置了 pre-commit、pre-push 鉤子檢查,且正計劃為遠程倉庫([Remotes](https://git-scm.com/book/en/v1/Git-Basics-Working-with-Remotes))配置 pre-receive 鉤子檢查。兩種鉤子的檢查目的各不相同,本地檢查是為了盡早給提交代碼的同學反饋,哪些地方不符合規范,哪些地方需要注意;而遠程檢查是為了確保遠程倉庫收到的代碼是符合團隊約定的規范的,因為如果沒有遠程檢查環節,熟悉 Git 的同學使用 `--no-verify`(簡寫為 `-n`) 參數跳過本地檢查時,本地檢查就形同虛設。
可能有同學會嘀咕,在 IDE 里面配置各種檢查難道還不夠么?對個人開發者來說足夠了,但對于團隊,如果對代碼里面的壞味道聽之任之,久而久之整個團隊的代碼質量標準都會被拉低,到最后坑的還是團隊的每個成員,不是么?之前沒想到這層的同學建議去看看破窗理論。
那么增加 Git Hooks 的必要性聊清楚了,我們應該在 Git Hooks 里面做哪些事情呢?通常來說:檢查編碼規范,把低級錯誤趁早挖出來修好;運行測試,用自動化的方法做功能回歸,測試本身就包含很多話題,且按下不表。
前端社區里有多種結合 npm script 和 git-hooks 的方案,比如 [pre-commit](https://github.com/observing/pre-commit)、[husky](https://github.com/typicode/husky),相比較而言 husky 更好用,它支持更多的 Git Hooks 種類,再結合 [lint-staged](https://github.com/okonet/lint-staged) 試用就更溜。
接下來我們逐步給示例項目配置本地的 Git Hooks,而在鉤子中運行的是已有的 npm script,比如 lint、test:
### 1\. 安裝項目依賴
使用如下命令安裝 husky、lint-staged 到項目依賴中:
```
npm i husky lint-staged -D
# npm install husky lint-staged --save-dev
# yarn add husky lint-staged -D
```
husky 的基本工作原理可以稍作解釋下,翻看 husky 的 [package.json](https://github.com/typicode/husky/blob/master/package.json),注意其中的 scripts 聲明:
```
"scripts": {
"test": "jest",
"format": "prettier --single-quote --no-semi --write **/*.js",
"install": "node ./bin/install.js",
"uninstall": "node ./bin/uninstall.js"
},
```
這里面的 install 就是你在項目中安裝 husky 時執行的腳本(所有的魔法都藏在在這里了,哈哈)。
然后再檢查我們倉庫的 `.git/hooks` 目錄,會發現里面的鉤子都被 husky 替換掉了,注意下圖中三個紅色框中的內容:

### 2\. 添加 npm script
接下來需要在 scripts 對象中增加 husky 能識別的 Git Hooks 腳本:
```
"scripts": {
+ "precommit": "npm run lint",
+ "prepush": "npm run test",
"lint": "npm-run-all --parallel lint:*",
"lint:js": "eslint *.js",
```
這兩個命令的作用是在代碼提交前運行所有的代碼檢查 npm run lint;在代碼 push 到遠程之前,運行 lint 和自動化測試(**言外之意,如果測試失敗,push 就不會成功**),雖然運行的是 npm run test,但是 lint 也配置在了 pretest 里面。
然后嘗試提交代碼:`git commit -am 'add husky hooks'`,能看到 pre-commit 鉤子已經生效:

### 3\. 用 lint-staged 改進 pre-commit
如上的配置乍看起來沒有任何問題,但是在大型項目、遺留項目中接入過 lint 工作流的同學可能深有體會,每次提交代碼會檢查所有的代碼,可能比較慢就不說了,接入初期 lint 工具可能會報告幾百上千個錯誤,這時候估計大多數人內心是崩潰的,尤其是當你是新規范的推進者,遇到的阻力會增大好幾倍,畢竟大多數人不愿意背別人的鍋,壞笑。
好在,我們有 lint-staged 來環節這個問題,每個團隊成員提交的時候,只檢查當次改動的文件,具體改動如下:
```
"scripts": {
- "precommit": "npm run lint",
+ "precommit": "lint-staged",
"prepush": "npm run test",
"lint": "npm-run-all --parallel lint:*",
},
+ "lint-staged": {
+ "*.js": "eslint",
+ "*.less": "stylelint",
+ "*.css": "stylelint",
+ "*.json": "jsonlint --quiet",
+ "*.md": "markdownlint --config .markdownlint.json"
+ },
"keywords": [],
```
接下來我們故意在 index.js 中引入錯誤:
```
- return NaN;
+ return NaN
```
然后嘗試提交這個文件:`git commit -m 'try to add eslint error' index.js`,結果如下圖:

上圖中帶有 `Running Tasks` 字樣的列表就是 lint-staged 根據當前要提交的文件和 package.json 中配置的檢查命令去執行的動態輸出。紅色框里面提示 husky 的 pre-commit 鉤子執行失敗,提交也就沒有成功。
關于 lint-staged 還有些高級的用法,比如對單個文件執行多條命令,對單個文件動態自動修復,自動格式化等等,留待大家自己去探索好了。
撤銷掉有錯誤的修改,提交之后,我們往遠程 push 新分支,結果如下圖:

> 讀過我其他文章的同學可能已經想到,本小節的內容部分和我早期的文章[《用 husky 和 lint-staged 構建超溜的代碼檢查工作流》](https://juejin.im/post/592615580ce463006bf19aa0)有部分內容是重疊的。
* * *
> 本節用到的代碼見 [GitHub](https://github.com/wangshijun/automated-workflow-with-npm-script/tree/11-run-npm-script-in-git-hooks),想邊看邊動手練習的同學可以拉下來自己改(**記得安裝 npm 依賴之后再運行腳本**),注意切換到正確的分支 `11-run-npm-script-in-git-hooks`。
* * *
- 為什么選擇 npm script
- 入門篇 01:創建并運行 npm script 命令
- 入門篇 02:運行多個 npm script 的各種姿勢
- 入門篇 03:給 npm script 傳遞參數和添加注釋
- 進階篇 01:使用 npm script 的鉤子
- 進階篇 02:在 npm script 中使用環境變量
- 進階篇 03:實現 npm script 命令自動補全
- 高階篇 01:實現 npm script 跨平臺兼容
- 高階篇 02:把龐大的 npm script 拆到單獨文件中
- 高階篇 03:用 node.js 腳本替代復雜的 npm script
- 實戰篇 01:監聽文件變化并自動運行 npm script
- 實戰篇 02:結合 live-reload 實現自動刷新
- 實戰篇 03:在 git hooks 中運行 npm script
- 實戰篇 04:用 npm script 實現構建流水線
- 實戰篇 05:用 npm script 實現服務自動化運維