工程化之代码规范——eslint + prettier + husky梳理实践

8,992 阅读16分钟

还没有给项目添加一套规范的代码质量检查机制的同学快上车,从0到1,理解&实践。

eslint

作用

eslint本质就是一个内置有解析器的工具,它可以将项目代码解析成AST,然后根据AST抽象语法树分析出代码里存在的问题然后给出警告或者报错。eslint的初衷就是在代码编写阶段尽早得发现存在的错误;除了语法检查外,eslint也具有一定的代码格式化能力,但是不是其能力的重心(prettier在代码格式方面更加专业)。

初始化

如果使用脚手架初始化项目,比如通过vite创建项目:pnpm create vite这样得到的项目模板都对eslint进行了初始化配置。

如果手动给项目配置eslint检查:

# 全局安装eslint依赖
npm i eslint -g
​
# 给项目初始化eslint,包括安装devDependencies依赖 & 生成配置
eslint --init

经过问答之后生成eslint的配置文件.eslintrc.cjs

配置项——parser & parserOptions

本身eslint的语法检查就是一个先对代码进行静态解析得到AST,然后再判断的过程。所以在eslint默认的解析器基础上,自然需要一些更高级的解析器来支持更新的语法以及语言,比如eslint的默认解析器是Espree,它只支持对es5的js进行解析,所以我们如果项目中使用了ts,Espree就不行了,自然需要用到更高级的(支持ts以及最新es版本的)解析器@typescript-eslint/parser

更加准确的说,在配置文件中parser项的value为所使用解析器的模块名,如

module.exports = {
    "parser": "@typescript-eslint/parser", // 使用@typescript-eslint/parser这个解析器进行语法解析
    // ...
}

通过parser我们指定了项目所使用的语法解析器,parserOptions就相当于给出解析器更详细的解析配置,比如如下配置,parserOptions就具体指定了@typescript-eslint/parser解析器应该支持最新版本的es标准("ecmaVersion": "latest")以及项目的模块化标准为esModule"sourceType": "module"

module.exports = {
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaVersion": "latest",
        "sourceType": "module"
    },
}

配置项——globals & env

globals是定义全局变量,规定他们可不可以重写以及是否被禁用,env就是一组globals预设(一组打包的globals配置),如下配置:

{
  "globals": {
    // 声明 jQuery 对象为全局变量
    "$": false, // true表示该变量为 writeable,而 false 表示 readonly, 即不可重写$
    "jQuery": false,
    "Promise": "off" // 禁用Promise
  },
  "env": {
    "es6": true // 启用es6这个预设,里面肯定包含了若干globals配置来对es6进行支持
   },
}

配置项——rules & plugins & extends

rules就是具体的eslint进行语法检查所依据的规则;但是官方只提供了关于标准js的检查规则,所以需要plugins来拓展规则集合,比如针对用Vite创建的react + Ts的项目,就通过react-refresh这个plugin拓展了eslint的规则,但plugins中拓展的规则默认不开启,所以plugins要与rules配合使用来拓展eslint的功能;但是我们总不可能把所有规则一条一条的写在rules中,extends就相当于一组配置好的rules与plugins组合,解决了这个痛点。

rules中的规则优先级最高,会覆盖拓展以及插件中引入的规则。

一般来说规则的值有三个值,只需控制是开启还是关闭:

  • off 或 0:关闭规则
  • warn 或 1:开启规则,warn 级别的错误 (不会导致程序退出)
  • error 或 2:开启规则,error级别的错误(当被触发的时候,程序会退出)

"eqeqeq": "off"。有的规则有自己的属性,使用起来像这样:"quotes": ["error", "double"]。具体内容查看规则文档

文件级别的配置优先级

我们的项目中可以存在多个eslint配置文件,那么文件进行eslint检查时,文件所处位置向上直至文件系统的根目录路径上所有的eslint配置文件都会生效,但是越”靠近“文件的配置优先级越高(可以理解为高优先级规则覆盖低优先级规则)。

