关于前端:前端-GitHooks-工程化实践

53次阅读

共计 9152 个字符,预计需要花费 23 分钟才能阅读完成。

前言

前段时间,部门的前端我的项目迁徙到 monorepo 架构,笔者在其中负责跟 git 工作流相干的事件,其中就包含 git hooks 相干的工程化的实际。用到了一些罕用的相干工具如 husky、lint-staged、commitizen、commit-lint 等,以此文记录一下整个的实际过程和踩过的坑。

留神:下文中的例子以及命令都是基于 Mac OS,如果你是 windows 用户,也不必放心,文中也会论述大抵原理和运行逻辑,对应的 windows 命令能够推理得悉。

Git Hooks

Git Hooks 是什么

大多数同学应该都对 git hooks 相当理解,然而笔者还是想在这里具体解释一下。
首先是 hook,这其实是计算机领域中一个很常见的概念,hook 翻译过去的意思是钩子或者勾住,而在计算机领域中则要分为两种解释:

  1. 拦挡音讯,在音讯达到指标前,提前对音讯进行解决
  2. 对特定的事件进行监听,当某个事件或动作被触发时也会同时触发对应的 hook
    也就是说 hook 自身也是一段程序,只是它会在特定的机会被触发。

了解了 hook 这一概念,那么 git hooks 也就不难理解了。git hooks 就是在运行某些 git 命令时,被触发的对应的程序。

在前端畛域,钩子的概念也并不少见,比方 Vue 申明周期钩子、React Hooks、webpack 钩子等,说到底它们都是在特定的机会触发的办法或者函数

常见的 Git Hooks 有哪些

git hooks 分为两类

客户端 hook

  • pre-commit hook, 在运行 git commit 命令时且在 commit 实现前被触发
  • commit-msg hook, 在编辑完 commit-msg 时被触发,并且承受一个参数,这个参数是寄存以后 commit-msg 的临时文件的门路
  • pre-push hook, 在运行 git push 命令时且在 push 命令实现前被触发

服务端 hook

  • pre-receive 在服务端承受到推送时且在推送过程实现前被触发
  • post-receive 在服务端接管到推送且推送实现后被触发

这里只列举了一部分,更多的 git hooks 详细信息见官网文档

在本地 git 仓库中的 .git/hooks 文件夹中也能够看到罕用的 git hooks 示例

从图中能够看到,默认的 git hooks 都是 shell 脚本,只须要将 git hooks 的示例文件的 .sample 扩展名去掉,那么示例文件即可失效。
一般来说,在前端工程中利用 git hooks 都是运行 javaScript 脚本,就像这样

#!/bin/sh
node your/path/to/script/xxx.js

或者是这样

#!/usr/bin/env node
// javascript code ...

原生的 Git Hooks 的缺点

原生的 git hooks 有一个比拟大的问题是 .git 文件夹下的内容不会被 Git 追踪。这就示意,无奈保障让一个仓库中所有的成员都应用同样的 git hooks,除非仓库的所有成员都手动同步同一份 git hooks,但这显然不是个好方法。

Husky

Husky 的应用

  1. 装置 husky
pnpm install husky --save-dev
  1. husky 初始化
npx husky install
  1. 设置 package.json 的 prepare。来保障 husky 能够失常运行
npm set-script prepare "husky install"
  1. 增加 git hooks
npx husky add .husky/${hook_name} ${command}

husky install 命令做了什么

事实上,husky install 命令是解决 git hooks 问题的要害

  • 第一步:husky install 会在我的项目根目录下创立 .husky 以及 .husky/_ 文件夹(文件夹也能够自定义),而后在 .husky/_ 文件夹下创立 husky.sh 脚本文件。这个文件的作用就是保障通过 husky 创立的脚本可能失常运行,它的理论利用的中央前面会讲到。更多对于这个脚本的探讨能够看这里 github issue。
  • 第二步:husky install 会运行 git config core.hooksPath ${path/to/hooks_dir},这个命令用来指定 git hooks 的门路,此时察看我的项目下 .git/config 文件,[core] 上面会多出一条配置:hooksPath = xxx。当 git hooks 被某些命令触发时,Git 会运行 core.hooksPath 指定的文件夹下的 git hook。

更多对于 husky 的配置、命令相干文档,看这这里

值得注意的是 core.hooksPath 是 Git v2.9 推出的新个性,而 Husky 也是在 v6 版本开始应用 core.hooksPath 这个个性。在这之前的版本,Husky 会间接笼罩 .git/hooks 文件夹下所有的 hook,来使通过 Husky 配置的 hooks 失效。另外,在配置了 core.hooksPath 后 Git 会疏忽 .git/hooks 文件夹下的 git hooks

