如何用husky来管理代码规范
前言
在项目中我们可以使用 prettier
、 eslint
来管理代码规范,开发环境下,可以手动来 lint
代码格式问题,但是如何保证远程仓库中的代码符合团队的规范呢?husky
能够在 git hooks commit
中帮助到你。
主要内容
通过本文你将学习到以下内容:
husky
是什么东西,我们为什么要使用husky
。husky
究竟怎么做git hooks
。husky
内部是怎么实现的。
husky基本介绍
-
Github:github.com/typicode/hu… 使用
Git
钩子变得简单 -
在做前端工程化时
husky
可以说是一个必不可少的工具。husky
可以让我们向项目中方便添加git hooks
。 -
这个库的名字指的是 “
哈士奇
”,结合库主要用来在提交前发现、规范代码的作用,应该是这个意思:‘不好好规范你的代码,你就像一个哈士奇一样,会用代码拆家的’ 。这个寓意跟另一个经常和husky
搭配使用的库 lint-staged 很像。
怎么理解这两个东西呢?
- 你可以把
husky
当做锄头挖坑,lint-staged
当做萝卜,一个萝卜一个坑!🔥
我们先来探讨一下没有 husky
的情况下,我们该怎么去做 git hooks
操作。
- 首先我们创建于一个
index
文件夹,在终端执行git init
命令,就会出现.git
文件夹,我们可以查看到hooks
。
- 新建
.pre-commit
文件,在pre-commit
钩子函数中写入:
npm run file // 执行 node脚本
// 异常退出
exitCode="$?"
exit $exitCode
- 执行
chmod 700 .pre-commit/*
, 给文件设置权限。 - 新建
file.js
文件,键入信息git hooks pre-commit start
。 git add .
git commit -m "feat: fist commit"
效果:
这样我们就可以在 git commit
之前去做想做的事情了,比如添加 eslint
来检查代码问题。当然 git hooks
还有很多,并不是本文的主题,我们只是探讨了一下在没有 husky
的时候,我们应该怎么做。再比如本地的 .git
文件不会提交到远程仓库,我们需要配置脚本来方便协同开发者。
"scripts": {
"file": "node file.js", // 这里你可以配置lint来检查代码
"empowerInstall": "git config core.hooksPath hooks && chmod 700 .pre-commit/*"
},
那么新起一个项目都要这么配置未免有些麻烦。那么 husky
能够帮助你做这些。
husky的基本使用
- 安装
husky
npm install husky --save-dev
- 使
git hooks
生效
npx husky install
在这里就会创建一个 .husky
的文件
- 在
package.json
里面创建执行脚本
npm pkg set scripts.prepare="husky install"
- 创建
git hooks
文件
npx husky add .husky/pre-commit "npm run file"
所以我们现在 commit
的时候也会走 git hooks
:
如果有文件没有权限的错误,那就需要执行: chmod 700 .husky/*
,进行授权。更多linux权限详解
husky配合lint-staged使用
- 安装
lint-staged
npm i lint-staged -S
- 配置
lint-staged
// package.json
"lint-staged": {
"*.{js,ts,vue,json}": [
"prettier --write"
]
}
- 执行
lint-staged
// .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
// 这个命令允许你从 npm 包(本地安装或远程获取)中运行任意命令。在类似于通过 `npm run` 运行它的环境中。
npm exec lint-staged
- 检查
6. 检查通过
注意:
lint-staged
只会对有修改记录的文件做检查!!!
所以经过 eslint、prettier、lint-staged
校验的代码,一定是非常符合规范的,而且他会帮你指出来具体的错误地方。
husky的源码解析
husky
的github
仓库- 把源码下载到本地来,分别执行:
npm i // 安装所需要的依赖
npm run prepare // 打包并生成lib文件,创建钩子
// package.json
"bin": "lib/bin.js",
入口文件为 lib/bin.js
,当然这里的是给用户用的文件,源码只有 src
下面的两个文件。
index.ts
import cp = require('child_process') // 子进程
import fs = require('fs') // fs模块
import p = require('path') // path模块
// 日志打印
const l = (msg: string): void => console.log(`husky - ${msg}`)
// 通过node执行shell脚本,拼接返回执行结果,比如 git commit
const git = (args: string[]): cp.SpawnSyncReturns<Buffer> =>
cp.spawnSync('git', args, { stdio: 'inherit' })
export function install(dir = '.husky'): void {
if (process.env.HUSKY === '0') {
l('HUSKY env variable is set to 0, skipping install')
return
}
// Ensure that we're inside a git repository
// If git command is not found, status is null and we should return.
// That's why status value needs to be checked explicitly.
if (git(['rev-parse']).status !== 0) {
l(`git command not found, skipping install`)
return
}
// Custom dir help
const url = 'https://typicode.github.io/husky/#/?id=custom-directory'
// 判断安装目录是否为项目根目录并创建 .husky 文件夹
if (!p.resolve(process.cwd(), dir).startsWith(process.cwd())) {
throw new Error(`.. not allowed (see ${url})`)
}
// 判断项目中是否存在 .git 文件。
// 如果不存在 则不存在 githook 文件。
if (!fs.existsSync('.git')) {
throw new Error(`.git can't be found (see ${url})`)
}
try {
// 在 .husky 文件下创建文件夹 '_'
fs.mkdirSync(p.join(dir, '_'), { recursive: true })
// 在 '_' 文件下写入文件 .gitignore, 文件内容为 ‘*’, 忽略该目录下所有文件的 git 提交
fs.writeFileSync(p.join(dir, '_/.gitignore'), '*')
// 复制 husky 项目根目录下的 husky.sh 文件 到 目标项目的 '_' 目录下,名称不变
fs.copyFileSync(p.join(__dirname, '../husky.sh'), p.join(dir, '_/husky.sh'))
// 执行 git 操作,修改 githook 的执行路径为目标项目的 .husky 文件下
const { error } = git(['config', 'core.hooksPath', dir])
if (error) {
throw error
}
} catch (e) {
l('Git hooks failed to install')
throw e
}
// 执行成功 or 失败后的 log 提示
l('Git hooks installed')
}
// 创建hooks文件,并写入内容
export function set(file: string, cmd: string): void {
const dir = p.dirname(file)
// 如果文件目录不存在,则执行husky install
if (!fs.existsSync(dir)) {
throw new Error(
`can't create hook, ${dir} directory doesn't exist (try running husky install)`,
)
}
// 写入文件, 指定解释器为 sh 执行 shell 脚本,
// cmd 动态参数,为开发者想要在这个 githook 阶段执行的操作,一般为脚本
// * 例:npm run lint
fs.writeFileSync(
file,
`#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
${cmd}
`,
{ mode: 0o0755 },
)
// 创建成功,打印log
l(`created ${file}`)
}
// add: 在已有的 githook 文件中追加命令
export function add(file: string, cmd: string): void {
if (fs.existsSync(file)) { // githook 文件是否存在
fs.appendFileSync(file, `${cmd}\n`) // 存在,在原有文件基础上追加新的内容
l(`updated ${file}`) // 打印成功日志
} else { // 不存在
set(file, cmd) // 执行 set 添加该 githook 文件
}
}
// 卸载hooksPath, 恢复到.git的默认路径
export function uninstall(): void {
git(['config', '--unset', 'core.hooksPath'])
}
上述代码主要就做了,创建 .husky
、 .ignore
、 husky.sh
以及 gitHooks
等文件,并实现了set
与 add
方法,操作 gitHooks
文件。
bin.ts
用于接受命令行参数,触发 src/index.ts
中的执行文件。
#!/usr/bin/env node
import p = require('path') // path模块
import h = require('./') // ./src/index.ts
// Show usage and exit with code
function help(code: number) {
console.log(`Usage:
husky install [dir] (default: .husky)
husky uninstall
husky set|add <file> [cmd]`)
process.exit(code)
}
// Get CLI arguments 获取命令行参数
const [, , cmd, ...args] = process.argv
const ln = args.length
const [x, y] = args
// 处理需要参数的主文件的函数并执行, 对错误的参数进行了长度判断
const hook = (fn: (a1: string, a2: string) => void) => (): void =>
// Show usage if no arguments are provided or more than 2
!ln || ln > 2 ? help(2) : fn(x, y)
// 执行命令相对应的 src/index.ts 文件中的函数,
// 没有参数的直接调用,需要参数的,套了一层 hook 函数,用于参数处理
const cmds: { [key: string]: () => void } = {
install: (): void => (ln > 1 ? help(2) : h.install(x)),
uninstall: h.uninstall,
set: hook(h.set),
add: hook(h.add),
['-v']: () =>
// eslint-disable-next-line
// @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-var-requires
console.log(require(p.join(__dirname, '../package.json')).version),
}
// Run CLI
try {
// Run command or show usage for unknown command
// 命令存在则运行指定函数,不存在则打印帮助 log
cmds[cmd] ? cmds[cmd]() : help(0)
} catch (e) {
console.error(e instanceof Error ? `husky - ${e.message}` : e)
process.exit(1)
}
husky.sh
凭借着百度,我大概读懂了这个 .sh
文件的内容。
#!/bin/sh
// 判断变量 husky_skip_init 的长度是否为 0
if [ -z "$husky_skip_init" ]; then
// 为 0 时, 创建 debug 函数, 用来打印报错日志
debug () {
// HUSKY_DEBUG 为 “1” 时打印
if [ "$HUSKY_DEBUG" = "1" ]; then
// $1 表示参数
echo "husky (debug) - $1"
fi
}
// 声明一个只读参数, 内容为 basename + 文件名称
readonly hook_name="$(basename "$0")"
debug "starting $hook_name..."
// 判断变量 HUSKY 是否 = “0”
if [ "$HUSKY" = "0" ]; then
debug "HUSKY env variable is set to 0, skipping hook"
exit 0
fi
// 判断 ~/.huskyrc 是否为普通文件
if [ -f ~/.huskyrc ]; then
debug "sourcing ~/.huskyrc"
. ~/.huskyrc
fi
// 声明只读变量
export readonly husky_skip_init=1
// 当前文件名 是否在 传进来的参数中 存在则执行
sh -e "$0" "$@"
exitCode="$?"
// 当 exitCode 不等于 0 时,打印当前执行的 hook 名称 以及 退出码
if [ $exitCode != 0 ]; then
echo "husky - $hook_name hook exited with code $exitCode (error)"
fi
exit $exitCode
fi
总结
本文主要讲解了,在没用 husky
的时候是怎么去配置 gitHooks
的,并且使用了 husky + lint-staged
来管理代码规范,在后面我们也一起去看了 husky
的具体实现,知道了如何去修改 gitHooks
的默认执行路径,总之 gitHooks
不仅仅只有 pre-commit
,还有其他的一些钩子也值得我们去探索。
转载自:https://juejin.cn/post/7205849733134352442