eslint能够帮助咱们约束代码规范,保证团队代码风格的一致性,前端项目不可或缺的代码检查工具,重要性不言而喻。那么你真正的看懂配置了吗?plugins是什么插件,extends又是继承了什么东西,有些时候怎么写的和plugin差很少呢,parser又是作什么的,rules要写这么多吗,去哪里找到对应的规则?javascript
当你出现这些疑问的时候,那么就应该看看本文,也许可以帮助你找到答案。html
帮助你们快速了解eslint,若是你的时间和精力很充裕,最直接有效的方式仍是看官方文档,强烈推荐。(做者废话多,文章很长,感谢阅读)前端
首先聊聊为何会有这篇文章,要从一个夜黑风高伸手掏出手机的瞬间提及,收到某大佬公众号的文章,红宝书4发了电子版,其author居然再也不是Nicholas C. Zakas,缘由是当年约稿,尼古拉斯认为不是时候,前端变化太快,还要等等,终于等到适合开始写的时候,却病了。同时,恰好团队在推动vue3 + ts的最佳实践,梳理eslint的发现author居然是Nicholas,真的是缘起缘聚呀,当下就有了写文章的冲动,难在一时不可脱身,搁浅。过了两周,我又翻出项目代码,发现撸了一遍源码的配置,居然忘了!一点没剩,这能忍?因此,此文诞生。vue
为何命名为eslint?理解为java
(ECMAScript, Lint) => eslint
复制代码
Lint是C语言著名的静态程序分析工具,使用在UNIX系统中,历史推移,陆续演变出Linux系统的splint及Windows系统中的PC-Lint.node
前端最先出现的Lint工具是Douglas Crockford于2002年建立的JSLint,若是你认同jslint默认配置规则的话,那么开箱即用。同时这也是它的缺点,不可配置致使没办法自由扩展,不能定制规则,出错的时候也很难找到出错点。jquery
JSHint是JSLint的fork版本,支持配置,使用者能够任意配置规则,也有相对完整的文档支持。初始自带少许配置,须要开发者本身添加不少规则,同时它很难找到哪条规则引发的错误,不支持自定义扩展。webpack
出自Google,比较久远,已经废弃。缘由是Javascript语法更新太快,其不支持后续的ES2015等后续内容的更新,没人维护。git
没有默认规则,支持定制化,支持预设,也可以很好的找到哪里出错,能够很好的被继承。只是它只检测code style问题,不能发现潜在的bug,好比未使用的变量,或者不当心定义的全局变量。github
灵活,可扩展,容易理解,让开发者自定义规则,提供完整的插件机制,动态加载规则,超详细的文档支持。(终于等到你,啰哩啰嗦讲了这么久,还不到重点!同志,你要有耐心)
读到这里,至少要意识到:
全文仿照官方文档结构介绍,重点讲述其背后的逻辑关系,让你少走弯路,看懂配置。
简单理解,就是各类rule组成的集合。 利用不一样配置字段进行rule组装,而后经过rules配置进行微调处理。
先从简单的规则入手,了解rules是什么,看个例子:
{
"rules": {
"semi": ["error", "always"],
"quotes": ["error", "double"]
}
}
复制代码
这两条规则分别表示的:句子末尾分号必须存在,使用双引号。
value值数组中,第一个值表示错误级别,有三个可选值:
咱们能够配置任意多个这样的规则,固然随着规则的增多,写起来就比较麻烦,因此eslint提供了一些预设,像这样:
{
"extends": "eslint:recommended"
}
复制代码
简单的解释,就是从基础配置中,继承可用的规则集合。
有两种使用方式:
1. 字符串
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": "eslint:recommended"
复制代码
这句简单的配置,到底怎么找到的对应rules的呢?
------------------------------------------题外话-------------------------------------------
以经常使用的IDE工具vscode为例说明,有三种方式使用eslint:
本质上,三种方式是同一种,经过不一样的方式,调用当前项目下node_modules下的eslint包。
1. vscode extension
~/.vscode/extensions/dbaeumer.vscode-eslint-x.x.x
./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
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`的代码逻辑里,能够追踪出三种配置方式:
插件是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()
共享配置就是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
在eslint的配置文件中,支持直接引入第三方插件,例如:
@typescript-eslint/eslint-plugin
eslint-plugin-vue
eslint-plugin-prettier
@jquery/eslint-plugin-jquery
复制代码
一样,`eslint-plugin-`前缀能够忽略。
{
"plugins": [
"@typescript-eslint",
"vue",
"prettier",
"@jquery/jquery"
]
}
复制代码
名称转化规则
看个官方例子
{
// ...
"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有什么关联关系?
因此结论是:使用preset,就是extends的用法,不使用preset就引用插件就行,而后自行配置rules。
默认状况下,eslint使用`Espress`解析,咱们能够选择不一样的解析器。好比咱们使用`eslint-plugin-vue`插件,就须要配置自定义parser。
{
"parser": "vue-eslint-parser"
}
复制代码
解析器`vue-eslint-parser`能够解析`.vue`文件。
该选项与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:指定其余语言功能
有的插件自带处理器,处理器能够从另外一种文件中提取js代码,而后让elint对js代码进行lint处理。或者在预处理中转换js代码。
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
2. plugin:vue/vue3-strongly-recommended
eslint-plugin-vue
提供的preset。3. @vue/typescript/recommended
@vue/eslint-config-typescript
提供的preset。经过源码能够看到,全部默认提供的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用了好几年,最近花不少时间进行梳理,尝试所有讲清楚彷佛也不太容易,源码看了不少。那么当咱们看源码的时候,咱们应该学什么?
文章较长,可能也比较乱,各位菜鸟大佬们,不要手上嘴上留情,哪里没理解,读起来不通顺,明显逻辑不正确,欢迎斧正,积极交流,互相学习。
团队内初次分享后,发现一些明显的问题,在这里以QA的形式,补充说明下。
1.写文章思路是帮助你们看懂配置,理解配置项表明的含义。而这些须要读者具有一些初级的eslint知识,至少了解什么是rule,本身接触过配置,修改过,有一点点的学习门槛。
2.上述被我忽略的前提说明,发现一个新的文章思路,教程类的文章《教别人配置eslint》,内容能够从基础eslint提及,介绍完默认配置等属性后。引入若是要使用vue,那么如何约束vue代码呢,进一步,若是使用typescript,又改如何引入ts的校验语法规则呢。这个三步走战略能够是一篇很好的教程文章。
3.为何使用项目vue-cli集成的命令,npm run lint 检查结果和项目启动检测结果不一样,两处应该是一致的才对,是哪里出现问题?
这个问题,能够肯定的结论:
4.在eslint的配置文件中,extends属性,支持多种配置方法,那么plugin和share config 有什么区别?
必定是我太认真了,居然晕晕的。这么简单的区别,其实从名字上就能够明白,一个是共享,一个是插件。共享就是不会新加内容,只对原有内容的梳理,把最终配置好的文件,共享给其余人使用。插件,顾名思义,是会引入新东西的,会有新的rule引入,导出的配置文件,包含了新引入的rule规则。而share config没有新的rule。
4. Eslint官网
7. ESLint工做原理