husky add 命令做了什么

当运行如下命令

npx husky add .husky/pre-commit npx eslint

.husky 目录下会新增一个 pre-commit 文件,文件内容为

#!/usr/bin/env sh
. "$(dirname --"$0")/_/husky.sh"

npx eslint

此时曾经胜利增加了一个 pre-commit git hook,这个脚本会在运行 git commit 命令时执行。
在脚本的第二行,援用了下面所说的 .husky.sh 文件,也就是说通过 husky 创立的 git hook 在被触发时,都会执行这个脚本。

梳理一下,husky 是如何解决原生的 git hooks 的问题的,首先后面曾经提到了原生 git hooks 次要的问题是 git 无奈跟踪 .git/hooks 下的文件,然而这个问题曾经被 git core.hooksPath 解决了,那么新的问题就是,开发者依然须要手动设置 git core.hooksPath。husky 在 install 命令中帮忙咱们设置了 git core.hooksPath,而后在 package.json 的 scripts 中增加 "prepare": "husky install",这样每次装置依赖的时候就会执行 husky install,因而就能够保障设置的 git hooks 能够被触发了。

罕用的 git 相干工具库

lint-staged

在 pre-commit hook 中,一般来说都是对以后要 commit 的文件进行校验、格式化等,因而在脚本中咱们须要晓得以后在 Git 暂存区的文件有哪些,而 Git 自身也没有向 pre-commit 脚本传递相干参数,lint-staged 这个包为咱们解决了这个问题,lint-staged 的文档中第一句这样说道:

Run linters against staged git files and don’t let 💩 slip into your code base!

lint-staged 的应用

  1. 装置 lint-staged

    pnpm install lint-staged --save-dev
  2. 配置 lint-staged
    个别状况下,倡议 lint-staged 搭配着 Husky 一起应用,当然这不是必须的,只须要保障 lint-staged 会在 pre-commit hook 中被运行就能够了。在搭配 Husky 应用的状况下,能够运行上面的命令,在 pre-commit hook 中运行 lint-staged

    npx husky add .husky/pre-commit "npx lint-staged"

对于 lint-staged 的配置,在模式上与常见的工具包的配置形式大同小异,能够通过在 package.json 中增加一个 lint-staged 项、也能够在根目录增加一个 .lintstagedrc.json 文件等,上面以在 package.json 中配置为例:
配置项中的 key 为 glob 模式匹配语句,值为要运行的命令(能够配置多个),例如想要为暂存区中 src 文件夹下所有的 .ts 和 .tsx 文件运行 eslint 查看以及 ts 类型查看,那么配置如下:
具体的配置文档看这这里
如果 git hooks 脚本运行失败(过程完结时返回的状态码不为 0),那么会终止后续操作。比方上例中  eslint 查看报错,那么会间接终止 commit,git commit 命令失败。

lint-staged 是如何晓得以后暂存区有哪些文件的

事实上,lint-staged 外部也没有什么黑魔法,它在外部运行了 git diff --staged --diff-filter=ACMR --name-only -z 命令,这个命令会返回暂存区的文件信息,相似如下所示的代码:

const {execSync} = require('child_process');
const lines = execSync('git diff --staged --diff-filter=ACMR --name-only -z')
    .toString()

const stagedFiles = lines
    .replace(/\u0000$/, '')
    .split('\u0000')

commitizen

在应用 Git 过程中,不可避免的须要填写 commit message,这其实是一件相当令人头疼的事件。如果没有良好的 commit message 标准,那么在查看历史 commit 的时候只会一脸懵 *。
commitizen 能够帮助开发者填写 commit 信息

commitizen 的应用

  1. 装置 commitizen

    pnpm install commitizen -D
  2. 初始化 commitizen

    npx commitizen init cz-conventional-changelog --save-dev --save-exact --pnpm

commitizen init 做了什么

  1. 装置 cz-conventional-changelog 适配器 npm 模块
  2. 将其保留到 package.json 的 devDependencies 中
  3. config.commitizen 配置增加到 package.json 中 如下所示:

    "config": {
      "commitizen": {"path": "./node_modules/cz-conventional-changelog"}
    }

commitizen 自身只提供命令行交互框架以及一些 git 命令的执行,理论的规定则须要通过适配器来定义,commitizen 留有对应的适配器接口。而   cz-conventional-changelog 就是一个 commitizen 适配器。

