ESLint 在中大型團(tuán)隊(duì)的應(yīng)用實(shí)踐
使用背景
代碼規(guī)范是軟件開發(fā)領(lǐng)域經(jīng)久不衰的話題,幾乎所有工程師在開發(fā)過程中都會遇到,并或多或少會思考過這一問題。隨著前端應(yīng)用的大型化和復(fù)雜化,越來越多的前端工程師和團(tuán)隊(duì)開始重視 JavaScript 代碼規(guī)范。
主要解決的問題:
對于獨(dú)立開發(fā)者,或者執(zhí)行力較強(qiáng)、技術(shù)場景較為單一的小型團(tuán)隊(duì)而言,直接使用 ESLint 及其生態(tài)提供的一些標(biāo)準(zhǔn)方案,可以用較低成本來實(shí)現(xiàn) JavaScript 代碼規(guī)范的落地。如果再搭配一些輔助工具(例如 husky 和 lint-staged),整個流程會更加順暢。
ESLint對工程代碼進(jìn)行靜態(tài)檢查,發(fā)現(xiàn)和修復(fù)不符合規(guī)范的代碼。如果想降低配置成本,也可以直接使用開源配置方案,例如 eslint-config-airbnb 或 eslint-config-standard。
一、理解代碼檢查
代碼檢查,顧名思義就是檢查代碼,發(fā)生于開發(fā)階段,可有效幫助開發(fā)者減少 JavaScript 粗心代碼,如語法錯誤、變量未定義、變量未使用等等問題。除此之外,代碼檢查還可以約束統(tǒng)一開發(fā)人員的代碼風(fēng)格,利于團(tuán)隊(duì)協(xié)作。
我們再從三個方面來展開分析,加深對代碼檢查的實(shí)踐理解。
這三個方面分別為以下三點(diǎn):
1)代碼檢查的功能
2)代碼檢查的類型
3)代碼檢查的工具
1、代碼檢查的功能
代碼檢查這個切面大概可以幫助我們做以下三件事情:
- 語言語法檢查:比如檢查出字符串引號或者函數(shù)調(diào)用括號沒有匹配等問題。
- 編碼錯誤檢查:比如檢查出開發(fā)者在使用一個不存在的變量或者變量定義了卻沒有使用等問題。
- 代碼風(fēng)格檢查:比如檢查出開發(fā)者沒有使用分號(與所選風(fēng)格有關(guān))等問題。
2、代碼檢查的方式
以代碼檢查發(fā)生的不同時間和場景來劃分,我把代碼檢查的方式分類成以下四種:
- 編碼時檢查:編寫代碼時檢查,通常表現(xiàn)為由 IDE 自動實(shí)時檢查并進(jìn)行代碼提示。
- 編碼后檢查:編寫代碼后檢查,通常表現(xiàn)為手動調(diào)用檢查腳本 / 工具進(jìn)行代碼的檢查或者代碼保存后由 IDE 自動檢查當(dāng)前文件。
- 構(gòu)建前檢查:構(gòu)建執(zhí)行前檢查,通常表現(xiàn)為將代碼檢查作為構(gòu)建任務(wù)的一個前置切面,構(gòu)建時自動觸發(fā)代碼檢查。
- 提交前檢查:git commit 前檢查,通常表現(xiàn)為將代碼檢查作為 git commit 的一個 hooks 任務(wù),代碼提交前自動觸發(fā)代碼檢查。
理解代碼檢查的方式很重要,這直接反映對代碼檢查這個概念本身的掌握程度。
3、代碼檢查工具
代碼檢查的實(shí)現(xiàn)通常不會僅僅是字符串分析處理,這其中會大量涉及到語法分析。既然涉及到語法,那么就需要對不同的代碼使用不同的代碼檢查工具,通常來說,我們會使用 Eslint 工具來實(shí)現(xiàn)對 JavaScript 和 Typescript 代碼的檢查,使用 stylelint 工具對樣式代碼進(jìn)行代碼檢查。
二、ESLint 特性簡介
目前較為通用的方案——ESLint,它是一款插件化的 JavaScript 代碼靜態(tài)檢查工具,其核心是通過對代碼解析得到的 AST(Abstract Syntax Tree,抽象語法樹)進(jìn)行模式匹配,定位不符合約定規(guī)范的代碼。
1、插件化
下圖簡單地描述了 ESLint 的工作過程:
ESLint 的能力更像一個引擎,通過提供的基礎(chǔ)檢測能力和模式約束,推動代碼檢測流程的運(yùn)轉(zhuǎn)。原始代碼經(jīng)過解析器的解析,在管道中逐一經(jīng)過所有規(guī)則的檢查,最終檢測出所有不符合規(guī)范的代碼,并輸出為報(bào)告。借助插件化的設(shè)計(jì),不但可以對所有的規(guī)則進(jìn)行獨(dú)立的控制,還可以定制和引入新的規(guī)則。ESLint 本身并未和解析器強(qiáng)綁定,我們可以使用不同的解析器進(jìn)行原始代碼解析,例如可以使用 babel-eslint 支持更新版本、不同階段的 ES 語法,支持 JSX 等特殊語法,甚至可以借助 @typescript-eslint/parser 支持 TypeScript 語言的檢查。
2、配置能力全面、可層疊、可共享
ESLint 提供了全面、靈活的配置能力,可以對解析器、規(guī)則、環(huán)境、全局變量等進(jìn)行配置;可以快速引入另一份配置,和當(dāng)前配置層疊組合為新的配置;還可以將配置好的規(guī)則集發(fā)布為 npm 包,在工程內(nèi)快速應(yīng)用。
3、社區(qū)生態(tài)較為成熟
開源社區(qū)中基于 ESLint 的項(xiàng)目非常多,既有針對各種場景、框架的插件,也有各種 ESLint 規(guī)則配置方案,基本可以涵蓋前端開發(fā)的所有場景。
4、規(guī)范配置方案設(shè)計(jì)
基于 ESLint 的插件化、可層疊配置特性,以及面向各種場景、框架的開源方案,如下圖所示的 ESLint 配置架構(gòu):
「美團(tuán)技術(shù)團(tuán)隊(duì)」
該配置架構(gòu)采用了分層、分類的結(jié)構(gòu),其中:
- 基礎(chǔ)層:制定統(tǒng)一的基礎(chǔ)語法和格式規(guī)范,提供通用的代碼風(fēng)格和語法規(guī)則配置,例如縮進(jìn)、尾逗號等等。
- 框架支撐層(可選):提供對通用的一些技術(shù)場景、框架的支持,包括 Node.js、React、Vue、React Native 等;這一層借助開源社區(qū)的各種插件進(jìn)行配置,并對各種框架的規(guī)則都進(jìn)行了一定的調(diào)整。
- TypeScript 層(可選):這一層借助typescript-eslint,提供對 TypeScript 的支持。
- 適配層(可選):提供對特殊場景的定制化支持,例如 MRN(美團(tuán)內(nèi)部的 React Native 定制化方案)、配合 prettier 使用、或者某些團(tuán)隊(duì)的特殊規(guī)則訴求。
*具體各個規(guī)則如何配置可以查看:https://eslint.org/docs/rules
三、為項(xiàng)目接入 Eslint 實(shí)現(xiàn)代碼檢查
Eslint 是一款插件(檢查規(guī)則)化的 JavaScript 代碼檢查工具。概念言簡意賅,需要注意的是,概念中說到 eslint 是一個插件化的檢查工具,其意思是指 eslint 的設(shè)計(jì)是把檢查工具和檢查規(guī)則之間解耦了。也就是說,在安裝好 eslint 的開發(fā)依賴之后,我們還可以并且需要選擇安裝一個我們中意的檢查規(guī)則。
1、安裝檢查工具 eslint
npm install eslint --save-dev
2、安裝并配置檢查規(guī)則
在探討配置安裝檢查規(guī)則之前,我們有必要先明確一下我們的檢查目標(biāo)是什么。我認(rèn)為,檢查目標(biāo)自然是構(gòu)建前的代碼,并且是自己 / 自己團(tuán)隊(duì)編寫的代碼(非第三方模塊)。畢竟檢查的最終目標(biāo)是為修復(fù)服務(wù)的,我們只負(fù)責(zé)修復(fù)自己 / 自己團(tuán)隊(duì)編寫的代碼,構(gòu)建后代碼以及第三方代碼即使檢查不通過我們也不會也不應(yīng)該由我們?nèi)バ迯?fù)。
檢查規(guī)則在項(xiàng)目中通常有兩種表現(xiàn)形式,即:
- 配置文件中配置的規(guī)則:主要形式,通過繼承和擴(kuò)展的方式聲明了大量規(guī)則;
- 項(xiàng)目代碼中的魔法注釋:次要形式,通常是用于作為配置文件中規(guī)則的特例;
生成 eslint 配置文件:
對于配置文件,我們通常會使用 npx eslint --init 生成一個 ESLint 配置文件 .eslintc.js,如下示例:
module.exports = {
root: true,
env: {
browser: true,
es2021: true
},
extends: [
'eslint:recommended',
'plugin:vue/essential',
'standard'
],
parser: require.resolve('vue-eslint-parser'),
parserOptions: {
ecmaVersion: 12,
sourceType: 'module'
},
plugins: [
'vue'
],
rules: {
indent: ['off', 2]
}
}
代碼中的魔法注釋寫法:
除了配置文件中配置規(guī)則,eslint 還有一個代碼中通過魔法注釋打規(guī)則補(bǔ)丁的辦法,如下示例:
// 屏蔽整行的代碼檢查
const str1 = "${name} is a coder" // eslint-disable-line
// 屏蔽某一個規(guī)則:如此行的no-template-curly-in-string規(guī)則
const str1 = "${name} is a coder" // eslint-disable-line no-template-curly-in-string
*擴(kuò)展:ESLint 禁用檢查
1.禁用代碼塊
/* eslint-disable */
consle.log("foo");
consle.log("bar");
/* eslint-disable */
2.禁用單行(放在該行代碼后面)
consle.log("foo"); // eslint-disable-line
3.禁用下一行
// eslint-disable-next-line
console.log("foo")
4.禁用文件(放在代碼最頂部)
/* eslint-disable */
consle.log("foo");
consle.log("bar");
四、Eslint 編碼后檢查 JS 代碼
對于一個工作流程的解釋,我還是更傾向于直接演示一個簡單的 demo。根據(jù)代碼檢查的邏輯,demo 演示和講解時我會遵循以下思路,即:
- 目標(biāo)問題代碼
- 代碼檢查規(guī)則配置
- 代碼檢查操作和結(jié)果
- 修復(fù)代碼操作
1、目標(biāo)問題代碼
/* eslint-disable */
const noUsedVar = 1;
function fn() {
console.log('hello')
cnsole.log('eslint');
}
fn(
fn2();
以上短短幾行代碼就可以表示出 eslit 代碼檢查的三個部分,它們分別是:
- 語法錯誤
- 編碼錯誤:未定義、未使用
- 編碼風(fēng)格:沒有分號
2、代碼檢查規(guī)則配置
通過 eslint --init,根據(jù)項(xiàng)目特征來回答問題之后得到的 eslint 配置文件如下:
/* eslint-disable */
module.exports = {
env: {
browser: true,
es6: true,
},
extends: [
'airbnb-base',
],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
},
parserOptions: {
ecmaVersion: 2018,
},
rules: {
},
};
3、代碼檢查操作和結(jié)果
第一輪檢查結(jié)果先是報(bào)了語法錯誤,在修復(fù)語法錯誤之后,第二輪檢查報(bào)錯了很多編碼以及風(fēng)格上的錯誤。將兩次檢查結(jié)果關(guān)聯(lián)到問題代碼可以得到如下分析:
const noUsedVar = 1; // find program:'noUsedVar' is assigned a value but never used
function fn() {
console.log('hello') // enforce code style:Missing semicolon(分號)
cnsole.log('eslint'); // find program:'cnsole' is not defined
}
fn( // syntax error
fn2(); // find program:'fn2' is not defined
4、修復(fù)代碼
根據(jù)上述檢查結(jié)果進(jìn)行修復(fù)。對于代碼風(fēng)格上的不一致導(dǎo)致的錯誤,通過參數(shù) --fix 即可以自動修復(fù)大部分的問題。而對于語法以及編碼上的錯誤則大部分只能是開發(fā)者自己手動修復(fù)。經(jīng)過手動修復(fù)以及自動修復(fù)之后,問題代碼可能變?yōu)槿缦履樱?/p>
*執(zhí)行代碼:npx eslint --fix 相關(guān)地址
import { fn2 } from './test2';
function fn() {
console.log('hello');
console.log('eslint');
}
fn();
fn2();
五、Eslint 規(guī)則實(shí)現(xiàn)git 提交前檢查
探討 Eslint 如何實(shí)現(xiàn) git 的提交前檢查:
- 實(shí)現(xiàn) hook 任務(wù)流:通過 lint-staged 來配合 husky 來實(shí)現(xiàn)
- 實(shí)現(xiàn) git 提交前檢查:先執(zhí)行 eslint 任務(wù)而后執(zhí)行 git add 任務(wù)
下面我們進(jìn)入第一點(diǎn),git 提交前檢查原理:Git Hooks 的探討。
1、實(shí)現(xiàn) hook 任務(wù)流:通過 lint-staged 來配合 husky 來實(shí)現(xiàn)
實(shí)現(xiàn)步驟如下:
- 安裝 husky 和 lint-staged 模塊
- 配置 husky 的 hook 任務(wù)流:如下 package.json 任務(wù)
- 觸發(fā)任務(wù)流:git add -> git commit
1) 安裝 husky 和 lint-staged 模塊
yarn add husky --dev
yarn add lint-staged --dev
2) 配置 husky 的 hook 任務(wù)流:如下 package.json 任務(wù)
"scripts": {
"precommit": "lint-staged"
},
"husky": {
"hooks": {
"pre-commit": "yarn precommit"
}
},
"lint-staged": {
"*.js":[
"eslint --fix",
"git add"
]
}
3) 觸發(fā)任務(wù)流:git add -> git commit
實(shí)踐發(fā)現(xiàn),與單獨(dú)的 husky 模塊實(shí)現(xiàn)單任務(wù)相比而言,使用 lint-staged 之后,git commit 命令只有成功執(zhí)行(有 add 資源并且有提交信息)才會觸發(fā) lint stage 中的操作,且這些操作只會對 js 文件有效。
2、實(shí)現(xiàn) git 提交前檢查:先執(zhí)行 eslint 任務(wù)而后執(zhí)行 git add 任務(wù)
實(shí)現(xiàn)步驟如下:
- 安裝 husky 和 lint-staged 模塊
- 配置 husky 的 hook 任務(wù)流:如下 package.json 任務(wù)
- 觸發(fā)任務(wù)流:git add -> git commit
1): 安裝 husky 和 lint-staged 模塊
yarn add husky --dev
yarn add lint-staged --dev
2): 配置 husky 的 hook 任務(wù)流:如下 package.json 任務(wù)
"scripts": {
"precommit": "lint-staged"
},
"husky": {
"hooks": {
"pre-commit": "yarn precommit"
}
},
"lint-staged": {
"*.js":[
"eslint --fix",
"git add"
]
}
3) 觸發(fā)任務(wù)流:git add -> git commit
經(jīng)過這些開發(fā)包的下載以及配置,在我們執(zhí)行 git commit 之后,就會觸發(fā) husky 配置的 pre-commit 的 hook 任務(wù),而這個 hook 任務(wù)又把任務(wù)交給了 lint-staged 處理,進(jìn)而通過 lint-staged 實(shí)現(xiàn)對 js 文件的代碼檢查以及自動風(fēng)格修復(fù)后(錯誤則會中斷提交)重新 add,而后再執(zhí)行 commit 任務(wù),保證了代碼在提交前一定經(jīng)過了代碼檢查。