对于开发人员而言,拥有一个良好的反馈回路至关重要。规范的项目具有CI/CD管道,可通过运行必要的检查(例如静态代码分析和测试)来确保代码不会破坏应用程序逻辑或代码库本身中的任何内容。

但问题是,只有当代码在存储库中之后,才可能看到CI/CD产生的错误,可能是在打开请求之后。看到CI/CD的管道后,开发人员必须在本地修复代码,然后再次将代码推送到存储库中,这导致会比实际需要更多的时间。

在CI/CD管道上执行的许多检查都可以在开发人员的计算机上本地运行。但是,没有一个理智的人会期望开发人员每次将要提交某些东西时都执行一组命令。

相反,该过程应该是自动化的,以免破坏开发人员的工作流程,并确保每个开发人员在其计算机上运行相同的检查。

如果我们有某种机制可以在提交时通知我们,则可以轻松实现此过程的自动化。值得庆幸的是,该机制已经存在,称为git hooks。

什么是git hooks?

Git钩子是预先配置的自定义脚本,在git中执行动作之前先执行。默认情况下,所有已安装的钩子在git/hooks目录中均可用, 每个文件名均为钩子名称。

有许多钩子,可用于配置真正的高级设置。但是,在我们的情况下,我们仅对post-mergepre-rebasepre-commit钩子感兴趣。所有可用的钩子都可以在这里找到。

向项目添加git hook

可以在GitHub上找到本教程的演示存储库。

Husky

通过在目录中添加正确命名的文件,可以将钩子添加到项目中。但是,我们可以使用名为Husky的库来自动执行该过程.git/hooks,而不是手动执行它们。

Husky将确保每次安装项目依赖项时,都将根据配置正确配置钩子。这样,开发人员不必自己的计算机配置钩子。

为了Husky,请运行以下命令:

npm install --save-dev husky

然后将以下配置添加到:package.json

{
  // ...
  "husky": {
    "hooks": {
      "pre-commit": "<command>",
    }
  }
}

有了该配置,每次进行提交时,Husky就会执行提供的<command>

lint-staged

我们还将使用一个名为lint-staged的库,该库使我们可以在已暂存的文件上执行命令。因此,如果我们有一个包含数百或数千个文件的大项目,但是只更改了一个小文件,那么对每个文件运行检查将是多余且耗时的。相反,将仅检查更改的文件。

npm install --save-dev lint-staged

并修改package.json为如下所示:

{
  // ...
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.ts": "<command>"
  }
}

现在我们已将Husky配置为在pre-commit钩子上运行lint-staged命令。

Lint阶段配置支持将glob模式用作键,因此,作为示例,我们提供了glob模式以使用"*.ts"glob模式匹配所有TypeScript文件。

现在,在执行提交之前,Husky将执行lint-staged命令,该命令将依次在所有Typescript文件上执行指定的命令<command>。一旦获得正确的结果,它将使提交通过。否则它将失败,并将错误消息记录到控制台。

lint-staged如何工作的?

假设我们有以下配置:

{
  // ...
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.ts": "eslint"
  }
}

有人可能想知道,当配置仅是eslint命令本身时,lint-staged如何确保ESLint仅在指定文件上运行。与许多其他CLI工具一样,ESLint以下格式调用:

eslint [options] [file|dir|glob]*

假定提供的命令在结尾处期望使用空格分隔的绝对文件路径的列表。因此,它将采用暂存文件的所有绝对路径,并通过在路径末尾附加路径来执行命令。

因此,如果我们更改两个文件main.tsapp.ts 。lint-staged将执行以下脚本:

eslint project/main.ts project/app.ts

假设两个文件都在根目录中,并且我们的项目名称为project

这种方式,ESLint有类似命令行格式的工具,不需要任何附加配置就能会lint-staged配合工作。

将最流行的工具与lint-staged集成

安装必要的工具并了解它们的工作原理后,让我们添加三个最受欢迎的工具,并查看它们如何与lint-staged集成。

执行linter

当涉及到从代码样式指南不一致到安全性问题时,lint是最有用的工具。最好在每次提交之前运行它以检查最后一次是否一切正常。ESLint是JavaScript/Node.js项目中最流行的linter,因此让我们看一下如何将其与lint-staged集成。