此时运行 npx cz 命令 就会呈现以下命令行交互页面:

这个适配器生成的 commit message 模板如下

<type>(<scope>): <subject>
< 空行 >
<body>
< 空行 >
<footer>

这也是最常见的提交约定,当然也能够装置其余适配器,或者自定义适配器来定制本人想要的 commit message 模板。
当运行 npx cz,commitizen 在通过适配器模板以及用户的输出拿到最终的 commit message 后,会在外部运行 git commit -m "XXX" 命令,到此为止,就实现了一次 git commit 操作

更多对于 commitizen 的具体等信息能够看 github 和 cz-git

自定义 commitizen 适配器

如果你想自定义适配器,那么能够抉择应用 cz-customizable 这个工具包。
在没有这个工具包的状况下,如果想要自定义一个 commitizen 适配器,那么你还须要把握 inquirer 的 API,commitizen 只会为适配器传递一个 inquirer 对象,适配器的规定须要通过这个 inquirer 对象来创立规定,这是在不太易用,而 cz-customizable 能够让我咱们只专一于规定而不必去思考 inquirer 的 API。

cz-customizable 的应用

  1. commitizen 配置

    "config": {
      "commitizen": {"path": "./node_modules/cz-customizable"}
    }
  2. cz-customizable 配置,在根目录新增一个 .cz-config.js 文件,配置示例如下

    module.exports = {
      types: [{ value: 'feat', name: 'feat: A new feature'},
     {value: 'fix', name: 'fix: A bug fix'},
      ],
      scopes: [{name: 'accounts'}, {name: 'admin'}],
      allowTicketNumber: false,
      messages: {
     type: "Select the type of change that you're committing:",
     scope: '\nDenote the SCOPE of this change (optional):',
     customScope: 'Denote the SCOPE of this change:',
      },
      subjectLimit: 100,
    };

这里是对于 cz-customizable 更具体的 示例 和 配置

应用 git cz 命令运行 commitizen

在全局 PATH 配置正确的状况下,也能够间接应用 git cz 命令去运行 commitizen。如果你在我的项目中装置了 commitizen, 那么在你的我的项目下的 node_modules/.bin 目录下将会看到两个脚本: czgit-cz , 如下图所示:

这两个脚本的内容是截然不同的,官网的文档中会举荐在 package.json 的 scripts 中增加如下内容:

commit: "cz"

这样就能够应用 npm run commit 来运行 commitizen 了。然而如果想要应用 git cz 命令运行 commitizen,那么则要求 git-cz 文件所在的目录在全局的 PATH 下,运行以下命令来查看 PATH

echo $PATH

PATH 以冒号分隔,检查一下所有的 PATH 中是否有一条能匹配到你的 cz 脚本,一般来说都是有的,如果没有,那么你能够在你的 ~/.zshrc 或者 ~/.bash_profile 中加上一条:

PATH=$PATH:./node_modules/.bin

而后从新加载一下配置文件,运行 source ~/.zshrc 或者 source ~/.bash_profile,这样在你我的项目根目录下 就能够间接应用 git cz 命令了。
如果你是用 npm 全局装置的 commitizen,那么你大概率不须要放心 PATH 的问题,因为 npm 的依赖装置门路下的 bin 文件夹会被 node 或者 NVM 主动退出到 PATH 中。

回到刚刚所说的 node_modules/.bin 文件夹 下的 git-cz 脚本,实际上它是 git cz 命令能够运行的要害。不晓得你是否纳闷,为什么应用 Git 能够去运行一个 npm 库,实际上,这是 git 自定义命令。想要增加一个 git 自定义命令有如下几个要求:

  1. 是一个可执行文件
  2. 文件名必须是 git-XXX
  3. 这个文件所在门路必须在你的 PATH 下

所以前文中,提到想要运行 git cz 命令,须要全局 PATH 配置正确。

你也能够根据上述要求尝试增加别的自定义 git 命令。须要留神的是,要检查一下你增加的 shell 脚本是否具备可执行权限,若没有可执行权限会导致如下报错 _git: 'your command' is not a git command_, 此时能够运行 _chmod a+x <path to your file>_来批改文件的权限使其可运行即可。

commitlint

commitlint 这个工具库,能够通过配置一些规定来校验 commit message 是否标准。
那么咱们曾经有了 commitizen 为什么还须要 commitlint 呢?上文中说到,commitizen 的作用是帮助开发者填写 commit message,尽管能够通过抉择不同的适配器或者自定义适配器来制订对应的 commit 信息标准以及模板,然而短少了对 commit message 的校验性能,开发者依然可能在无心中应用原生的 git commit 命令来提交,而 commitlint 在 commit-msg 这个 git hook 中对 commit message 进行校验,正好解决了这个问题。

