Husky原理解析及在代碼Lint中的應用

背景

最近在研究代碼 Lint 相關的內容,業界比較經常使用的方案是Husky配合lint-staged在代碼提交前進行Lint,防止將不規範的代碼提交到遠端。前端

Husky的工做原理很感興趣,花了點時間研究,藉此文作一下總結,但願對正在學習這塊內容的朋友有一些幫助。node

Lint最佳實踐

說「最佳實踐」可能有點不恰當,但我見過的大多數前端項目都是採用這種組合git

以Javascript爲例,要進行代碼Lint,主要有如下步驟:github

  1. 安裝相應的包,包括:eslinthuskylint-staged,如何安裝不是本文重點,請自行學習
  2. 添加相應配置:
    1. 增長.eslintrc.js文件,配置eslint,具體配置根據項目和團隊需求自行配置,可參見eslint文檔
    2. package.json或者.huskyrc文件中增長 husky 配置項
    "husky": {
      "hooks": {
        "pre-commit": "lint-staged"
      }
    },
    複製代碼
    1. 在package.json中增長 lint-staged 配置項
    "lint-staged": {
      "**/*.js": "eslint"
    },
    複製代碼

到此,git commit時就會進行代碼校驗,而且只會校驗staged的文件。shell

ESlintlint-staged不是本文的重點,請自行學習,本文重點關注Husky的原理npm

Husky的原理

git hooks介紹

Husky是如何在代碼提交時觸發代碼校驗的?在研究它的原理以前,須要介紹另一個概念:git hooks,官方文檔的描述是:json

和其它版本控制系統同樣,Git 能在特定的重要動做發生時觸發自定義腳本。 有兩組這樣的鉤子:客戶端的和服務器端的。 客戶端鉤子由諸如提交和合並這樣的操做所調用,而服務器端鉤子做用於諸如接收被推送的提交這樣的聯網操做。 你能夠爲所欲爲地運用這些鉤子。bash

目前git支持17個hooks,都以單獨的腳本形式存儲在.git/hooks文件夾下:服務器

以一次commit爲例,會前後觸發pre-commitprepare-commit-msgcommit-msgpost-commit等hooks。咱們能夠利用這些hooks作一些有趣的事。好比:咱們能夠利用pre-commit進行代碼校驗,利用commit-msg進行commit message的校驗,只要你懂得shell語法,固然你也可使用Perl、Ruby或者Python。markdown

另外,並不須要咱們建立全部hook腳本,只須要按需建立便可。

Husky和git hooks的關係

Husky官方的描述是:

Git hooks made easy(讓git hooks變得簡單)

想象一個場景,好比在一個多人協做的團隊,你在.git/hooks中建立了一些hooks,你但願共享給隊友,但.git/hooks文件夾並不會提交到遠端,無奈只能拷貝。

Husky就是爲了解決這個問題而生的,只須要簡單的配置,就能夠完成hook的工做,具體配置方法參見Husky使用文檔,以package.json爲例:

// package.json
"husky": {
    "hooks": {
      "pre-commit": "eslint"
    }
  },
複製代碼

在npm安裝Husky時,Husky會在項目的.git/hooks文件夾下建立全部支持的hooks,另外還會建立 husky.local.shhusky.sh兩個文件。其實每一個hook腳本的內容都同樣:

# pre-commit
#!/bin/sh
# husky
 # Created by Husky v4.2.5 (https://github.com/typicode/husky#readme)
# At: 2020/8/3 上午11:25:21
# From: ...(https://github.com/typicode/husky#readme)

. "$(dirname "$0")/husky.sh"
複製代碼

咱們能夠看到僅僅是執行husky.sh腳本,重點在husky.sh腳本中。

Husky安裝hooks的原理

Husky是如何作到在安裝後建立hooks中的文件的? 實際上是用了npm scripts的install指令,當npm包在安裝完畢後會自動執行該指令下的腳本,具體可參加文檔。 經過查看Husky的package.json可知,指令爲:

node husky install

最終執行的是 ./lib/installer/bin中的腳本,而 hooks 的建立邏輯在 ./lib/installer/hooks.js中,有興趣的同窗能夠去看下源碼。

husky核心源碼解讀

Husky的核心代碼都在husky.sh文件中:

# Created by Husky v4.2.5 (https://github.com/typicode/husky#readme)
#   At: 2020/8/3 上午11:25:21
#   From: ... (https://github.com/typicode/husky#readme)

