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工做原理