作者:Morrain
【前端科普系列】幫助閱讀者了解web前端,主要覆蓋web前端的基礎知識,但不深入講解,定位為大而全并非細而精,適合非前端開發的同學對前端有一個系統的認識,能更好的與前端開發協作。盡可能的寫成科普類文章,對于前端開發而言,只適合剛入門的新手。
本文為第五章,主要講前端工程化中的很重要工具 ESLint,主要介紹 ESLint 的歷史、用法以及如何基于ESLint 打造保護代碼倉庫優雅的護城河。
一、前言
戰國時期強大的趙國想要一舉攻打并吞并北邊的燕國,而小國“梁城”位居兩國之間,為戰略要沖,是必取之地。于是趙國派遣大將巷淹中,率領十萬大軍攻打僅有四千人的“梁城”,梁城王向以守城著稱的墨家求救。但梁城等到的卻是一個其貌不揚、孤身應戰的墨家俠客革離,誰料革離足智多謀,指揮梁城四千軍民抵御十萬趙軍,功成身退。
(圖片來源:網絡)
“梁城”就好比我們的項目倉庫,“梁城”的秩序取決于“革離”有沒有守好它!那我們的項目倉庫呢? 你愿意看到城中雜亂無章、毫無規則、亂象叢生還是愿意看到城中秩序井然、風格統一、整齊有序?如何成為程序員里的“革離”,守好屬于我們的天空之城?
二、關于 ESLint
1、ESLint 是什么
先來看下它在官網上的定義:
Find and fix problems in your JavaScript code
沒錯就一句話,發現并修復你 JavaScript 代碼中的問題!
ESLint 最初是由 Nicholas C. Zakas 于 2013 年 6 月創建的開源項目。它的目標是提供一個插件化的 JavaScript 代碼檢測工具。
那為什么需要 JavaScript 代碼檢查工具呢?還是從 JavaScript 的語言特性說起。
2、lint 工具進化史
在 C 語言發展初期,源程序中存在很多不可移植的代碼,但卻不能被編譯器識別,因此貝爾實驗室 SteveJohnson 于 1979 年在PCC(PortableC Compiler)基礎上開發了一個靜態代碼分析的工具,用來掃描 C 源文件并對源程序中不可移植的代碼提出警告,這個工具被起名為 lint ,也因此后續類似的檢查代碼的工具都叫 xxLint。
我們在《前端科普系列(1):前端簡史》中講過 JavaScript 語言的歷史,有提到 JavaScript 是在 1995年4月,由 Netscape 公司雇傭的程序員 Brendan Eich 開發的網頁腳本語言,目的是嵌入到網頁中在提交前做一些簡單的校驗。
Brendan Eich 只用了10天,就設計完成了這種語言的第一版,估計作者也不曾想過 JavaScript 這門語言會發展到今天這個地步,所以在當初設計時存在非常多的不合理的地方。
于是就需要代碼校驗工具來分析使用不當的地方,JSlint 就應運而生,在 JavaScript 語言 lint 工具進化史中,有三個里程碑式的工具:JSLint、JSHint 和 ESLint。
(1)****開山鼻祖 JSLint
2008年,有一本非常著名的書《JavaScript語言精粹》出版,因為封面圖是一個蝴蝶,所以俗稱"蝴蝶書"。非常薄的一本書,是適合所有入門 JavaScript 必讀并且要讀很多遍的一本書。這本書的作者叫 Douglas Crockford。
[圖片上傳失敗...(image-208923-1603850391450)]
從《JavaScript語言精粹》的書名就可以看出來,全書悉數了 JavaScript 語言的優美特性,同時在書籍的最后面,作者也毫不客氣的列舉了 JavaScript 的糟粕和雞肋的地方,從書籍中的筆風就能看出,Douglas 是個眼里容不得瑕疵的人,于是在書籍最后也介紹了作者在 2002 年開發的 JSLint 工具,Douglas 定義了所有 JSLint 的規則,對于糟粕的語法是嚴格不讓使用的,如果你要使用JSLint,就必須接受它所有規則。
(2)****繼往開來 JSHint
2011 年 12 月 20 日,Anton Kovalyov 發表了一篇標志性的文章《Why I forked JSLint to JSHint》,指出了 JSLint 存在的幾個主要問題:
令人不安地固執己見,沒有提供一些規則的配置
對社區反饋不關注
[圖片上傳失敗...(image-bbf6b0-1603850391450)]
于是 JSHint 就誕生了,它在 JSLint 的基礎上,在社區開發者共同努力下,加入了如下特性:
更多可配置的規則,這是社區的核心訴求
代碼模塊化
命令行工具的支持,很好得和各種 IDE 集成
諸多優勢,讓 JSLint 迅速取代 JSHint 成為一種必然。
(3)****重新出發 ESLint
JSLint 是從 JSHint 繼承而來,所以沿用了JSLint Top Down Operator Precedence(自頂向下的運算符優先級)技術實現源碼的解析,但前端爆發式增長帶來的巨大需求讓 JSHint 變得愈加難以應對,通過暴露 AST 信息來支持第三方插件無疑是一劑良方。
AST:抽象語法樹
[圖片上傳失敗...(image-b89cce-1603850391450)]
于是《JavaScript 高級程序設計》作者 Nicholas C. Zakas 于 2013 年 6 月創建了 ESLint,ESLint 將源代碼解析成 AST,然后檢測 AST 來判斷代碼是否符合規則,為 ESLint 的高可擴展性奠定了結實的基礎。ESLint 保持內核足夠簡單,只有 100 行代碼,規則的實現完全和內核分離。
但是,那個時候 ESLint 并沒有大火,因為需要將源代碼轉成 AST,運行速度上輸給了 JSHint ,并且 JSHint 當時已經有完善的生態(編輯器的支持)。真正讓 ESLint 大火是因為 ES6 的出現。
參考 《前端科普系列(1):前端簡史》中 ES6 的相關內容。
ES6 發布后,因為新增了很多語法,JSHint 短期內無法提供支持,而 ESLint 只需要有合適的解析器就能夠進行 lint 檢查。這時 Babel 為 ESLint 提供了支持,開發了 babel-eslint,讓 ESLint 成為最快支持 ES6 語法的 lint 工具。
三、ESLint 怎么用
了解了 ESLint 工具的歷史意義和發展過程,接下來看下 ESLint 到底怎么用?
1、給項目安裝 ESLint
先決條件:Node.js (>=6.14), npm version 3+。
// 新建demo工程目錄,初始化 npm 項目
npm init
// 安裝 eslint 推薦安裝為項目的開發依賴
npm i -D eslint
// 初始化 eslint 配置文件 因為不是安裝到全局的,所以不能直接使用 eslint --init
./node_modules/.bin/eslint --init
在初始化的過程中,會讓你選擇一些配置,譬如 如何使用 ESLint?我們選擇第三項,功能最多。
逐一選擇完 ESLint 的使用配置后,會在項目根目錄生成 .eslintrc.js 配置文件,同時會安裝需要的 npm 包。demo 中安裝的 npm 包有:eslint-config-standard、eslint-plugin-import、eslint-plugin-node、eslint-plugin-promise、eslint-plugin-standard
demo 中選擇如下所示:
初始化后,生成的配置內容如下所示,具體配置項的含義,后面我們再聊。
// .eslintrc.js
module.exports = {
env: {
es2020: true,
node: true
},
extends: [
'standard'
],
parserOptions: {
ecmaVersion: 11,
sourceType: 'module'
},
rules: {
}
}
需要強調的是在選擇代碼風格時,我選擇了比較流行的standard規范。
接下來我們就可以使用 ESLint 來檢查和修復代碼了。首先在 demo 項目中,新建 src 目錄,并新建 index.js 文件,內容如下:
// src/index.js
let a = 10;
let b = 15;
let sum = a + d;
console.log(sum);
同時,在 package.json 中,增加 eslint 命令 eslint src/** 來檢查 src 目錄下的所有文件。
//package.json
"scripts": {
"eslint": "eslint src/**"
}
在 demo 目錄執行 npm run eslint 結果如下:
可以看到,檢查出來了如此多的錯誤,其中‘let 要使用 const 替換’,‘不能使用封號’等屬于 standard 規范中指定的規則,除了風格外,還檢查出了‘未定義的變量’等語法錯誤,并逐一給出提示。
如果想自動修復檢查出來的問題,怎么辦呢?eslint 支持使用 --fix 參數。修改 package.json 中的 eslint 命令為 eslint src/** --fix
再次執行 npm run eslint 結果如下:
打開 src/index.js 文件發現,內容已經發生了改變:
// src/index.js
const a = 10
const b = 15
const sum = a + d
console.log(sum)
可以看到,ESLint 自動修復了可以被修復的風格問題,同時對于不能被自動修復問題給出提示。
更多 eslint cli 配置參數,請參考官網cli的詳細介紹。
到這里我們已經‘挖好了護城河’,可是河里并沒有水,敵人想要過來依然可以暢行無阻。完全依賴開發人員自覺手動運行 npm run eslint 來完成,那怎么樣才能讓讓‘護城河’真正發揮作用呢?我們先看下 ESLint 常見的配置含義,然后在 如何守住優雅的護城河詳細介紹。
2、常用配置規則
剛才在初始化之后,在項目根目錄生成了 .eslintrc.js 文件,這里存放了所有 eslint 的配置項。
module.exports = {
env: {
es2020: true,
node: true
},
extends: [
'standard'
],
parserOptions: {
ecmaVersion: 11,
sourceType: 'module'
},
rules: {
}
}
(1)環境與全局變量
demo 中的 env 配置就是為相應的環境定義了一組預定義的全局變量。從之前的例子中我們已經看到,ESLint 會檢測出來未定義的變量并報錯,但有一些是運行環境或者框架提供的全局變量,譬如 jQuery 提供的 $,此時有如下幾種解決方案:
- 在你的 JavaScript 源碼文件中,用注釋指定全局變量,格式如下:
/* global $ */
const dom = $('id')
- 在配置文件中配置全局變量,將 globals 配置屬性設置為一個對象,該對象包含以你希望使用的每個全局變量。對于每個全局變量,將對應的鍵值設置為 "writable" 以允許重寫變量,或 "readonly" 不允許重寫變量。例如:
{
"globals": {
"$": "readonly"
}
}
- 使用 env 配置。為了避免上面兩種方案需要一一配置每一個全局變量的麻煩,ESLint 預設了好多環境全局變量集合,譬如我們要使用 jQuery 提供的全局變量,只要需要在 env 配置中添加 jquery:true就可以了。
demo 中 env 的配置,es2020:true 表示增加了 es2020 的語法特性,node:true 表示增加 node 中所有的全局變量。更多的環境可以參考官網 指定環境 相關章節。
(2)擴展
在 .eslintrc.js 中 rules 用來配置 ESLint 的規則,具體配置規則的方法請參考官網 如何配置規則 以及 所有規則的說明,這里不作詳細介紹,同樣為了方便使用,ESLint 使用 extends 配置來一次性生效一整套規則。譬如 demo 中 rules 中沒有配置任何規則,因為通過 extends 配置了符合 standard 規范的規則集合。
ESLint支持三種類型的擴展:
eslint:' 開頭的 ESLint 官方擴展。
包括 eslint:recommended 和 eslint:all,其中 eslint:recommended是推薦的一套規則,eslint:all 是 ESLint 中的所有規則,不推薦使用,因為可能隨時被 ESLint 更改。
共享的擴展。
通過 npm 包提供一套共享的配置,包名前綴必須為 eslint-config-,extends 屬性值可以省略包名的前綴 eslint-config-。demo 中 stanard 對應的就是 package.json 中 'eslint-config-standard' 這個包提供的一套規則。
插件中提供的擴展。
在 demo 初始化時,我們可以看到 eslint-plugin-node 等插件包被安裝,這些插件包是 eslint-config-standard 的依賴,所以會被自動安裝,這些插件包也提供了一些規則可供擴展。
譬如如下代碼在 node 的模塊中寫法是錯誤的,應該寫成 module.exports,如果想要 ESLint 能檢查出這個錯誤,就需要增加 eslint-plugin-node 包中提供的規則到擴展中。
// src/index.js
exports = {
foo: 1
}
// .eslintrc.js
extends: [
'standard',
'plugin:node/recommended'
]
由于 eslint-plugin-node 已經被默認安裝了,所以不需要再單獨安裝,對于沒有安裝的插件包,如果想使用它提供的規則,需要手動安裝這個插件包。
(3)插件
上面講擴展時,已經提到了如何加載插件中的擴展配置。既然已經有了這么多擴展可以使用,為什么還需要插件呢?因為 ESLint 只能檢查標準的 JavaScript 語法,如果你使用 Vue 單文件組件, ESLint 就束手無策了。這個時候,相應框架就會提供配套的插件來定制特定的規則進行檢查。插件和擴展類似,也有固定的前綴 eslint-plugin-,配置時也可以省略前綴。
我們新加一個 Vue 的單文件組件如下,執行 npm run eslint 后發現沒有效果,并不能檢查 .vue 中的錯誤,此時就需要安裝 eslint-plugin-vue 插件。
// src/index.vue
<script>
let a = 10
const b = 15
const sum = a + d
console.log(sum)
</script>
npm i -D eslint-plugin-vue
安裝后在 .eslintrc.js 中配置插件,有兩種方式:
- 通過 plugins 配置
module.exports = {
plugins: ['vue']
}
配置完成執行 npm run eslint 發現并沒有檢查 src/index.vue 文件,原來 plugins: ['vue'] 只是聲明想要引用的 eslint-plugin-vue 提供的規則,但是具體用哪些,怎么用,還是需要在 rules 中逐一配置。所以一般我們使用第二種方式配置插件。
- 通過 extends 配置,即上述的配置擴展的方式。
module.exports = {
extends: ['plugin:vue/recommended']
}
通過這種方式既加載了插件又指定了使用插件提供的規則,已經能成功檢查 vue 文件中上的代碼,如下圖所示:
(4)解析器及其配置
module.exports = {
parserOptions: {
ecmaVersion: 11,
sourceType: 'module'
}
}
demo 中 parserOptions 為解析器配置,ESLint 默認只支持 ES5 的語法,但可以通過解析器配置來設置支持的 ES 版本,譬如 demo 中的 ecmaVersion:11 表示支持 ES11(即ES2020) 的語法,這里需要注意的是通過解析器配置只是支持語法,對于該版本新增加的全局變量依然要通過 env 配置來完成支持,相關說明以及更多的解析器配置請參考官網 指定解析器配置。
ESLint 默認是使用ESPree作為其解析器的,但也可以通過 parser 字段指定一個不同的解析器,可以參考官網 指定解析器。
那為什么需要指定解析器呢?因為 ESLint 默認的解析器只支持已經形成 ES 標準的語法特性,對于處于實驗階段以及非標準的(譬如 Flow、TypeScript等)需要使用 Babel 轉換的語法,就需要指定由 Babel 提供的 @babel/eslint-parser了。由此可見,正常情況下,是不需要指定第三方的解析器的。
以 @babel/eslint-parser 為例,當指定它作為 ESLint 的解析器后,我們開發的源碼首先由 @babel/eslint-parser 根據 Babel 的配置(參考《前端科普系列(4):Babel——把 ES6 送上天的通天塔》) 把源碼轉化為 ESLint 默認支持的 AST,并保持住源碼的行列數,方便輸出錯誤的定位。光指定 @babel/eslint-parser 還不夠,解析器的作用只是負責把 ESLint 不能識別的語法特性轉化為 ESLint 能識別的,但它本身不包括規則,還需要使用 @babel/eslint-plugin 插件來提供對應的規則,才能正常執行 ESLint 對代碼的檢測。
更多詳情,請參考 @babel/eslint-parser官方文檔。
至此,常見的配置已經介紹完了,更多配置介紹,請參考 ESLint 官網文檔 配置 ESLint。
四、如何守住優雅的護城河
前面也提到了到目前為止,我們只是‘挖好了護城河’,可是河里并沒有水,敵人想要過來依然可以暢行無阻。源碼檢測完全依賴開發人員自覺手動運行 npm run eslint 來完成,那怎么樣才能讓讓‘護城河’真正發揮作用呢?
1、享受開發時的樂趣
首當其沖的需求就是在開發的過程中最好就能做代碼檢測,而不是需要代碼開發完成后,運行 npm run eslint 才能看到錯誤,此時可能已經一堆錯誤了。
以 VS Code 編輯器為例(其它編輯器應該也有類似的插件),安裝ESLint 擴展插件 。該編輯器插件會讀取當前項目中的 .eslintrc.js 的配置,并在編輯器中把不符合規則的錯誤給提示出來。
首先可以看到目錄樹上,有問題的文件變紅,點開這個文件,對應的行上也會有錯誤提示,鼠標停留會提示錯誤的信息方便修復。但眼尖的同學可能已經發現了,運行 npm run eslint 不光能檢測 index.js 中的錯誤,還能檢測 index.vue 中的錯誤,一共是 7 個錯誤。而編輯器只檢測了 index.js 的錯誤。
原來是編輯器的 ESLint 插件默認只能檢測 .js 的文件,需要調整編輯器 ESLint 插件的設置,讓它支持 .vue 文件的檢測。
如上圖所示:
找到插件的設置入口。
因為增加對 vue 文件的支持是針對項目的,并不是所有項目都是 vue 項目,所以我們把設置生效到工作區。
在 Validata 配置中增加 vue。
可以看到,index.vue 文件也已經變紅,里面的錯誤也能夠被檢測了,并且在編輯器的“問題”欄也能顯示項目所有的 7 條錯誤,和運行 npm run eslint 效果一樣了。
這樣一來,開發時就能有錯誤提示,根據提示修改就好了,但我們之前提到運行 npm run eslint 可以通過 --fix 參數來自動修復可以修復的問題,譬如格式問題,let 改成 const 等這些問題。
那在開發時,是否也可以對于檢測出來的錯誤自動修復呢?
三種方案,可以根據自喜好選擇:
- 設置保存時自動修復。
- 調出 VS Code 編輯器的命令面板,找到 ESLint 插件提供的修復命令。
- 將 ESLint 插件提供的修復命令設置成喜歡的快捷鍵,使用快捷鍵修復。
此時我們發現,自動修復又是只針對 index.js 文件生效,同樣的依然要配置 ESLint 的插件,使其支持 Vue 文件的自動修復功能。
2、將樂趣進行到底
現在我們已經能做到了在開發時檢測出來錯誤并且方便開發人員及時修復問題,但這依賴于開發同學自覺,如果開發同學不自覺或者忘記了,此時提交代碼就依然會把錯誤的代碼提交到倉庫中去。此時我們需要借助husky來攔截 git 操作,在 git 操作之前再進行一次代碼檢測。
[圖片上傳失敗...(image-ba7dcb-1603850391450)]
(圖片來源:網絡)
- 安裝并配置 husky
npm i -D husky
// package.json
{
"husky": {
"hooks": {
"pre-commit": "eslint src/** --fix"
}
}
}
此時提交前會檢查 src 下的所有文件,由于 demo 中源碼是有問題的,ESLint 檢查通不過,所以現在無法提交,從而阻斷開發忘記修復 ESLint 檢查出來問題的情況。
那對于老的項目,可能已經存在很多遺留的風格問題,導致 ESLint 檢查通不過,此時又不可能把所有問題都一一修復掉,全盤阻止提交勢必會造成影響。另外對于單次提交而言,如果每次都檢查 src 下的所有文件,也是沒有必要的。所以我們需要使用lint-staged工具只針對當前修改的部分進行檢測。
- 安裝并配置 lint-staged
npm i -D lint-staged
// package.json
{
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,vue}": [
"eslint --fix",
"git add"
]
}
}
具體配置請參考 lint-staged 官網文檔。示例中配置表示的是,對當前改動的 .js 和 .vue 文件在提交時進行檢測和自動修復,自動修復完成后 add 到 git 暫存區。如果有無法修復的錯誤會報錯提示。
3、安裝“黑匣子”
飛機上都裝有黑匣子,當出現故障時,可以很方便的回溯航行記錄,發現問題。我們的代碼倉庫也一樣,每次提交都應該有記錄。但每個開發同學提交時輸入的信息各不一樣,沒有統一的格式,導致后面回溯提交記錄時眼花繚亂,效率很低。
接下來看下,如何約束提交,來守住優雅得提交日志這道大門。
[圖片上傳失敗...(image-852348-1603850391450)]
commitizen是用來格式化 git commit message 的工具,它提供了一種問詢式的方式去獲取所需的提交信息。
cz-conventional-changelog是用來規定提交時需要輸入哪些信息,譬如提交的類型是修復問題還是功能開發,提交影響范圍等等,cz-conventional-changelog 是官網提供的規則,完全可以根據項目實際情況自已開發適合的規則。
standard-version提交信息并約束后,提交的日志信息就會比較統一,使用 standard-version 很容易自動生成提交的日志 CHANGELOG 文件。
- 安裝并配置
npm i -D commitizen cz-conventional-changelog standard-version
//package.json
{
"scripts": {
"c": "git-cz",
"version": "standard-version"
},
"standard-version": {
"changelogHeader": "# Changelog\n\n所有項目的變更記錄會記錄在如下文件.\n",
"dryRun": true
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
}
}
配置完成后,使用 npm run c 來提交修改,會如現如下圖所示的問詢式的交互提示,根據規則要求填寫對應的內容就好了。
通過此方式提交,提交的日志是格式統一的,然后就是使用 npm run version 來生成 CHANGELOG 文件了。
standard-version 會自動 bump 項目的版本號,并生成兩個版本之間的提交日志記錄文件,然后打個版本 tag 上傳到倉庫。更多關于 standard-version 的功能請參考官網文檔standard-version。
4、最后一道防線
現在開發如果使用 npm run c 來提交修改,那么一切都會非常美好,可是萬一開發忘了使用 npm run c 來提交修改,直接使用 git 命令,或者其它工具提交改動,怎么辦?如何守好最后一道防線?
答案就是在提交時對提交信息進行校驗,如果不符合要求就不讓提交,并提示。校驗的工作由commitlint來完成,校驗的時機則由husky來指定。husky 繼承了 Git 下所有的鉤子,在觸發鉤子的時候,husky 可以阻止不合法的 commit,push 等等。
- 安裝并配置
// 安裝工具包
npm install --save-dev @commitlint/{config-conventional,cli}
// 生成 commitlint 配置文件
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
@commitlint/cli 用來執行檢查,@commitlint/config-conventional 是檢查的標準,即提交的信息是否符合這個標準的要求,只有符合要求才允許提交。
生成配置文件,指定要使用的規范,同時增加 husky 中的 'commit-msg' 鉤子。配置完成后,再通過非 npm run c 途徑的提交都會被攔截并報錯。
// package.json
{
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}
}
- 錯誤提交時的校驗
五、總結
基于 ESLint ,我們成功化身為程序員界的‘革離’,守好了我們的戰場,讓屬于我們的天空之城干凈純粹、整齊劃一,在優雅里翱翔!
六、參考文獻
書籍:《JavaScript語言精粹》