1. eslint 介绍
eslint 属于一种 QA 工具,是一个 ECMAScript/JavaScript 语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。
eslint 完全可配置,它的目标是提供一个插件化的 javascript 代码检测工具。这意味着您可以关闭每个规则,只能使用基本语法验证,或者混合并匹配捆绑的规则和自定义规则,使 eslint 完美的适用于当前项目。
[ESLint 中文官方网站]:http://eslint.cn/
官网:https://eslint.org/
2. lint 工具发展过程
在 JavaScript 20 多年的发展历程中,也出现过许许多多的 lint 工具,下面就来介绍下主流的三款 lint 工具。
- JSLint
JSLint 可以说是最早出现的 JavaScript 的 lint 工具,由 Douglas Crockford (《JavaScript 语言精粹》作者) 开发。JSLint 的所有规则都是由 Douglas 自己定义的,可以说这是一个极具 Douglas 个人风格的 lint 工具,如果你要使用它,就必须接受它所有规则。值得称赞的是,JSLint 依然在更新,而且也提供了 node 版本:node-jslint。
- JSHint
由于 JSLint 让很多人无法忍受它的规则,感觉受到了压迫,所以 Anton Kovalyov 基于 JSLint 开发了 JSHint。JSHint 在 JSLint 的基础上提供了丰富的配置项,给了开发者极大的自由,JSHint 一开始就保持着开源软件的风格,由社区进行驱动,发展十分迅速。早起 jQuery 也是使用 JSHint 进行代码检查的,不过现在已经转移到 ESLint 了。
- ESLint
ESLint 由 Nicholas C. Zakas (《JavaScript 高级程序设计》作者) 于2013年6月创建,它的出现因为 Zakas 想使用 JSHint 添加一条自定义的规则,但是发现 JSHint 不支持,于是自己开发了一个。
ESLint 号称下一代的 JS Linter 工具,它的灵感来源于 PHP Linter,将源代码解析成 AST,然后检测 AST 来判断代码是否符合规则。ESLint 使用 esprima 将源代码解析成 AST,然后你就可以使用任意规则来检测 AST 是否符合预期,这也是 ESLint 高可扩展性的原因。
但是,那个时候 ESLint 并没有大火,因为需要将源代码转成 AST,运行速度上输给了 JSHint ,并且 JSHint 当时已经有完善的生态(编辑器的支持)。真正让 ESLint 大火是因为 ES6 的出现。
ES6 发布后,因为新增了很多语法,JSHint 短期内无法提供支持,而 ESLint 只需要有合适的解析器就能够进行 lint 检查。这时 babel 为 ESLint 提供了支持,开发了 babel-eslint,让 ESLint 成为最快支持 ES6 语法的 lint 工具。
在 2016 年,ESLint 整合了与它同时诞生的另一个 lint 工具:JSCS,因为它与 ESLint 具有异曲同工之妙,都是通过生成 AST 的方式进行规则检测。
3. lint 工具优点
- 避免低级 bug,找出可能发生的语法错误
使用未声明变量、修改 const 变量
- 提示删除多余的代码
- 确保代码遵循最佳实践
可参考 airbnb style、javascript standard, AlloyTeam eslint-config-alloy
- 统一团队的代码风格
加不加分号、使用 tab 还是空格
4. 如何使用
4.1 安装
如何在项目中引入 eslint:
# 全局安装 ESLint
$ npm install -g eslint
# 进入项目
$ cd ~/workspace/project
# 初始化 ESLint 配置
$ eslint --init
在使用 eslint --init 后,会出现很多用户配置项,具体可以参考:eslint cli 部分的源码。
经过一系列一问一答的环节后,你会发现在你文件夹的根目录生成了一个 .eslintrc.js 文件。
4.2 配置
ESLint 一共有两种配置方式:
- 使用注释把 lint 规则直接嵌入到源代码中
// ~/workspace/project/src/test.js
/* eslint eqeqeq: "error" */
const num = 1
num == '1'
这时运行 eslint src/test.js
会报错误
Expected '===' and instead saw '=='
当然我们一般使用注释是为了临时禁止某些严格的 lint 规则出现的警告
/* eslint-disable */
alert('该注释放在文件顶部,整个文件都不会出现 lint 警告')
/* eslint-enable */
alert('重新启用 lint 告警')
/* eslint-disable */
alert('只禁止某一个或多个规则')
/* eslint-disable-next-line */
alert('当前行禁止 lint 警告')
alert('当前行禁止 lint 警告') // eslint-disable-line
- 使用配置文件进行 lint 规则配置
在初始化过程中,有一个选项就是使用什么文件类型进行 lint 配置(What format do you want your config file to be in?)
官方一共提供了三个选项:
- JavaScript (eslintrc.js)
- YAML (eslintrc.yaml)
- JSON (eslintrc.json)
翻阅 ESLint 源码 可以看到,其配置文件的优先级如下:
// 优先级按定义从上往下依次降低
const configFilenames = [
".eslintrc.js",
".eslintrc.yaml",
".eslintrc.yml",
".eslintrc.json",
".eslintrc",
"package.json"
];
4.3 项目级与目录级的配置
我们有如下目录结构,此时在根目录运行 ESLint,那么我们将得到两个配置文件 .eslintrc.js(项目级配置) 和 src/.eslintrc.js(目录级配置),这两个配置文件会进行合并,但是 src/.eslintrc.js 具有更高的优先级
# project 项目文件目录
.
├── src
│ ├── .eslintrc.js
├── .eslintrc.js
...
但是,我们只要在 src/.eslintrc.js 中配置 "root": true,那么 ESLint 就会认为 src 目录为根目录,不再向上查找配置。
4.4 配置参数
- 解析器配置
{
// 解析器类型
// espima(默认), babel-eslint, @typescript-eslint/parse
"parser": "esprima",
// 解析器配置参数
"parseOptions": {
// 代码类型:script(默认), module
"sourceType": "script",
// es 版本号,默认为 5,也可以是用年份,比如 2015 (同 6)
"ecamVersion": 6,
// es 特性配置
"ecmaFeatures": {
"globalReturn": true, // 允许在全局作用域下使用 return 语句
"impliedStrict": true, // 启用全局 strict mode
"jsx": true // 启用 JSX
},
}
}
对于 @typescript-eslint/parse 这个解析器,主要是为了替代之前存在的 TSLint,TS 团队因为 ESLint 生态的繁荣,且 ESLint 具有更多的配置项,不得不抛弃 TSLint 转而实现一个 ESLint 的解析器。同时,该解析器拥有 @typescript-eslint/parse 不同配置
{
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"useJSXTextNode": true,
"project": "./tsconfig.json",
"tsconfigRootDir": "../../",
"extraFileExtensions": [".vue"]
}
}
- 环境与全局变量
ESLint 会检测未声明的变量,并发出警告,但是有些变量是我们引入的库声明的,这里就需要提前在配置中声明。
{
"globals": {
// 声明 jQuery 对象为全局变量
"$": false // true表示该变量为 writeable,而 false 表示 readonly
}
}
在 globals 中一个个的进行声明未免有点繁琐,这个时候就需要使用到 env ,这是对一个环境定义的一组全局变量的预设(类似于 babel 的 presets)。
{
"env": {
"amd": true,
"commonjs": true,
"jquery": true
}
}
可选的环境很多,预设值都在 这个文件 中进行定义,查看源码可以发现,其预设变量都引用自 globals 包。
4.5 规则配置
ESLint 附带有 大量的规则,你可以在配置文件的 rules 属性中配置你想要的规则。每一条规则接受一个参数,参数的值如下:
- "off" 或 0:关闭规则
- "warn" 或 1:开启规则,warn 级别的错误 (不会导致程序退出)
- "error" 或 2:开启规则,error级别的错误(当被触发的时候,程序会退出)
但是,事情往往没有我们想象中那么简单,ESLint 的规则不仅只有关闭和开启这么简单,每一条规则还有自己的配置项。如果需要对某个规则进行配置,就需要使用数组形式的参数, 如下 quotes 规则设置
{
"rules": {
// 使用数组形式,对规则进行配置
// 第一个参数为是否启用规则
// 后面的参数才是规则的配置项
"quotes": [
"error",
"single",
{
"avoidEscape": true
}
]
}
}
按照如上规则
// bad
let str = "test 'ESLint' rule"
// good
let str = 'test "ESLint" rule'
5 扩展
扩展就是直接使用别人已经写好的 lint 规则,方便快捷。扩展一般支持三种类型:
{
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"eslint-config-standard",
]
}
- eslint: 开头的是 ESLint 官方的扩展,一共有两个:eslint:recommended 、eslint:all。
- plugin: 开头的是扩展是插件类型,也可以直接在 plugins 属性中进行设置。
- 最后一种扩展来自 npm 包,官方规定 npm 包的扩展必须以 eslint-config- 开头,使用时可以省略这个头,上面案例中 eslint-config-standard 可以直接简写成 standard
如果你觉得自己的配置十分满意,也可以将自己的 lint 配置发布到 npm 包,只要将包名命名为 eslint-config-xxx 即可,同时,需要在 package.json 的 peerDependencies 字段中声明你依赖的 ESLint 的版本号。
6. 插件
虽然官方提供了上百种的规则可供选择,但是这还不够,因为官方的规则只能检查标准的 JavaScript 语法,如果你写的是 JSX 或者 Vue 单文件组件,ESLint 的规则就开始束手无策了。
这个时候就需要安装 ESLint 的插件,来定制一些特定的规则进行检查。ESLint 的插件与扩展一样有固定的命名格式,以 eslint-plugin- 开头,使用的时候也可以省略这个头。
# npm install --save-dev eslint-plugin-vue eslint-plugin-react
配置如下:
{
"plugins": [
"react", // eslint-plugin-react
"vue", // eslint-plugin-vue
]
}
或者是在扩展中引入插件,前面有提到 plugin: 开头的是扩展是进行插件的加载。
{
"extends": [
"plugin:react/recommended",
]
}
eslint-plugin-react 源码 可以看到:
module.exports = {
// 自定义的 rule
rules: allRules,
// 可用的扩展
configs: {
// plugin:react/recommended
recomended: {
plugins: [ 'react' ]
rules: {...}
},
// plugin:react/all
all: {
plugins: [ 'react' ]
rules: {...}
}
}
}
配置名是插件配置的 configs 属性定义的,这里的配置其实就是 ESLint 的扩展,通过这种方式即可以加载插件,又可以加载扩展
6.1 开发插件
ESLint 官方为了方便开发者,提供了 Yeoman 的模板(generator-eslint)
# 安装模块
npm install -g yo generator-eslint
# 创建目录
mkdir eslint-plugin-demo
cd eslint-plugin-demo
# 创建模板
yo eslint:plugin
创建好项目之后,就可以开始创建一条规则了,幸运的是 generator-eslint 除了能够生成插件的模板代码外,还具有创建规则的模板代码。
# 生成模板
yo eslint:rule
打开 lib/rules/rule-demo.js ,可以看到默认的模板代码如下
module.exports = {
meta: {
docs: {
description: "disable console",
category: "Fill me in",
recommended: false
},
schema: []
},
create: function (context) {
// variables should be defined here
return {
// give me methods
};
}
};
详细的参数介绍可以查看官方文档
具体示例可以参考 博文
7. 忽略文件
我们的整个项目中,有一些文件是不需要 eslint 进行检查,我们可以用到忽略文件。
在我们的工程目录中新建一个文件,命名为 “.eslintignore” ,eslint 会自动识别这个文件
- 您可以通过提供不同文件的路径来覆盖此行为。--ignore-path
eslint --ignore-path tmp/.eslintignore file.js
eslint --ignore-path .gitignore file.js
- --no-ignore 禁用从文件的排除 .eslintignore , --ignore-path 和 --ignore-pattern
eslint --no-ignore file.js
比如,你已经在忽略文件 .eslintignore 中忽略了某文件,但是你又在命令行中执行了 eslint 该文件,则为警告提示,您可以使用 --no-ignore 忽略使用忽略规则。
- 在 package.json 中使用 eslintIgnore
如果 .eslintignore 未找到文件并且未指定备用文件,ESLint 将在 package.json 中查找 eslintIgnore 密钥以检查要忽略的文件。
{
"name": "mypackage",
"version": "0.0.1",
"eslintConfig": {
"env": {
"browser": true,
"node": true
}
},
"eslintIgnore": ["hello.js", "world.js"]
...
}
8. 工程应用
实际工程中,我们一般会搭配一些优秀的插件和第三方库构建工作流。
- 搭配使用 Prettier 统一代码风格
- npm 安装 husky 和 lint-staged,配置 git 检查工作流
下面是 我项目中用到的一些配置
// package.json
{
{
"name": "...",
"version": "...",
"scripts": {
"lint:style": "stylelint "src/**/*.scss" --syntax scss",
"lint:style-fix": "stylelint "src/**/*.scss" --syntax scss --fix",
"lint": "eslint src --ext .ts,.tsx",
"lint-fix": "eslint src --fix --ext .ts,.tsx",
...
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{ts,tsx}": [
"eslint --fix",
"git add"
],
"src/**/*.scss": [
"stylelint --config=.stylelintrc --fix",
"git add"
]
},
}
// .eslintrc.json
{
"extends": [
"plugin:@typescript-eslint/recommended",
"react-app",
"prettier/@typescript-eslint",
"plugin:prettier/recommended"
],
"plugins": ["react", "@typescript-eslint", "prettier"],
"env": {
"browser": true,
"jasmine": true,
"jest": true
},
"rules": {
"prettier/prettier": ["error", {
"singleQuote": true,
"endOfLine": "auto"
}],
"@typescript-eslint/interface-name-prefix": ["error", "always"],
"@typescript-eslint/explicit-member-accessibility": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/no-angle-bracket-type-assertion": 0,
"@typescript-eslint/array-type": 0,
"jsx-a11y/anchor-is-valid": 0
},
"parser": "@typescript-eslint/parser"
}