debug () {
  if [ "$HUSKY_DEBUG" = "true" ] || [ "$HUSKY_DEBUG" = "1" ]; then
    echo "husky:debug $1"
  fi
}

command_exists () {
  command -v "$1" >/dev/null 2>&1
}

run_command () {
  if command_exists "$1"; then
    "$@" husky-run $hookName "$gitParams"
    exitCode="$?"
    debug "$* husky-run exited with $exitCode exit code"

    if [ $exitCode -eq 127 ]; then
      echo "Can't find Husky, skipping $hookName hook"
      echo "You can reinstall it using 'npm install husky --save-dev' or delete this hook"
    else
      exit $exitCode
    fi

  else
    echo "Can't find $1 in PATH: $PATH"
    echo "Skipping $hookName hook"
    exit 0
  fi
}

hookIsDefined () {
  grep -qs $hookName \
    package.json \
    .huskyrc \
    .huskyrc.json \
    .huskyrc.yaml \
    .huskyrc.yml
}

huskyVersion="4.2.5"
gitParams="$*"
hookName="$(basename "$0")"

debug "husky v$huskyVersion - $hookName"

# Skip if HUSKY_SKIP_HOOKS is set
if [ "$HUSKY_SKIP_HOOKS" = "true" ] || [ "$HUSKY_SKIP_HOOKS" = "1" ]; then
  debug "HUSKY_SKIP_HOOKS is set to $HUSKY_SKIP_HOOKS, skipping hook"
  exit 0
fi

# Source user var and change directory
. "$(dirname "$0")/husky.local.sh"
debug "Current working directory is $(pwd)"

# Skip fast if hookName is not defined
# Don't skip if .huskyrc.js or .huskyrc.config.js are used as the heuristic could
# fail due to the dynamic aspect of JS. For example:
# `"pre-" + "commit"` or `require('./config/hooks')`)
if [ ! -f .huskyrc.js ] && [ ! -f husky.config.js ] && ! hookIsDefined; then
  debug "$hookName config not found, skipping hook"
  exit 0
fi

# Source user ~/.huskyrc
if [ -f ~/.huskyrc ]; then
  debug "source ~/.huskyrc"
  . ~/.huskyrc
fi

# Set HUSKY_GIT_STDIN from stdin
case $hookName in
  "pre-push"|"post-rewrite")
    export HUSKY_GIT_STDIN="$(cat)";;
esac

# Windows 10, Git Bash and Yarn 1 installer
if command_exists winpty && test -t 1; then
  exec < /dev/tty
fi

# Run husky-run with the package manager used to install Husky
case $packageManager in
  "npm") run_command npx --no-install;;
  "npminstall") run_command npx --no-install;;
  "pnpm") run_command pnpx --no-install;;
  "yarn") run_command yarn run --silent;;
  *) echo "Unknown package manager: $packageManager"; exit 0;;
esac
複製代碼

咱們提取其中幾個關鍵點來進行分析:

1、第一個關鍵點是經過 basename "$0" 獲取當前腳本的名稱,好比: pre-commit,這一點很重要,後面的指令匹配都是圍繞這個名稱,後面內容中的 hookName 都以 pre-commit 爲例

hookName="$(basename "$0")"
複製代碼

2、帶二個關鍵點是 hookIsDefined 函數,它的原理就是經過grep指令判斷各個配置文件中是否存在 pre-commit,

hookIsDefined () {
  grep -qs $hookName \
    package.json \
    .huskyrc \
    .huskyrc.json \
    .huskyrc.yaml \
    .huskyrc.yml
}
複製代碼

第三個關鍵點是 run_command 函數,做用就是用本地的husky-run指令執行hook,按照文章開頭的配置,就是執行eslint

npx --no-install husky-run pre-commit "$gitParams"
複製代碼

husky-run對應的執行腳本是.node_modules/husky/bin/run.js,腳本內容也很簡單,就是調用了.node_modules/husky/lib/runner/bin.js,而最終是調用了 .node_modules/husky/lib/runner/index.js中的runCommand接口,在接口中起了子進程執行pre-commit中對應的腳本。

Husky原理總結

到此Husky的原理介紹完畢,咱們進行一下總結:

  1. 安裝時建立hooks
  2. 提交時從配置文件中(package.json、.huskyrc、.huskyrc.json...)讀取相應的 hook 配置
  3. 執行配置中的指令/腳本
相關文章
相關標籤/搜索