如下,source.js使用配置A,但是test.js使用配置B和配置A,但是配置B中的规则会覆盖掉A中相同的规则。

your-project
├── .eslintrc  - eslint配置A
├── lib
│ └── source.js
└─┬ childFolder
  ├── .eslintrc - eslint配置B
  └── test.js

配置项"root": true可以阻止继续递归的查找比较远的根目录。

package.json中也可以对eslint进行配置,所以项目中文件的eslint配置文件可以总结为:

  1. 与要检测的文件在同一目录下的 .eslintrc.*package.json 文件
  2. 继续在父级目录寻找 .eslintrcpackage.json文件,直到根目录(包括根目录)或直到发现一个有"root": true的配置。

vscode中的使用

在vscode中安装eslint插件之后,无需在命令行中手动执行eslint命令即可在编码时实时提供eslint语法检查,而且也可以开启eslint的代码格式化功能,需要进行如下vscode配置:

ctrl + shift + p打开搜索栏搜索settings.json配置文件,项目内生成.vscode文件夹,在其下的settings.json中新增配置:

{
    "[typescriptreact]": {
        "editor.formatOnSave": true
    },
}

意为对tsx语言进行保存时格式化。

支持的语言:

javascript;
javascriptreact;
typescript;
typescriptreact;
json;
graphql;

prettier

作用

prettier的作用就是代码格式化,可以理解为prettier把代码解析之后再按照预期的规则进行重新打印,并且支持各种语言,如JavaScriptFlowTypeScriptCSSSCSSLessJSXVueGraphQLJSONMarkdown等等

初始化 & prettier执行

pnpm i prettier -D安装之后,工程目录下新建.prettierrc.js配置文件以及.prettierignore忽略文件,这样就相当于告诉我们的工程我们使用了prettier,之后执行prettier格式化命令时即按照配置进行格式化,如npx prettier. --write格式化所有文件。

prettier 隐式忽略node_modules,并不需要将其添加到.prettierignore

.prettierrc.js

配置就比较简单直观了,都是一条一条具体的格式化规则,如下配置中基本都是默认值:

//此处的规则供参考,其中多半其实都是默认值,可以根据个人习惯改写
module.exports = {
  printWidth: 80, //单行长度
  tabWidth: 2, //缩进长度
  useTabs: false, //使用空格代替tab缩进
  semi: true, //句末使用分号
  singleQuote: true, //使用单引号
  quoteProps: 'as-needed', //仅在必需时为对象的key添加引号
  jsxSingleQuote: true, // jsx中使用单引号
  trailingComma: 'all', //多行时尽可能打印尾随逗号
  bracketSpacing: true, //在对象前后添加空格-eg: { foo: bar }
  jsxBracketSameLine: true, //多属性html标签的‘>’折行放置
  arrowParens: 'always', //单参数箭头函数参数周围使用圆括号-eg: (x) => x
  requirePragma: false, //无需顶部注释即可格式化
  insertPragma: false, //在已被preitter格式化的文件顶部加上标注
  proseWrap: 'preserve', //不知道怎么翻译
  htmlWhitespaceSensitivity: 'ignore', //对HTML全局空白不敏感
  vueIndentScriptAndStyle: false, //不对vue中的script及style标签缩进
  endOfLine: 'lf', //结束行形式
  embeddedLanguageFormatting: 'auto', //对引用代码进行格式化
};

vscode中的使用

安装prettier的vscode插件,安装之后我们可以右键需要进行格式化的文件然后选择prettier进行格式化。

自动化:

ctrl + shift + p打开搜索栏搜索settings.json配置文件,项目内生成.vscode文件夹,在其下的settings.json中新增配置:

{
  // 设置全部语言的默认格式化程序为prettier
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  // 设置特定语言的默认格式化程序为prettier
  //   "[javascript]": {
  //     "editor.defaultFormatter": "esbenp.prettier-vscode"
  //   },
  // 设置全部语言在保存时自动格式化
  "editor.formatOnSave": true
  // 设置特定语言在保存时自动格式化
  //   "[javascript]": {
  //     "editor.formatOnSave": true
  //   }
}

