Husky 【前端工程化】&prettier、eslint、stylelint、lint-staged、husky

405 阅读11分钟

Husky 是一个 Git Hook 工具,可以触发 Git 提交的各个阶段:pre-commit 、commit-msg 、pre-push 、pre-rebase 等等。Husky支持所有 Git 钩子,可以根据项目的需要选择适当的钩子来运行自定义脚本

npm install --save-dev lint-staged husky
npx husky init

为了保证每个成员npm install 的时候会init Husky

需要在package.json 中添加 "prepare" 脚本

"scripts": {
  "prepare": "husky install"
}

当项目其他成员克隆我们的代码后运行 npm install 时,"prepare" 会被触发,husky init 会运行

在.husky 文件中 加入钩子脚本

pre-commit

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


# 获取当前分支
current_branch=$(git rev-parse --abbrev-ref HEAD)

echo -e "\033[33m -------------------  当前分支: $current_branch 正在对提交的代码执行commit操作 -------------------- \033[0m"

# 检查是否有待提交的文件
if [ -z "$(git diff --cached --name-only)" ]; then
  echo "暂存区域没有文件"
  exit 1
fi

npx --no-install lint-staged

# 添加格式化后的文件到暂存区
git add $(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|jsx|ts|tsx|json|css|scss|md)$')


# 运行 Jest 单元测试
echo "Running Jest tests..."
npx jest --passWithNoTests
if [ $? -ne 0 ]; then
  echo "一些测试失败了。请在提交之前修复它们。"
  exit 1
fi

echo -e "\033[33m -------------------  代码校验通过准备提交 ✅ -------------------- \033[0m"
exit 0



#!/bin/sh:指定脚本解释器,告诉操作系统用一个 shell 程序来执行该脚本; . "(dirname"(dirname "0")/_/husky.sh":确保了 Husky 的环境和变量在钩子脚本中被正确设置;

下面就是具体的任务脚本

使用lint-staged 对暂存代码进行优化

lint-staged 的配置项允许你在提交代码之前对特定类型的文件进行 lint 检查和格式化,确保代码质量和一致性。通过这种方式,可以避免将不符合规范的代码提交到版本库中。

"lint-staged": {
    "*.{ts,tsx,js}": [
            "eslint --config .eslintrc.js"
    ],
    "*.{css,less,scss}": [
            "stylelint --config .stylelintrc.js"
    ],
    "*.{ts,tsx,js,json,html,yml,css,less,scss,md}": [
            "prettier --write"
    ]
},

  1. "*.{ts,tsx,js}"

    • 匹配规则:匹配所有扩展名为 .ts.tsx, 和 .js 的文件。

    • 命令"eslint --config .eslintrc.js"

      • 作用:使用 ESLint 对匹配的文件进行 lint 检查。
      • 配置文件.eslintrc.js,这是 ESLint 的配置文件。
  2. "*.{css,less,scss}"

    • 匹配规则:匹配所有扩展名为 .css.less, 和 .scss 的文件。

    • 命令"stylelint --config .stylelintrc.js"

      • 作用:使用 Stylelint 对匹配的文件进行 lint 检查。
      • 配置文件.stylelintrc.js,这是 Stylelint 的配置文件。
  3. "*.{ts,tsx,js,json,html,yml,css,less,scss,md}"

    • 匹配规则:匹配所有扩展名为 .ts.tsx.js.json.html.yml.css.less.scss, 和 .md 的文件。

    • 命令"prettier --write"

      • 作用:使用 Prettier 对匹配的文件进行格式化。
      • 配置文件:Prettier 的配置文件通常是 .prettierrc 或者在 package.json 中的 prettier 字段。