由于有许多方法可以根据所使用的技术和语言将ESLint添加到项目中,因此我们将不着重于如何安装ESLint本身。如果您想学习如何自行设置,请参阅本文

在上面的示例中使用了ESLint,因此希望可以清楚地将其添加到配置中。

"lint-staged": {
    "*.ts": [
      "eslint --fix",
    ]
  }

与上面的示例唯一不同的是,我们添加了参数--fix,以允许ESLint自动修复在检查文件时遇到的任何规则验证。如果无法修复,则该命令将中止。

注意,全局模式现在可以接受命令数组。这样,我们以后可以添加更多命令。这些命令是按顺序执行的,因此,最好先提供失败几率最高的命令。

执行代码格式化

在代码格式化中保持一致性的重要性不可夸大。这非常重要,因此将其配置为pre-commit钩子是个好主意。

如果要在项目中设置Prettier,请参阅本文。配置了Prettier之后,让我们将其添加到挂钩中。

 "lint-staged": {
    "*.ts": [
      "eslint --fix",
      "prettier --write"
    ]
  }

Prettier的命令与ESLint的行为非常相似。它接受要执行的文件列表。通过提供参数--write,我们可以确保Prettier将覆盖暂存文件中发现的所有不一致之处。

运行测试

单元测试非常适合在每次提交之前运行。它们速度很快,不需要特定的设置。集成端到端测试应在的CI/CD管道上运行,因为它们需要事先设置特定的环境,并且通常需要很长时间运行。

我们可以使用许多库来编写单元测试。在这里,我们使用Jest。这是有关如何配置Jest的文章

为了使Lest-staged集成Jest命令,我们必须提供一些参数:

 "lint-staged": {
    "*.ts": [
      "npm run lint -- --cache",
      "jest --bail --passWithNoTests --findRelatedTests",
      "prettier --write"
    ]
  }

首先,我们设置参数--bail,这会使Jest在发现错误后立即退出。

然后我们提供参数--passWithNoTests,因为某些提交实际上可能不包含与单元测试有关的更改。Jest希望至少运行一个测试,否则会引发错误。

最后一个参数--findRelatedTests,是最重要的一个。它接受将由lint-staged提供的以空格分隔的文件列表。因此,如果我们更改main.ts文件,所有依赖该文件中的代码的测试都将执行。

请注意,该参数必须是最后一个参数--findRelatedTests,因为lint-staged将在命令末尾提供暂存文件的路径。

还要注意,执行单元测试实际上是序列中的第二个执行命令,因为当我们不确定代码是否通过测试,在测试不通过时不必运行Prettier。

验证提交消息

提交消息是提交所包含的更改的描述。它总是让我们写在一个统一的格式的消息,原因有很多,更多解释在这里

有一个叫做commitlint的工具可以为我们完成所有繁重的工作。我们要做的就是将其集成到我们现有的配置中。

为了安装程序包,请运行:

npm install --save-dev @commitlint/config-conventional @commitlint/cli

在安装后,创建一个配置文件,其名称如下:commitlint.config.js

module.exports = {
  extends: ['@commitlint/config-conventional']
};

这次,我们将使用commit-msg钩子。我们必须在文件中编辑Husky配置,如下所示:package.json

{
  // ...  
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  },
}

团队可以使用许多规则来选择其提交消息的格式。有了此配置,每次我们提交内容时,提交的消息都会得到验证。

测试设置

设置完所有内容后,我们可以提交更改以查看所有内容是否按预期工作。

测试设置

由于每次检查都成功,因此提交已通过,现在可以将其推送到远程存储库。

跳过检查

如果出于某种原因您需要跳过检查,那么可以使用选项--no-verify来执行此操作。一个例子:

git commit -m "Quick fix" --no-verify

结论

通过设置git hook,我们可以确保推送到存储库的代码符合预期的标准。 当然,有一种方法可以跳过所有本地运行的检查,因此git hooks不是最终的工具。 它不是CI/CD管道的替代品,而是一种在提交代码之前接收有关代码反馈的方法,从而大大减少了解决发现的问题所需的时间。