eslint && prettier

最佳实践——解决eslint与prettier在代码格式上的冲突

因为eslint本身也具备对代码格式的控制与检查能力,所以不可避免可能会与prettier的代码格式冲突,比如eslint配置rules中对缩进的要求为2并在不满足时报错,.eslint.cjs

rules: {
    indent: ['error', 2],
},

.prettierrc.cjs

module.exports = {
      tabWidth: 6, //缩进长度
};

那么我们在执行npx prettier . --write后进行eslint代码检查eslint .就会把所有缩进问题进行报错,检查不通过。

解决方案很简单——思路就是把prettier的规则复写进eslint中,并对原本eslint中的格式配置进行覆盖,这样就做到了eslint的格式化检查与prettier的格式化行为统一。

用社区的轮子即可:

# 安装eslint-config-prettier
pnpm install -D eslint-config-prettier
// 在 .eslintrc.* 文件里面的 extends 字段最后添加一行:
{
  "extends": [
    ...,
    "已经配置的规则",
+   "prettier",
  ]
}

extends的值为数组,后面的数组项会继承和覆盖前面的配置,所以完成了prettier规则对eslint规则的扩充和覆盖。

最佳实践——省略prettier格式化命令,eslint进行格式化与检查一步到位

完成上述操作本质是做到了 ESLint 会按照 Prettier 的规则做相关校验,也就是说先执行Prettier格式化后再执行eslint检查不会因为格式问题冲突而报错,但是还是需要运行 Prettier 命令来进行格式化。为了避免多此一举,社区也提供了整合上面两步的方案:在使用 eslint --fix(eslint错误修复) 时候,实际使用 Prettier 来替代 ESLint 的格式化功能。操作如下:

# 安装eslint-plugin-prettier
pnpm install -D eslint-plugin-prettier
// 在 .eslintrc.* 文件里面的 extends 字段最后再添加一行:
{
  "extends": [
    ...,
    "已经配置的规则",
+   "plugin:prettier/recommended"
  ],
  "rules": {
+   "prettier/prettier": "error",
  }
}

这个时候运行 eslint --fix 实际使用的是 Prettier 去格式化文件。在rules中添加"prettier/prettier": "error",用意是编写代码时不符合prettier格式规范的编码eslint直接自动报错(结合vscode的eslint插件实时报错的能力)。

当然直接执行eslint --fix会没有反应,原因是eslint命令缺少目标文件,可以用--ext [文件拓展名,[文件拓展名]]的形式指定需要进行eslint修复以及检查的文件,比如react + ts项目中:

eslint . --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0

表示进行eslint检查的同时进行自动修复(--fix),针对的文件是以ts、tsx为拓展名的(--ext ts,tsx),还有一些错误打印相关的要求(--report-unused-disable-directives),代码执行不退出的可以容忍的警告数量为0。

eslint命令参数参考

eslint [options] file.js [file.js] [dir]
​
Basic configuration: //基本配置
  --no-eslintrc                  Disable use of configuration from .eslintrc.* //禁止使用来自.eslintrc.*的配置文件
  -c, --config path::String      Use this configuration, overriding .eslintrc.* config options if present //如果存在.eslintrc.*,则使用且重写该配置文件
  --env [String]                 Specify environments //指定环境
  --ext [String]                 Specify JavaScript file extensions - default: .js //指定的JS文件扩展名,默认:.js
  --global [String]              Define global variables //定义全局变量
  --parser String                Specify the parser to be used //指定使用某种解析器
  --parser-options Object        Specify parser options //指定解析参数
​
Specifying rules and plugins: //指定规则和插件
  --rulesdir [path::String]      Use additional rules from this directory //从该路径使用额外的规则
  --plugin [String]              Specify plugins //指定插件
  --rule Object                  Specify rules//指定规则