commitlint 的应用

  1. 装置
pnpm install --save-dev @commitlint/config-conventional @commitlint/cli
  1. 应用 husky 增加 commit-msg hook
npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1
  1. commitlint 配置
    在我的项目根目录减少一个 commitlint.config.js 文件, 文件内容如下:
module.exports = {extends: ['@commitlint/config-conventional'],
    // 自定义局部规定
    rules: {'scope-case': [0, 'always', 'camel-case'],
        'scope-empty': [2, 'never'],
        'scope-enum': [2, 'always', [...]],
    },
};

commitlint 与 commitizen 一样,分为两局部,一部分是执行的主程序,另一部分是规定或者说是适配器。@commitlint/cli 是执行的主程序,@commitlint/config-conventional 则是规定。commitlint 和 commitizen 别离采纳了策略模式和适配器模式,因而都领有十分高的可用性和良好的扩展性。
在 commitlint 的配置文件中,能够先援用一个 commitlint 规定包,而后在定义局部本人想要的规定,就像 eslint 的配置一样。
须要留神的是,在将 commitlint 增加到 commit-msg hooks 中时,执行 commitlint 的 shell 命令中 --edit $1  参数是必须的,这个参数的意思是:存储 commit message 的临时文件门路是 $1, 而$1 则是 Git 传给 commit-msg hook 的参数,它的值是 commit message 的长期存储文件的门路,默认状况下是 .git/COMMIT_EDITMSG。如果不传这个参数,那么 commitlint 将无奈得悉以后的 commit message 是什么。

更多 commitlint 的相干详情看这里

commitlint 与 commitizen 的配置共用

前文中说到 commitlint 解决了 commitizen 没有对 commit message 做校验的问题,然而应用了 commitlint 后,新的问题呈现了,如果 commitlint 的规定集与 commitizen 的适配器中的规定不统一,那么可能会导致应用 commitizen 生成的 commit message 被 commitlint 校验时不通过从而 git commit 失败。
解决这个问题的方法有两种:

  1. 将 commitizen 的适配器规定翻译为 commitlint 规定集,已有的对应工具包为 commitlint-config-cz,这个包须要你所应用的 commitizen 适配器为 cz-customizable,也就是自定义适配器。
  2. 将 commitlint 规定集转化为 commitizen 的适配器,已有对应的工具包为 @commitlint/cz-commitlint

这里以第二种选用 @commitlint/cz-commitlint 为例:

  1. 装置 @commitlint/cz-commitlint

    pnpm install --save-dev @commitlint/cz-commitlint
  2. 批改 packages.json 中 commitizen 的配置

      "config": {
     "commitizen": {"path": "./node_modules/@commitlint/cz-commitlint"}
      }

conventional-changelog 生态

关上 commitlint 的 github 仓库,就会发现它在 conventional-changelog 这个 Organization 下,而 commitizen/cz-cli 这个仓库的 README.md 文件中也提到了 conventional-changelog 生态:

For this example, we’ll be setting up our repo to use AngularJS’s commit message convention, also known as conventional-changelog.

这也难怪为什么 commitlint 还专门提供了一个 @commitlint/cz-commitlint 包来配合 commitizen。

那么 conventional-changelog 生态还蕴含什么呢?

反对 Conventional Changelog  的插件

  • grunt
  • gulp
  • atom
  • vscode
  • emacs

Conventional Changelog 生态中的重要模块

  • conventional-changelog-cli – the full-featured command line interface –_功能丰富的命令行接口_
  • standard-changelog – command line interface for the angular commit format. –_angular 格调的命令行接口_
  • conventional-github-releaser – Make a new GitHub release from git metadata –_通过 git 元数据生成一个新的 GitHub release_
  • conventional-recommended-bump – Get a recommended version bump based on conventional –commits 依据 conventional 格调的提交生成举荐的版本变更
  • conventional-commits-detector – Detect what commit message convention your repository is using —_对存储库应用的 commit 音讯约定进行查看_
  • commitizen – Simple commit conventions for internet citizens.
  • commitlint – Lint commit messages

因为本文次要是讲 git hooks,这里对于 conventional-changelog 生态就不开展讲了,有趣味的话能够自行去看一下他们的 github 仓库 和 这篇文章

正文完
 0