颜色美化

  • 文本颜色

    • 黑色:\033[0;30m
    • 红色:\033[0;31m
    • 绿色:\033[0;32m
    • 黄色:\033[0;33m
    • 蓝色:\033[0;34m
    • 紫色:\033[0;35m
    • 青色:\033[0;36m
    • 白色:\033[0;37m
  • 背景颜色

    • 黑色:\033[40m
    • 红色:\033[41m
    • 绿色:\033[42m
    • 黄色:\033[43m
    • 蓝色:\033[44m
    • 紫色:\033[45m
    • 青色:\033[46m
    • 白色:\033[47m
  • 文本样式

    • 加粗:\033[1m
    • 下划线:\033[4m
    • 反显:\033[7m
  • 重置所有属性

    • \033[0m

image.png

"scripts": {
   "lint-staged": "lint-staged"
} 

lint-staged 配置会对文件进行ESLint校验和Prettier格式化

ESLint校验会拦截代码冲突和代码错误,Prettier会格式化代码格式后再提交保证仓库代码的统一性,减少多余的格式化提交。

# 添加格式化后的文件到暂存区
git add $(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|jsx|ts|tsx|json|css|scss|md)$')

ESLint

.eslintrc.js

require('@rushstack/eslint-patch/modern-module-resolution')

module.exports = {
    root: true, // 指定此配置文件为根配置,ESLint 将不会查找父目录中的配置文件
    env: {
        node: true, // 启用 Node.js 环境
        browser: true, // 启用浏览器环境
        es2021: true // 启用 ES2021 的全新语法
    },
    extends: [
        'plugin:vue/vue3-essential', // 基本 Vue 3 规则
        'eslint:recommended', // ESLint 推荐的规则
        '@vue/eslint-config-typescript', // Vue + TypeScript 的 ESLint 配置
        '@vue/eslint-config-prettier/skip-formatting' // 与 Prettier 兼容的 ESLint 配置,跳过格式化规则
    ],
    overrides: [
        {
            files: ['cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}'], // 对 Cypress 测试文件的特定规则
            extends: ['plugin:cypress/recommended'] // 启用 Cypress 的推荐规则
        }
    ],
    parserOptions: {
        ecmaVersion: 2021, // 支持 ECMAScript 2021 的语法
        sourceType: 'module' // 使用 ES 模块的语法
    },
    rules: {
        // 代码风格规则
        'semi': [2, 'never'], // 不使用分号
        'indent': [
            2,
            4,
            { SwitchCase: 1 } // switch 语句中的 case 分支使用 1 个空格缩进
        ],
        'no-multi-spaces': 2, // 不允许多个连续的空格
        'space-unary-ops': [2, { words: true, nonwords: false }], // 一元运算符后必须有空格
        'space-infix-ops': 2, // 中缀操作符周围必须有空格
        'space-before-blocks': [2, 'always'], // 代码块前必须有空格
        'no-mixed-spaces-and-tabs': 2, // 不允许混合使用空格和制表符
        'no-multiple-empty-lines': [2, { max: 1 }], // 连续空行不超过 1 行
        'no-trailing-spaces': 2, // 行尾不允许有空格
        'no-whitespace-before-property': 2, // 属性名和点运算符之间不能有空格
        'no-irregular-whitespace': 2, // 不允许出现不规则的空白字符
        'space-in-parens': [2, 'never'], // 圆括号内不能有空格
        'comma-dangle': [2, 'never'], // 逗号不允许有拖尾
        'array-bracket-spacing': [2, 'never'], // 数组括号内不允许有空格
        'object-curly-spacing': [2, 'never'], // 对象括号内不允许有空格
        'max-len': ['error', { code: 120 }], // 行宽最大为 120 字符
        'operator-linebreak': [2, 'before'], // 运算符换行时,运算符在行首
        'comma-style': [2, 'last'], // 逗号风格:换行时在行尾
        'no-extra-semi': 2, // 不允许出现多余的分号
        'curly': [2, 'all'], // 使用大括号包裹所有控制结构
        'key-spacing': [2, { beforeColon: false, afterColon: true }], // 属性名与冒号之间不能有空格,冒号后必须有空格
        'comma-spacing': [2, { before: false, after: true }], // 逗号后必须有空格
        'semi-spacing': [2, { before: false, after: true }], // 分号后必须有空格
        'camelcase': [1, { properties: 'always' }], // 强制使用驼峰命名法
        'new-cap': ['error', { newIsCap: true, capIsNew: false }], // 构造函数首字母必须大写
        'spaced-comment': [2, 'always'], // 注释后必须有空格
        'no-inline-comments': 2, // 不允许行内注释
        'eqeqeq': [2, 'always', { null: 'ignore' }], // 强制使用全等 (===) 运算符
        'no-else-return': [1, { allowElseIf: false }], // 禁止 else 语句,如果 if 语句中已返回值
        'no-loop-func': 2, // 禁止在循环中定义函数
        'no-restricted-syntax': [
            1,
            {
                selector: 'BinaryExpression[operator=\'instanceof\']',
                message: 'Use \'instanceof\' for object type detection.' // 不建议使用 instanceof 来检测对象类型
            },
            {
                selector: 'BinaryExpression[operator=\'typeof\']',
                message: 'Use \'typeof\' for type detection.' // 不建议使用 typeof 来检测类型
            },
            {
                selector: 'CallExpression[callee.name=\'parseInt\']',
                message: 'Use Math.floor, Math.round, or Math.ceil instead of parseInt to remove decimal points.' // 不建议使用 parseInt 来移除小数点
            }
        ],
        'no-implicit-coercion': [1, { allow: ['!!'] }], // 禁止隐式类型转换
        'radix': [2, 'always'], // parseInt 函数必须指定进制
        'quotes': [2, 'single'], // 强制使用单引号
        'no-array-constructor': 2, // 不允许使用 Array 构造函数
        'max-lines-per-function': [
            1,
            {
                max: 50, // 函数最大行数为 50 行
                skipComments: true, // 跳过注释行
                skipBlankLines: true, // 跳过空行
                IIFEs: true // 对立即调用的函数表达式 (IIFE) 应用规则
            }
        ],
        'max-params': [1, 6], // 函数参数最大数量为 6
        'no-eval': 2, // 禁止使用 eval
        'prefer-const': 1, // 建议使用 const 声明不变的变量
        'no-var': 1, // 建议使用 let/const 替代 var
        'prefer-destructuring': [
            1,
            { object: true, array: false } // 建议使用解构赋值
        ],
        'prefer-template': 1, // 建议使用模板字符串
        'template-curly-spacing': [2, 'never'], // 模板字符串中的花括号内不允许有空格
        'no-duplicate-imports': 2, // 禁止重复导入
        // TypeScript 特定规则
        '@typescript-eslint/no-unused-vars': 'error', // 禁止未使用的变量
        '@typescript-eslint/explicit-module-boundary-types': 'off' // 允许省略函数的返回类型
    },
    globals: {
        withDefaults: true, // Vue 3 特性
        defineExpose: true, // Vue 3 特性
        defineEmits: true, // Vue 3 特性
        defineProps: true // Vue 3 特性
    }
}

Prettier

{
  "printWidth": 200,
  "tabWidth": 4, 
  "useTabs": true, 
  "singleQuote": true, 
  "quoteProps": "as-needed",
  "trailingComma": "none", 
  "bracketSpacing": true, 
  "arrowParens": "always",
  "rangeStart": 0, 
  "requirePragma": false, 
  "insertPragma": false, 
  "proseWrap": "preserve",
  "htmlWhitespaceSensitivity": "ignore",
  "vueIndentScriptAndStyle": false,
  "endOfLine": "auto",
  "semi": true, 
  "overrides": [
    {
      "files": "*.json",
      "options": {
        "tabWidth": 4 
      }
    },
    {
      "files": "*.md",
      "options": {
        "printWidth": 100
      }
    }
  ]
}

git 提供的钩子有:

pre-commit: 在执行 git commit 命令时,在提交被创建之前触发。它允许你在执行提交之前自定义一些操作,例如代码风格检查、代码静态分析、单元测试等。

prepare-commit-msg:在提交消息编辑器打开之前触发,如果使用-m传递提交信息,则不会触发该钩子

commit-msg: 它在执行 git commit 命令时,编辑提交信息之后、提交之前触发。具体来说,commit-msg 钩子会在提交信息(commit message)被写入提交文件(如 .git/COMMIT_EDITMSG)后被触发。

post-commit: 在执行 git commit 命令时,在提交被创建之后触发。

pre-push:在执行 git push 命令之前触发

post-update:在执行 git push 命令后,远程仓库中的更新已成功推送到目标仓库后触发。

pre-receive:运行在服务端,在远程仓库接收推送操作时,在所有分支引用更新之前触发

update:运行在服务端,在执行 git push 命令后,远程仓库中的更新被成功推送到目标仓库,在每个分支引用被更新之前触发,pre-receive 先于 update。

pre-applypatch:在应用 patch 到工作目录之前触发。

applypatch-msg: 在 git 应用 patch 时被触发。具体来说,applypatch-msg 钩子会在 git 应用补丁到工作目录之前,对补丁的提交信息(commit message)进行处理。

pre-rebase:在执行 git rebase 命令之前触发

pre-merge-commit:在执行合并操作之前触发。具体来说,当你执行 git merge 命令时,git 将会在执行合并操作之前触发 pre-merge-commit 钩子。

push-to-checkout:运行在服务端,在客户端强制推送到当前检出分支时触发。

fsmonitor-watchman: fsmonitor-watchman 是一个可选的特性,git 可以通过 Watchman 服务来实现高效的文件系统监视功能。执行 git 的一些操作,比如 git status、git diff、git commit、git pull 等,需要检查文件系统的状态,在较大的代码库中,每次使用这些操作都会将整个项目文件夹检查一遍,频繁使用这些操作会导致较长时间的耗时,git 可以利用 WatchMan 提供的高效文件系统监视功能,从而减少状态检查操作的耗时。要使用 WatchMan,首先确保系统上已经安装了 Watchman ,并且 git 版本支持该特性。然后,通过配置 git,启用 core.fsmonitor 选项,并将其设置为 Watchman 来启用该特性。

watchman 通过减少不必要的操作来提高文件系统的检测性能,在检测时只关注文件变化的部分,而不是每次检测都将所有的项目文件都遍历一遍。fsmonitor-watchman 会在你执行任何与文件系统变更相关的 git 操作和文件系统变化时触发。

sendemail-validate:是 git 的一个配置选项,要想将其开启 sendemail-validate,可以通过 git config --global sendemail.validate true 设置,该选项的默认值取决于 git 版本。sendemail-validate 钩子在邮箱被发送之前调用。

对指定分支push限制

pre-push:在执行 git push 命令之前触发

npx husky add .husky/pre-push "npm run i"

推送代码的时候拦截指定分支的内容,如果有exit 1 终止push

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

# 获取当前分支
currBranch=$(git rev-parse --abbrev-ref HEAD)

# 获取当前分支已经合并的分支列表
mergedList=$(git branch --merged $currBranch)

# 打印当前分支信息
echo -e "\033[0;32m -------------------  当前分支: \033[33m $currBranch \033[0;32m 正在对提交的代码执行  \033[33m push \033[0;32m 操作 --------------------"

# 检查 mergedList 是否为空
if [ -z "$mergedList" ]; then
    echo "\033[0;33m 当前分支没有任何合并记录。"
    exit 0
fi

# 确保换行符是 Unix 格式的
mergedList=$(echo "$mergedList" | tr '\r' '\n')

# 使用 while 循环读取每一行
array=()
while IFS= read -r line; do
    array+=("$line")
done <<< "$mergedList"

# 定义要搜索的分支名称
search="test"
found=false

# 遍历合并列表,检查是否存在 test 分支
for item in "${array[@]}"; do
    item_without_spaces=${item//[[:space:]]/}
    if [ "$item_without_spaces" == "$search" ]; then
        found=true
        break
    fi
done

# 如果找到 test 分支,阻止合并操作
if $found; then
    echo  "\033[33m 当前分支 \033[0;32m  $currBranch  包含了 \033[0;31m $search 分支合并记录"
    echo "\033[0;31m test禁止合并到其他分支"
    exit 1
fi

# 如果未找到 test 分支,允许合并操作
exit 0

使用commitlint 限制提交信息的格式

创建commitlint.config.js 文件,编辑commit信息

添加钩子

npx husky add .husky/commit-msg "npm run commitlint"

配置

"scripts": {
   "commitlint": "commitlint --config commitlint.config.js -e -V",
} 

commitlint.config.js

module.exports = {
    ignores: [commit => commit.includes('init')],
    extends: ['@commitlint/config-conventional'],
    rules: {
        'body-leading-blank': [2, 'always'], // 主体前有空行
        'footer-leading-blank': [1, 'always'], // 末行前有空行
        'header-max-length': [2, 'always', 108], // 首行最大长度
        'subject-empty': [2, 'never'], // 标题不可为空
        'type-empty': [2, 'never'], // 类型不可为空
        'type-enum': [ // 允许的类型
            2,
            'always',
            [
                'wip', // 开发中
                'feat', // 新增功能
                'merge', // 代码合并
                'fix', // bug 修复
                'test', // 测试
                'refactor', // 重构
                'build', // 构造工具、外部依赖(webpack、npm)
                'docs', // 文档
                'perf', // 性能优化
                'style', // 代码风格(不影响代码含义)
                'ci', // 修改项目继续集成流程(Travis,Jenkins,GitLab CI,Circle等)
                'chore', // 不涉及 src、test 的其他修改(构建过程或辅助工具的变更)
                'workflow', // 流水线
                'revert', // 回退
                'types', // 类型声明
                'release', // 版本发布
            ],
        ],
    },
};

若commit信息信息不规范会被拦截

image.png