​
Fixing problems://修正问题
  --fix                          Automatically fix problems//自动修复问题
  --fix-dry-run                  Automatically fix problems without saving the changes to the file system//自动修复问题而不保存对文件系统的更改
​
Ignoring files: //忽略文件
  --ignore-path path::String     Specify path of ignore file //指定忽略的文件
  --no-ignore                    Disable use of ignore files and patterns //禁止使用忽略文件和样式
  --ignore-pattern [String]      Pattern of files to ignore (in addition to those in .eslintignore) //要忽略的文件模式(除了在.eslintignore中的文件)
​
Using stdin: //unix]标准输入(设备)文件
  --stdin                        Lint code provided on <STDIN> - default: false
  --stdin-filename String        Specify filename to process STDIN as //指定用于处理stdin的文件名
​
Handling warnings://处理警告
  --quiet                        Report errors only - default: false //仅以错误报告出来
  --max-warnings Int             Number of warnings to trigger nonzero exit code - default: -1 //要触发非零退出代码的警告数-默认值:-1
 
Output:
  -o, --output-file path::String  Specify file to write report to //指定输出的文件路径
  -f, --format String            Use a specific output format - default: stylish //使用特定的输出格式-默认:stylish
  --color, --no-color            Force enabling/disabling of color
​
Inline configuration comments: //内联配置注释
  --no-inline-config             Prevent comments from changing config or rules //阻止注释更改配置或规则
  --report-unused-disable-directives  Adds reported errors for unused eslint-disable directives //为未使用的eslint disable指令添加报告的错误
​
Caching:
  --cache                        Only check changed files - default: false //仅仅检查改变过的文件
  --cache-file path::String      Path to the cache file. Deprecated: use --cache-location - default: .eslintcache//缓存文件的路径。已弃用:使用--缓存位置-默认值:.eslintcache
  --cache-location path::String  Path to the cache file or directory//缓存文件或文件夹的路径
​
Miscellaneous://其他
  --init                         Run config initialization wizard - default: false //运行配置初始化向导-默认值:false
  --debug                        Output debugging information//输出调试信息
  -h, --help                     Show help 
  -v, --version                  Output the version number
  --print-config path::String    Print the configuration for the given file//打印给定文件的配置

最佳实践——覆盖vscode本地格式化配置(代码格式层面协作统一)

由于每个人本地的 VS Code 代码格式化配置不拘一格,在实际的项目开发中,多多少少会因为格式化问题产生争议。因此需要有一个统一的规范覆盖本地配置,editorconfig for vs code承担起了这个作用,只要在项目工程的根目录文件夹下添加.editorconfig文件,那么这个文件声明的代码规范规则能覆盖编辑器默认的代码规范规则,从而实现统一的规范标准。

一般我们都用prettier进行代码格式化,在vscode中Prettier读取配置的优先级即:

  1. Prettier 配置文件,比如.prettierrc.prettier.config.js
  2. .editorconfig文件,用于覆盖用户/工作区设置。

.editorconfig举例:

root = true                         # 根目录的配置文件,编辑器会由当前目录向上查找,如果找到 `roor = true` 的文件,则不再查找
​
[*]
indent_style = space                # 空格缩进,可选"space"、"tab"
indent_size = 4                     # 缩进空格为4个
end_of_line = lf                    # 结尾换行符,可选"lf"、"cr"、"crlf"
charset = utf-8                     # 文件编码是 utf-8
trim_trailing_whitespace = true     # 不保留行末的空格
insert_final_newline = true         # 文件末尾添加一个空行
curly_bracket_next_line = false     # 大括号不另起一行
spaces_around_operators = true      # 运算符两遍都有空格
indent_brace_style = 1tbs           # 条件语句格式是 1tbs
​
[*.js]                              # 对所有的 js 文件生效
quote_type = single                 # 字符串使用单引号
​
[*.{html,less,css,json}]            # 对所有 html, less, css, json 文件生效
quote_type = double                 # 字符串使用双引号
​
[package.json]                      # 对 package.json 生效
indent_size = 4                   # 使用2个空格缩进

