小沈是一個剛剛開始工做的前端實習生,第一次進行團隊開發,不免有些緊張。在導師的安排下,拿到了項目的 git 權限,開始進行 clone。javascript
$ git clone git@github.com:company/project.git
複製代碼
小沈開始細細品味着同事們的代碼,終於在他的不懈努力下,發現了老王 2 年前寫的一個 bug,跟導師報備以後,小沈開始着手修改。年輕人嘛,容易衝動,不只修復了老王的 bug,還把這部分代碼進行了重構,使用了前兩天剛剛從書裏學會的策略模式,去掉了一些沒必要要 if else 邏輯。小沈瀟灑的摸了摸本身稀疏的頭髮,得意的準備提交代碼,想着第一天剛來就秀了下本身的超強的編碼能力。接下來可怕的事情發生了,代碼死活不能經過 lint 工具的檢測,急得他面紅耳赤,趕忙跑去問導師,導師告訴他,只要按照控制檯的 warning 修改代碼就好。小沈反駁道,這個 lint 工具非讓我去掉分號,我在學校的時候,老師就教我分號是必不可少的,沒有分號的代碼是不完美的。導師無奈的笑了笑,打開了小沈的實習評分表,在團隊合做一項中勾選『較差』。前端
不服氣的小沈,寫了一篇博客發佈到了 CSDN 上,還收穫了很多閱讀量。vue
在計算機科學中,lint是一種工具的名稱,它用來標記代碼中,某些可疑的、不具結構性(可能形成bug)的語句。它是一種靜態程序分析工具,最先適用於C語言,在UNIX平臺上開發出來。後來它成爲通用術語,可用於描述在任何一種編程語言中,用來標記代碼中有疑義語句的工具。 -- by wikipediajava
在 JavaScript 20 多年的發展歷程中,也出現過許許多多的 lint 工具,下面就來介紹下主流的三款 lint 工具。node
JSLint 能夠說是最先出現的 JavaScript 的 lint 工具,由 Douglas Crockford (《JavaScript 語言精粹》做者) 開發。從《JavaScript 語言精粹》的筆風就能看出,Douglas 是個眼裏容不得瑕疵的人,因此 JSLint 也繼承了這個特點,JSLint 的全部規則都是由 Douglas 本身定義的,能夠說這是一個極具 Douglas 我的風格的 lint 工具,若是你要使用它,就必須接受它全部規則。值得稱讚的是,JSLint 依然在更新,並且也提供了 node 版本:node-jslint。react
因爲 JSLint 讓不少人沒法忍受它的規則,感受受到了壓迫,因此 Anton Kovalyov (如今在 Medium 工做) 基於 JSLint 開發了 JSHint。JSHint 在 JSLint 的基礎上提供了豐富的配置項,給了開發者極大的自由,JSHint 一開始就保持着開源軟件的風格,由社區進行驅動,發展十分迅速。早起 jQuery 也是使用 JSHint 進行代碼檢查的,不過如今已經轉移到 ESLint 了。jquery
ESLint 由 Nicholas C. Zakas (《JavaScript 高級程序設計》做者) 於2013年6月建立,它的出現由於 Zakas 想使用 JSHint 添加一條自定義的規則,可是發現 JSHint 不支持,因而本身開發了一個。git
ESLint 號稱下一代的 JS Linter 工具,它的靈感來源於 PHP Linter,將源代碼解析成 AST,而後檢測 AST 來判斷代碼是否符合規則。ESLint 使用 esprima 將源代碼解析吃成 AST,而後你就可使用任意規則來檢測 AST 是否符合預期,這也是 ESLint 高可擴展性的緣由。程序員
早期源碼:github
var ast = esprima.parse(text, { loc: true, range: true }),
walk = astw(ast);
walk(function(node) {
api.emit(node.type, node);
});
return messages;
複製代碼
可是,那個時候 ESLint 並無大火,由於須要將源代碼轉成 AST,運行速度上輸給了 JSHint ,而且 JSHint 當時已經有完善的生態(編輯器的支持)。真正讓 ESLint 大火是由於 ES6 的出現。
ES6 發佈後,由於新增了不少語法,JSHint 短時間內沒法提供支持,而 ESLint 只須要有合適的解析器就可以進行 lint 檢查。這時 babel 爲 ESLint 提供了支持,開發了 babel-eslint,讓ESLint 成爲最快支持 ES6 語法的 lint 工具。
在 2016 年,ESLint整合了與它同時誕生的另外一個 lint 工具:JSCS,由於它與 ESLint 具備殊途同歸之妙,都是經過生成 AST 的方式進行規則檢測。
自此,ESLint 在 JS Linter 領域一統江湖,成爲前端界的主流工具。
下面一塊兒來思考一個問題:Lint 工具對工程師來講究竟是代碼質量的保證仍是一種束縛?
而後,咱們再看看 ESLint 官網的簡介:
代碼檢查是一種靜態的分析,經常使用於尋找有問題的模式或者代碼,而且不依賴於具體的編碼風格。對大多數編程語言來講都會有代碼檢查,通常來講編譯程序會內置檢查工具。
JavaScript 是一個動態的弱類型語言,在開發中比較容易出錯。由於沒有編譯程序,爲了尋找 JavaScript 代碼錯誤一般須要在執行過程當中不斷調試。像 ESLint 這樣的可讓程序員在編碼的過程當中發現問題而不是在執行的過程當中。
由於 JavaScript 這門神奇的語言,在帶給咱們靈活性的同時,也埋下了一些坑。好比 ==
涉及到的弱類型轉換,着實讓人很苦惱,還有 this
的指向,也是一個讓人迷惑的東西。而 Lint 工具就很好的解決了這個問題,乾脆禁止你使用 ==
,這種作法雖然限制了語言的靈活性,可是帶來的收益也是可觀的。
還有就是做爲一門動態語言,由於缺乏編譯過程,有些本能夠在編譯過程當中發現的錯誤,只能等到運行才發現,這給咱們調試工做增長了一些負擔,而 Lint 工具至關於爲語言增長了編譯過程,在代碼運行前進行靜態分析找到出錯的地方。
因此彙總一下,Lint工具的優點:
使用未聲明變量、修改 const 變量……
聲明而未使用的變量、重複的 case ……
加不加分號?使用 tab 仍是空格?
說了那麼多,仍是來看下有點實際意義的,ESLint 究竟是如何使用的。
若是想在現有項目中引入 ESLint,能夠直接運行下面的命令:
# 全局安裝 ESLint
$ npm install -g eslint
# 進入項目
$ cd ~/Code/ESLint-demo
# 初始化 package.json
$ npm init -f
# 初始化 ESLint 配置
$ eslint --init
複製代碼
在使用 eslint --init
後,會出現不少用戶配置項,具體能夠參考:eslint cli 部分的源碼。
通過一系列一問一答的環節後,你會發如今你文件夾的根目錄生成了一個 .eslintrc.js
文件。
ESLint 一共有兩種配置方式:
這是最簡單粗暴的方式,直接在源代碼中使用 ESLint 可以識別的註釋方式,進行 lint 規則的定義。
/* eslint eqeqeq: "error" */
var num = 1
num == '1'
複製代碼
固然咱們通常使用註釋是爲了臨時禁止某些嚴格的 lint 規則出現的警告:
/* eslint-disable */
alert('該註釋放在文件頂部,整個文件都不會出現 lint 警告')
/* eslint-enable */
alert('從新啓用 lint 告警')
/* eslint-disable eqeqeq */
alert('只禁止某一個或多個規則')
/* eslint-disable-next-line */
alert('當前行禁止 lint 警告')
alert('當前行禁止 lint 警告') // eslint-disable-line
複製代碼
在初始化過程當中,有一個選項就是使用什麼文件類型進行 lint 配置(What format do you want your config file to be in?
):
{
type: "list",
name: "format",
message: "What format do you want your config file to be in?",
default: "JavaScript",
choices: ["JavaScript", "YAML", "JSON"]
}
複製代碼
官方一共提供了三個選項:
另外,你也能夠本身在 package.json
文件中添加 eslintConfig
字段進行配置。
翻閱 ESLint 源碼能夠看到,其配置文件的優先級以下:
const configFilenames = [
".eslintrc.js",
".eslintrc.yaml",
".eslintrc.yml",
".eslintrc.json",
".eslintrc",
"package.json"
];
複製代碼
.eslintrc.js > .eslintrc.yaml > .eslintrc.yml > .eslintrc.json > .eslintrc > package.json
複製代碼
固然你也可使用 cli 本身指定配置文件路徑:
咱們有以下目錄結構,此時在根目錄運行 ESLint,那麼咱們將獲得兩個配置文件 .eslintrc.js
(項目級配置) 和 src/.eslintrc.js
(目錄級配置),這兩個配置文件會進行合併,可是 src/.eslintrc.js
具備更高的優先級。
可是,咱們只要在 src/.eslintrc.js
中配置 "root": true
,那麼 ESLint 就會認爲 src
目錄爲根目錄,再也不向上查找配置。
{
"root": true
}
複製代碼
下面咱們一塊兒來細細品味 ESLinte 的配置規則。
{
// 解析器類型
// espima(默認), babel-eslint, @typescript-eslint/parse
"parse": "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 的解析器。同時,該解析器擁有不一樣的配置:
{
"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
包。
ESLint 附帶有大量的規則,你能夠在配置文件的 rules
屬性中配置你想要的規則。每一條規則接受一個參數,參數的值以下:
舉個例子,咱們先寫一段使用了平等(equality)的代碼,而後對 eqeqeq
規則分別進行不一樣的配置。
// demo.js
var num = 1
num == '1'
複製代碼
這裏使用了命令行的配置方式,若是你只想對單個文件進行某個規則的校驗就可使用這種方式。
可是,事情每每沒有咱們想象中那麼簡單,ESLint 的規則不只只有關閉和開啓這麼簡單,每一條規則還有本身的配置項。若是須要對某個規則進行配置,就須要使用數組形式的參數。
咱們看下 quotes
規則,根據官網介紹,它支持字符串和對象兩個配置項。
{
"rules": {
// 使用數組形式,對規則進行配置
// 第一個參數爲是否啓用規則
// 後面的參數纔是規則的配置項
"quotes": [
"error",
"single",
{
"avoidEscape": true
}
]
}
}
複製代碼
根據上面的規則:
// bad
var str = "test 'ESLint' rule"
// good
var str = 'test "ESLint" rule'
複製代碼
擴展就是直接使用別人已經寫好的 lint 規則,方便快捷。擴展通常支持三種類型:
{
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"eslint-config-standard",
]
}
複製代碼
eslint:
開頭的是 ESLint 官方的擴展,一共有兩個:eslint:recommended
、eslint:all
。plugin:
開頭的是擴展是插件類型,也能夠直接在 plugins
屬性中進行設置,後面一節會詳細講到。eslint-config-
開頭,使用時能夠省略這個頭,上面案例中 eslint-config-standard
能夠直接簡寫成 standard
。若是你以爲本身的配置十分滿意,也能夠將本身的 lint 配置發佈到 npm 包,只要將包名命名爲 eslint-config-xxx
便可,同時,須要在 package.json 的 peerDependencies 字段中聲明你依賴的 ESLint 的版本號。
雖然官方提供了上百種的規則可供選擇,可是這還不夠,由於官方的規則只能檢查標準的 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",
]
}
複製代碼
經過擴展的方式加載插件的規則以下:
extPlugin = `plugin:${pluginName}/${configName}`
複製代碼
對照上面的案例,插件名(pluginName) 爲 react,也就是以前安裝 eslint-plugin-react
包,配置名(configName)爲 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 的擴展,經過這種方式便可以加載插件,又能夠加載擴展。
ESLint 官方爲了方便開發者,提供了 Yeoman 的模板(generator-eslint)。
# 安裝模塊
npm install -g yo generator-eslint
# 建立目錄
mkdir eslint-plugin-demo
cd eslint-plugin-demo
# 建立模板
yo eslint:plugin
複製代碼
建立好項目以後,就能夠開始建立一條規則了,幸運的是 generator-eslint 除了可以生成插件的模板代碼外,還具備建立規則的模板代碼。打開以前建立的 eslint-plugin-demo
文件夾,在該目錄下添加一條規則,我但願這條規則能檢測出個人代碼裏面是否有 console
,因此,我給該規則命名爲 disable-console
。
yo eslint:rule
複製代碼
接下來咱們看看如何來指定 ESLinte 的一個規則:
打開 lib/rules/disable-console.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
};
}
};
複製代碼
簡單的介紹下其中的參數(更詳細的介紹能夠查看官方文檔):
eslint:recommended
在詳細講解如何建立一個規則以前,咱們先來談談 AST(抽象語法樹)。ESLint 使用了一個叫作 Espree 的 JavaScript 解析器來把 JavaScript 代碼解析爲一個 AST 。而後深度遍歷 AST,每條規則都會對匹配的過程進行監聽,每當匹配到一個類型,相應的規則就會進行檢查。爲了方便查看 AST 的各個節點類型,這裏提供一個網站能十分清晰的查看一段代碼解析成 AST 以後的樣子:astexplorer。若是你想找到全部 AST 節點的類型,能夠查看 estree。
能夠看到 console.log()
屬於 ExpressionStatement(表達式語句)
中的 CallExpression(調用語句)
。
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "console"
},
"property": {
"type": "Identifier",
"name": "log"
},
"computed": false
}
}
}
複製代碼
因此,咱們要判斷代碼中是否調用了 console
,能夠在 create 方法返回的對象中,寫一個 CallExpression 方法,在 ESLint 遍歷 AST 的過程當中,對調用語句進行監聽,而後檢查該調用語句是否爲 console
調用。
module.exports = {
create: function(context) {
return {
CallExpression(node) {
// 獲取調用語句的調用對象
const callObj = node.callee.object
if (!callObj) {
return
}
if (callObj.name === 'console') {
// 若是調用對象爲 console,通知 ESLint
context.report({
node,
message: 'error: should remove console'
})
}
},
}
}
}
複製代碼
能夠看到咱們最後經過 context.report
方法,告訴 ESLint 這是一段有問題的代碼,具體要怎麼處理,就要看 ESLint 配置中,該條規則是 [off, warn, error]
中的哪個了。
以前介紹規則的時候,有講到規則是能夠接受配置的,下面看看咱們本身制定規則的時候,要如何接受配置項。其實很簡單,只須要在 mate 對象的 schema 中定義好參數的類型,而後在 create 方法中,經過 context.options
獲取便可。下面對 disable-console
進行修改,畢竟禁止全部的 console 太過嚴格,咱們能夠添加一個參數,該參數是一個數組,表示容許調用的 console 方法。
module.exports = {
meta: {
docs: {
description: "disable console", // 規則描述
category: "Possible Errors", // 規則類別
recommended: false
},
schema: [ // 接受一個參數
{
type: 'array', // 接受參數類型爲數組
items: {
type: 'string' // 數組的每一項爲一個字符串
}
}
]
},
create: function(context) {
const logs = [ // console 的因此方法
"debug", "error", "info", "log", "warn",
"dir", "dirxml", "table", "trace",
"group", "groupCollapsed", "groupEnd",
"clear", "count", "countReset", "assert",
"profile", "profileEnd",
"time", "timeLog", "timeEnd", "timeStamp",
"context", "memory"
]
return {
CallExpression(node) {
// 接受的參數
const allowLogs = context.options[0]
const disableLogs = Array.isArray(allowLogs)
// 過濾掉容許調用的方法
? logs.filter(log => !allowLogs.includes(log))
: logs
const callObj = node.callee.object
const callProp = node.callee.property
if (!callObj || !callProp) {
return
}
if (callObj.name !== 'console') {
return
}
// 檢測掉不容許調用的 console 方法
if (disableLogs.includes(callProp.name)) {
context.report({
node,
message: 'error: should remove console'
})
}
},
}
}
}
複製代碼
規則寫完以後,打開 tests/rules/disable-console.js
,編寫測試用例。
var rule = require("../../../lib/rules/disable-console")
var RuleTester = require("eslint").RuleTester
var ruleTester = new RuleTester()
ruleTester.run("disable-console", rule, {
valid: [{
code: "console.info(test)",
options: [['info']]
}],
invalid: [{
code: "console.log(test)",
errors: [{ message: "error: should remove console" }]
}]
});
複製代碼
最後,只須要引入插件,而後開啓規則便可。
// eslintrc.js
module.exports = {
plugins: [ 'demo' ],
rules: {
'demo/disable-console': [
'error', [ 'info' ]
],
}
}
複製代碼
業界有許多 JavaScript 的推薦編碼規範,較爲出名的就是下面兩個:
同時這裏也推薦 AlloyTeam 的 eslint-config-alloy。
可是代碼規範這個東西,最好是團隊成員之間一塊兒來制定,確保你們都可以接受,若是實在是有人有異議,就只能少數服從多數了。雖然這節的標題叫最佳配置,可是軟件行業並有沒有什麼方案是最佳方案,即便 javascript standard 也不是 javascript 標準的編碼規範,它僅僅只是叫這個名字而已,只有適合的纔是最好的。
最後安利一下,將 ESLint 和 Prettier 結合使用,不只統一編碼規範,也能統一代碼風格。具體實踐方式,請參考個人文章:使用ESLint+Prettier來統一前端代碼風格。
看到這裏咱們作一個總結,JavaScript 的 linter 工具發展歷史其實也不算短,ESLint 之因此可以後來者居上,主要緣由仍是 JSLint 和 JSHint 採用自頂向下的方式來解析代碼,而且早期 JavaScript 語法萬年不更新,能這種方式夠以較快的速度來解析代碼,找到可能存在的語法錯誤和不規範的代碼。可是 ES6 發佈以後,JavaScript 語法發生了不少的改動,好比:箭頭函數、模板字符串、擴展運算符……,這些語法的發佈,致使 JSLint 和 JSHint 若是不更新解析器就無法檢測 ES6 的代碼。而 ESLint 另闢蹊徑,採用 AST 的方式對代碼進行靜態分析,並保留了強大的可擴展性和靈活的配置能力。這也告訴咱們,在平常的編碼過程當中,必定要考慮到後續的擴展能力。
正是由於這個強大擴展能力,讓業界的不少 JavaScript 編碼規範可以在各個團隊進行快速的落地,而且團隊本身定製的代碼規範也能夠對外共享。
最後,但願你經過上面的學習已經理解了 ESLint 帶來的好處,同時掌握了 ESLint 的用法,並能夠爲現有的項目引入 ESLint 改善項目的代碼質量。