前言
eslint可以帮助我们约束代码规范,保证团队代码风格的一致性,前端项目不可或缺的代码检查工具,重要性不言而喻。那么你真正的看懂配置了吗?plugins是什么插件,extends又是继承了什么东西,有些时候怎么写的和plugin差不多呢,parser又是做什么的,rules要写这么多吗,去哪里找到对应的规则?
当你出现这些疑问的时候,那么就应该看看本文,也许能够帮助你找到答案。
本文目的
帮助大家快速了解eslint,如果你的时间和精力很充裕,最直接有效的方式还是看官方文档,强烈推荐。(作者废话多,文章很长,感谢阅读)
契机
首先聊聊为什么会有这篇文章,要从一个夜黑风高伸手掏出手机的瞬间说起,收到某大佬公众号的文章,红宝书4发了电子版,其author竟然不再是Nicholas C. Zakas,原因是当年约稿,尼古拉斯认为不是时候,前端变化太快,还要等等,终于等到适合开始写的时候,却病了。同时,刚好团队在推进vue3 + ts的最佳实践,梳理eslint的发现author竟然是Nicholas,真的是缘起缘聚呀,当下就有了写文章的冲动,难在一时不可脱身,搁浅。过了两周,我又翻出项目代码,发现撸了一遍源码的配置,竟然忘了!一点没剩,这能忍?所以,此文诞生。
Lint历史
为什么命名为eslint?理解为
(ECMAScript, Lint) => eslint
Lint
Lint是C语言著名的静态程序分析工具,使用在UNIX系统中,历史推移,陆续演变出Linux系统的splint及Windows系统中的PC-Lint.
JSLint
前端最早出现的Lint工具是Douglas Crockford于2002年创建的JSLint,如果你认同jslint默认配置规则的话,那么开箱即用。同时这也是它的缺点,不可配置导致没办法自由扩展,不能定制规则,出错的时候也很难找到出错点。
JSHint
JSHint是JSLint的fork版本,支持配置,使用者可以任意配置规则,也有相对完整的文档支持。初始自带少量配置,需要开发者自己添加很多规则,同时它很难找到哪条规则引起的错误,不支持自定义扩展。
Closure Linter
出自Google,比较久远,已经废弃。原因是Javascript语法更新太快,其不支持后续的ES2015等后续内容的更新,没人维护。
JSCS
没有默认规则,支持定制化,支持预设,也能够很好的找到哪里出错,可以很好的被继承。只是它只检测code style问题,不能发现潜在的bug,比如未使用的变量,或者不小心定义的全局变量。
ESlint
灵活,可扩展,容易理解,让开发者自定义规则,提供完整的插件机制,动态加载规则,超详细的文档支持。(终于等到你,啰哩啰嗦讲了这么久,还不到重点!同志,你要有耐心)
小结
读到这里,至少要意识到:
- 文档的重要性,良好的文档是保证代码可使用的前提。
- 可维护性,code需要有人持续维护,才能发挥价值。
- 跟随时代变化,落后只能被淘汰。
正文
全文仿照官方文档结构介绍,重点讲述其背后的逻辑关系,让你少走弯路,看懂配置。
eslint
简单理解,就是各种rule组成的集合。 利用不同配置字段进行rule组装,然后通过rules配置进行微调处理。
configuration
先从简单的规则入手,了解rules是什么,看个例子:
{
"rules": {
"semi": ["error", "always"],
"quotes": ["error", "double"]
}
}
这两条规则分别表示的:句子末尾分号必须存在,使用双引号。
value值数组中,第一个值表示错误级别,有三个可选值:
- "off" or 0 - turn the rule off
- "warn" or 1 - turn the rule on as a warning (doesn't affect exit code)
- "error" or 2 - turn the rule on as an error (exit code will be 1)
我们可以配置任意多个这样的规则,当然随着规则的增多,写起来就比较麻烦,所以eslint提供了一些预设,像这样:
{
"extends": "eslint:recommended"
}
extends
简单的解释,就是从基础配置中,继承可用的规则集合。
有两种使用方式:
1. 字符串
- 配置文件path,如"./config/custom-vue-eslint.js"
- 共享配置的名称,如"eslint:recommended"
2. 字符串数组
extends: [
'eslint:recommended',
'plugin:vue/vue3-strongly-recommended',
'@vue/typescript/recommended'
]
eslint递归的继承配置,所以base config可以有`extends`属性,`rules`的内容可以继承或者重写规则集的任意内容,有几种方式:
1. 新添加规则
2. 修改规则的提醒级别,比如从`error`->`warn` // 这么神奇的配置代码怎么写的?
例如:
base config: ["error", "always", {"null": "ignore"}]
a === b
foo === true
item.value !== 'DOWN'
foo == null
derived config: "eqeqeq": "warn"
result config: "eqeqeq": ["warn", "always", {"null": "ignore"}]
3. 重写options参数。
例如:
base config: "quotes": ["error", "single", {"avoidEscape": true}]
e.g.: const double = "a string containing 'single' quotes"
derived config: "quotes": ["error", "single"]
result config: "quotes": ["error", "single"]
extends默认配置
那么
"extends": "eslint:recommended"
这句简单的配置,到底怎么找到的对应rules的呢?
------------------------------------------题外话-------------------------------------------
以常用的IDE工具vscode为例说明,有三种方式使用eslint:
- vscode extension 插件: ESLint
- npm node_modules包:eslint command
- vue-cli集成插件:cli-plugin-eslint
本质上,三种方式是同一种,通过不同的方式,调用当前项目下node_modules下的eslint包。
1. vscode extension
- Mac插件文件位置:
~/.vscode/extensions/dbaeumer.vscode-eslint-x.x.x
- vscode 启动,执行
./client/out/extension.js
- 检测项目目录是否存在
'.eslintrc.js', '.eslintrc.yaml', '.eslintrc.yml', '.eslintrc', '.eslintrc.json'
- 不存在的话,
utils.findEslint()
然后Terminal执行eslint --init
- 存在,
activate() -> realActivate() -> migration.record()
,通过激活eslint.xxxx命令,执行eslint包,检测并做出反馈提示,就是我们经常见到的红色波浪线。
2. eslint命令行
eslint --no-fix
或者eslint -c path/.eslintrc
- 全局安装直接
eslint
命令,当前项目安装使用npx eslint
- 入口在
./node_modules/eslint/bin/eslint.js
3. cli-plugin-eslint
- vue-cli启动,加载`cli-plugin-eslint`插件和`eslint-loader`,注册lint命令
- 通过
npm run lint
执行eslint/lib/lint.js
文件
最终都需要eslint/lib/cli-engine/cli-engine.js
文件,启动eslint引擎,lint or init。
而eslint config的内容解析在cli-engine.js引用的@eslint/eslintrc包中。
--------------------------------------------------------------------------------------------
通过查阅node_modules/@eslint/eslintrc包,找到如下关键代码:
@eslint/eslintrc -> lib/index.js -> lib/cascading-config-array-factory.js -> lib/config-array-factory.js
看代码帮助理解,可跳过阅读。
`@eslint/eslintrc/lib/config-array-factory.js`
/**
* Load configs of an element in `extends`.
* @param {string} extendName The name of a base config.
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
_loadExtends(extendName, ctx) {
debug("Loading {extends:%j} relative to %s", extendName, ctx.filePath);
try {
if (extendName.startsWith("eslint:")) {
return this._loadExtendedBuiltInConfig(extendName, ctx);
}
if (extendName.startsWith("plugin:")) {
return this._loadExtendedPluginConfig(extendName, ctx);
}
return this._loadExtendedShareableConfig(extendName, ctx);
} catch (error) {
error.message += `\nReferenced from: ${ctx.filePath || ctx.name}`;
throw error;
}
}
在`eslint/lib/cli-engine/cli-engine.js`,关注eslintRecommendedPath/eslintAllPath
变量
const configArrayFactory = new CascadingConfigArrayFactory({
additionalPluginPool,
baseConfig: options.baseConfig || null,
cliConfig: createConfigDataFromOptions(options),
cwd: options.cwd,
ignorePath: options.ignorePath,
resolvePluginsRelativeTo: options.resolvePluginsRelativeTo,
rulePaths: options.rulePaths,
specificConfigPath: options.configFile,
useEslintrc: options.useEslintrc,
builtInRules,
loadRules,
eslintRecommendedPath: path.resolve(__dirname, "../../conf/eslint-recommended.js"),
eslintAllPath: path.resolve(__dirname, "../../conf/eslint-all.js")
});
`_loadConfigData`函数的作用是加载指定的配置文件。
所以,配置"extends": "eslint:recommended"
指的使用就是eslint-recommended.js文件里面的内容。同理,还可以使用"extends": "eslint:all"
从`_loadExtends`的代码逻辑里,可以追踪出三种配置方式:
- 默认配置,如上述解释。
- 插件配置
- 共享配置
extends插件配置
插件是npm包导出的rules配置对象,有些插件可以导出一个或者多个配置对象,例如:
eslint-plugin-babel
eslint-plugin-vue
eslint-plugin-jest
eslint-plugin-eslint-plugin
插件配置可以忽略前缀`eslint-plugin-`,如下写法:
{
"extends": [
"plugin:jest/all",
"plugin:vue/recommended"
]
}
格式这样写:`plugin:${packageName}/${configurationName}`
逻辑代码在config-array-factory.js -> _loadExtendedPluginConfig()
extends共享配置
共享配置就是npm包导出的一个配置对象,例如:
eslint-config-standard
eslint-config-airbnb
eslint-config-prettier
@vue/eslint-config-typescript
extends配置可以忽略前缀`eslint-config-`,仅仅使用包名,所以在`.eslintrc.js`文件中,我们使用如下两种写法都是正确的。
{
"extends": "eslint-config-standard"
}
{
"extends": [
"standard",
"prettier",
"@vue/typescript"
]
}
逻辑代码在config-array-factory.js -> _loadExtendedShareableConfig()
命名解析在@eslint/eslintrc/lib/shared/naming.js
plugins
在eslint的配置文件中,支持直接引入第三方插件,例如:
@typescript-eslint/eslint-plugin
eslint-plugin-vue
eslint-plugin-prettier
@jquery/eslint-plugin-jquery
同样,`eslint-plugin-`前缀可以忽略。
{
"plugins": [
"@typescript-eslint",
"vue",
"prettier",
"@jquery/jquery"
]
}
名称转化规则
- eslint-plugin-foo -> foo/a-rule
- @foo/eslint-plugin -> @foo/a-config
- @foo/eslint-plugin-bar -> @foo/bar/a-environment
看个官方例子
{
// ...
"plugins": [
"jquery", // eslint-plugin-jquery
"@foo/foo", // @foo/eslint-plugin-foo
"@bar" // @bar/eslint-plugin
],
"extends": [
"plugin:@foo/foo/recommended",
"plugin:@bar/recommended"
],
"rules": {
"jquery/a-rule": "error",
"@foo/foo/some-rule": "error",
"@bar/another-rule": "error"
},
"env": {
"jquery/jquery": true,
"@foo/foo/env-foo": true,
"@bar/env-bar": true,
}
// ...
}
有个疑问,这里的plugin和extends中提到的plugin有什么关联关系?
- plugin配置,是把第三方规则引入到eslint的配置文件中,我们可以在rules配置更改plugin插件提供的rules。
- extends配置,是使用node_modules包提供的preset rules。同样,也可以在rules配置中修改rule内容。
- 两个配置的根本作用不同。extends可以使用plugin配置提供的preset,详见上面的`extends插件配置`部分。`preset rules`可以帮助减少rules规则的书写,在项目中,我们通常直接使用社区的最佳实践recommended,然后基于此rules,进行微调。
所以结论是:使用preset,就是extends的用法,不使用preset就引用插件就行,然后自行配置rules。
parser
默认情况下,eslint使用`Espress`解析,我们可以选择不同的解析器。比如我们使用`eslint-plugin-vue`插件,就需要配置自定义parser。
{
"parser": "vue-eslint-parser"
}
解析器`vue-eslint-parser`可以解析`.vue`文件。
parser options
该选项与parser配合使用,当使用自定义parser时,options的内容并不是每一项都会被自定义parser需要。options允许eslint自定义ECMAScript支持的语法。
{
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
}
}
-
ecmaVersion: 3, 5(default), 6, 7, 8, 9, 10, 11, 12 or 2015, 2016, ..., 2021
-
sourceType:"script" or "module",表示在什么模式下解析代码。
if (sourceType === "module" && ecmaVersion < 6) { throw new Error("sourceType 'module' is not supported when ecmaVersion < 2015. Consider adding `{ ecmaVersion: 2015 }` to the parser options."); }
-
ecmaFeatures:指定其他语言功能
processor
有的插件自带处理器,处理器可以从另一种文件中提取js代码,然后让elint对js代码进行lint处理。或者在预处理中转换js代码。
overrides
overrides配置可以更精细的控制某些规则,可以只针对某个特殊场景生效,这样设定很灵活。
其他配置
未介绍environments/globals等概念,看官方文档,与parser相关的AST(Abstract Syntax Tree)会单独文章讲解。
项目实践
目前项目(vue3 + typescript)上是这样使用的,配置文件.eslintrc.js
:
module.exports = {
root: true,
env: {
node: true,
browser: true
},
plugins: [
'vue',
'@typescript-eslint' // 可省略,why?
],
extends: [
'eslint:recommended',
'plugin:vue/vue3-strongly-recommended',
'@vue/typescript/recommended'
],
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 2020,
sourceType: 'module'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'semi': ['error', 'always'],
'indent': ['error', 4, {
'SwitchCase': 1
}],
'no-empty-function': 'off',
'no-useless-escape': 'off',
// allow paren-less arrow functions
'arrow-parens': ['error', 'as-needed'],
// enforce consistent linebreak style for operators
'operator-linebreak': ['error', 'before'],
'space-before-function-paren': ['error', {
'anonymous': 'always',
'named': 'never',
'asyncArrow': 'always'
}],
'no-template-curly-in-string': 'error',
// require space before blocks
'space-before-blocks': ['error', 'always'],
// enforce consistent spacing before and after keywords
'keyword-spacing': 'error',
// enforce consistent spacing between keys and values in object literal properties
'key-spacing': 'error',
// require or disallow spacing between function identifiers and their invocations
'func-call-spacing': ['error', 'never'],
// enforce consistent spacing before and after commas
'comma-spacing': ['error', {
'before': false,
'after': true
}],
// disallow or enforce spaces inside of parentheses
'space-in-parens': ['error', 'never'],
// enforce consistent spacing inside braces
'object-curly-spacing': ['error', 'never'],
'vue/html-indent': ['error', 4],
'vue/max-attributes-per-line': ['error', {
'singleline': 10,
'multiline': {
'max': 1,
'allowFirstLine': false
}
}],
'@typescript-eslint/semi': ['error', 'always'],
'@typescript-eslint/indent': ['error', 4],
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-this-alias': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off'
},
overrides: [
{
files: ['*.ts', '*.tsx'],
parserOptions: {
parser: '@typescript-eslint/parser',
project: './tsconfig.json'
},
rules: {
'@typescript-eslint/restrict-plus-operands': 'error'
}
},
{
files: ['*.js', '*.ts'],
rules: {
'@typescript-eslint/explicit-module-boundary-types': 'warn'
}
}
]
};
运用我们前面的知识,该配置引入两个插件,eslint-plugin-vue
和@typescript-eslint/eslint-plugin
,使用三种预设规则,配置两种语法解析器,以及自定义支持的一系列规则,并重写ts的两个特殊规则。
示例讲解
重点看下extends下配置的三个preset:
eslint:recommended
- 在`extends`已经重点讲过。
- 所有eslint的rules通过官方文档可查阅。
- check mark表示默认启用的规则。
2. plugin:vue/vue3-strongly-recommended
- 来自插件
eslint-plugin-vue
提供的preset。 - 所有vue的rules通过官方文档可查阅。
- 插件提供多种preset。
3. @vue/typescript/recommended
- 来自共享配置
@vue/eslint-config-typescript
提供的preset。 - 所有typescript-eslint的rules通过官方文档可查阅。
通过源码可以看到,所有默认提供的preset。
再来讲下, '@typescript-eslint' // 可省略,why?
这句话是什么意思。
@vue/eslint-config-typescript/recommended.js(from version: 7.0.0)
module.exports = {
extends: [
'./index.js',
'plugin:@typescript-eslint/recommended'
],
// the ts-eslint recommended ruleset sets the parser so we need to set it back
parser: require.resolve('vue-eslint-parser'),
rules: {
// this rule, if on, would require explicit return type on the `render` function
'@typescript-eslint/explicit-function-return-type': 'off'
},
overrides: [
{
files: ['shims-tsx.d.ts'],
rules: {
'@typescript-eslint/no-empty-interface': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off'
}
}
]
};
extends使用文件path,引用当前路径下的index.js文件
@vue/eslint-config-typescript/index.js
module.exports = {
plugins: ['@typescript-eslint'], // Prerequisite `eslint-plugin-vue`, being extended, sets
// root property `parser` to `'vue-eslint-parser'`, which, for code parsing,
// in turn delegates to the parser, specified in `parserOptions.parser`:
// https://github.com/vuejs/eslint-plugin-vue#what-is-the-use-the-latest-vue-eslint-parser-error
parserOptions: {
parser: require.resolve('@typescript-eslint/parser'),
extraFileExtensions: ['.vue'],
ecmaFeatures: {
jsx: true
}
},
extends: [
'plugin:@typescript-eslint/eslint-recommended'
],
overrides: [{
files: ['*.ts', '*.tsx'],
rules: {
// The core 'no-unused-vars' rules (in the eslint:recommeded ruleset)
// does not work with type definitions
'no-unused-vars': 'off'
}
}]
};
把两个文件合在一起,看看长什么样。(eslint的extends是使用的递归方式检测配置,但最终和文件整合在一起原理一样,需要排列好关系优先级)
module.exports = {
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended'
],
plugins: ['@typescript-eslint'],
parser: require.resolve('vue-eslint-parser'),
parserOptions: {
parser: require.resolve('@typescript-eslint/parser'),
extraFileExtensions: ['.vue'],
ecmaFeatures: {
jsx: true
}
},
rules: {
'@typescript-eslint/explicit-function-return-type': 'off'
},
overrides: [
{
files: ['shims-tsx.d.ts'],
rules: {
'@typescript-eslint/no-empty-interface': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off'
}
},
{
files: ['*.ts', '*.tsx'],
rules: {
'no-unused-vars': 'off'
}
}
]
};
整合后有两个共享配置,我们再去看看@typescript-eslint/eslint-plugin
源码,发现@typescript-eslint/eslint-plugin/dist/configs/recommended.js
也已经经继承eslint-recommended.js
内容。
所以就解释了.eslintrc.js
中的注释内容。
{
plugins: ["@typescript-eslint"] // 可忽略
}
可以忽略上面的配置,@vue/typescript/recommended
已经包含。
--------------------------------------------------------------------------------------------
简化模型,粗略讲下extends的继承,主要是ConfigArray。
假如,简化后的.eslintrc.js
文件如下:
module.exports = {
root: true,
env: {
node: true,
browser: true
},
extends: [
'@vue/typescript/recommended'
],
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 2020,
sourceType: 'module'
}
};
@vue/eslint-config-typescript/recommended.js
简化后如下:
module.exports = {
extends: [
'plugin:@typescript-eslint/recommended'
],
parser: require.resolve('vue-eslint-parser'),
rules: {
'@typescript-eslint/explicit-function-return-type': 'off'
}
};
@typescript-eslint/eslint-plugin/recommended
简化后如下:
module.exports = {
rules: {
'@typescript-eslint/adjacent-overload-signatures': 'error',
'@typescript-eslint/ban-ts-comment': 'error'
}
};
那么最终生成待处理的ConfigArray(5)
[
{
type: 'config',
name: 'DefaultIgnorePattern',
// ...
},
{
type: 'config',
name: '.eslintrc.js » @vue/eslint-config-typescript/recommended » plugin:@typescript-eslint/recommended',
rules: {
'@typescript-eslint/adjacent-overload-signatures': 'error',
'@typescript-eslint/ban-ts-comment': 'error'
},
// ...
},
{
type: 'config',
name: '.eslintrc.js » @vue/eslint-config-typescript/recommended',
importerName: '.eslintrc.js » @vue/eslint-config-typescript/recommended',
rules: { '@typescript-eslint/explicit-function-return-type': 'off' },
// ...
},
{
type: 'config',
name: '.eslintrc.js',
filePath: 'xxxx/project/.eslintrc.js',
env: { node: true, browser: true },
globals: undefined,
ignorePattern: undefined,
noInlineConfig: undefined,
parser: {
error: null,
filePath: '/xxxx/project/node_modules/vue-eslint-parser/index.js',
id: 'vue-eslint-parser',
importerName: '.eslintrc.js',
importerPath: '/xxxx/project/.eslintrc.js'
},
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 2020,
sourceType: 'module'
},
plugins: {},
processor: undefined,
reportUnusedDisableDirectives: undefined,
root: true,
rules: undefined,
settings: undefined
},
{
type: 'ignore',
name: '.eslintignore',
// ...
}
]
从ConfigArray的5个子项中,很清晰的看到递归的解析过程。
--------------------------------------------------------------------------------------------
总结
Eslint用了好几年,最近花很多时间进行梳理,尝试全部讲清楚似乎也不太容易,源码看了很多。那么当我们看源码的时候,我们应该学什么?
- 明确,它要解决的问题是什么。
- 学习函数命名,组织代码结构,逻辑关系。
- 或多或少能够发现精彩的code写法,拿出小本本抄下来。
- 能够帮助我们更好的理解官方文档。
写在最后
文章较长,可能也比较乱,各位菜鸟大佬们,不要手上嘴上留情,哪里没理解,读起来不通顺,明显逻辑不正确,欢迎斧正,积极交流,互相学习。
QA
团队内初次分享后,发现一些明显的问题,在这里以QA的形式,补充说明下。
1.写文章思路是帮助大家看懂配置,理解配置项代表的含义。而这些需要读者具备一些初级的eslint知识,至少了解什么是rule,自己接触过配置,修改过,有一点点的学习门槛。
2.上述被我忽略的前提说明,发现一个新的文章思路,教程类的文章《教别人配置eslint》,内容可以从基础eslint说起,介绍完默认配置等属性后。引入如果要使用vue,那么如何约束vue代码呢,进一步,如果使用typescript,又改如何引入ts的校验语法规则呢。这个三步走战略可以是一篇很好的教程文章。
3.为什么使用项目vue-cli集成的命令,npm run lint 检查结果和项目启动检测结果不同,两处应该是一致的才对,是哪里出现问题?
这个问题,可以确定的结论:
- npm run lint 和 npm run serve 使用的都是是project下面的.eslintrc.js文件
- 但是npm run serve忽略了*.d.ts文件,所以没有error,至于进一步的问题,仍在跟进,from issue可能是bug
- npm run serve 只有首次运行后,有warning和error提示,是因为存在本地缓存,在node_modules/.cache目录,删除.cache目录后,就可以每次都看到错误提示信息,已经通过更改eslint-loader为eslint-webpack-plugin插件解决。
4.在eslint的配置文件中,extends属性,支持多种配置方法,那么plugin和share config 有什么区别?
一定是我太认真了,竟然晕晕的。这么简单的区别,其实从名字上就可以明白,一个是共享,一个是插件。共享就是不会新加内容,只对原有内容的梳理,把最终配置好的文件,共享给其他人使用。插件,顾名思义,是会引入新东西的,会有新的rule引入,导出的配置文件,包含了新引入的rule规则。而share config没有新的rule。
参考资料
4. Eslint官网
7. ESLint工作原理