最佳实践——Husky + lint-staged增加代码commit前检查

husky基本使用

husky做的事情就是在git工作流的某个时机触发脚本,也就是git hook,比如我们在git commit之前进行eslint语法检查,eslint检查过程中报错或者警告太多是会中断指令(git commit)执行,所以这样就保证了提交到远程的代码是通过eslint检查的。

(配置详情查询husky官网

安装依赖:

pnpm i -D husky

husky初始化:

pnpm dlx husky-init

不同包管理工具初始化的命令不同,去husky官网指南查看即可。

执行如上命令后,项目里生成.husky文件夹,文件夹下有个pre-commit文件,内容如下:

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

先不管前两行shell命令的作用,最后一行的命令pnpm lint即为在commit之前插入执行的命令,我们可以自由配置script里的脚本。同时注意到,因为husky初始化的执行,package.jsonscript脚本中还多了一个"prepare": "husky install",我去调研了一下,这个脚本的用意如下:

再早版本的husky给git添加hook的模式是全量注册,也就是说不管当前git hook中我们有没有想要插入的脚本,husky都会注册这个git hook(具体的git hook应该就是一些shell命令吧,可以在项目的.git/hooks目录中查看),但是这样做显然存在冗余,所以husky有一次大的版本迭代就是为了解决这个问题,只有存在想要插入的脚本,才会注册对应的git hook。

如上的prepare脚本会在install安装依赖之后执行,它对应的husky install命令的作用我猜测就是根据.husky文件夹下的这些具体的脚本文件来注册git hook,至于上面pre-commit文件中的前两行,应该就是注册git hook时的脚本命令吧。

经过上面的操作后,如果我们应该已经install过项目依赖了,那么我们手动执行一下prepare脚本,之后pre-commit之前就会执行pnpm lint这个命令了。

如果需要注册其它的git hook,就去官网查看具体指令吧,这里就不赘述了。

lint-staged

如果一个项目中期才引入husky,为了防止历史文件报错,或者说我们单纯的不想进行全量的eslint检查,我们只想对git add到暂存区(staged area)的文件进行lint检查,那么可以借助lint-staged来实现。

操作

首先pnpm i lint-staged -D安装。

其实安装lint-staged之后,本质上带给我们项目的就是一个命令行命令:lint-staged,这个命令的作用就是根据package.json中的顶层lint-staged配置对git add到暂存区的文件进行lint检查

可能的package.json配置:

{
  "scripts": {
    "prepare": "husky install",
    "lint-staged": "lint-staged"
  },
  "lint-staged": {
    "*.{ts,tsx}": [
        "echo 'begin to lint staged ts or tsx file'",
        "eslint --fix"
    ]
  }
}

(注:如果使用npm i lint-staged -g全局安装,那么可以直接在命令行执行lint-staged,但如果如上只在项目内安装lint-staged作为dev依赖,就需要借助npx或者script脚本间接执行lint-staged命令,也就是如上配置一个lint-stagedscript脚本)

修改.husky/pre-commit

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"# 使用pnpm运行script中的lint-staged脚本
pnpm lint-staged

经过如上配置,我们再次进行commit提交时就会触发lint-staged脚本,脚本内容是执行lint-staged命令,lint-staged命令根据package.json中的顶层lint-staged配置执行lint检查。

最佳实践——Husky + commitizen对commit msg进行格式化检查

本质还是利用husky添加git hook——检查commit的msg内容。下面记录一下配置步骤

1、pnpm install --D commitizen

2、项目使用不同的包管理工具执行的命令不同,可以去commitzen的github阅读文档,使用pnpm的话执行:

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

3、执行:

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

4、执行如下命令配置config文件:

# Configure commitlint to use conventional config
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

5、添加commit-msg这个git hook,使其检查msg格式:

npx husky add .husky/commit-msg  'npx --no -- commitlint --edit ${1}'