構(gòu)建可靠的前端項目 - 少不了這些必備工具集
構(gòu)建可靠的前端項目少不了這些必備工具集: ESLint、Prettier、Editorconfig、Husky、lint-staged、commitlint,它能幫助我們約束編碼風(fēng)格與提及規(guī)范,下文介紹這些工具集的使用,文末提供一些可參考的項目。
ESLint 代碼質(zhì)量檢測
ESLint 是 JavaScript/TypeScript 編程中一個最受歡迎的代碼規(guī)范檢查工具,可避免一些由于低級的代碼錯誤而導(dǎo)致的項目崩潰,另一方面也能規(guī)范我們的編碼習(xí)慣。
ESLint 基本使用
為項目開發(fā)時依賴添加 eslint 插件 npm i eslint -D,緊接著我們需要設(shè)置一個配置文件,如果沒有全局安裝 eslint 插件,可能就需要這樣去做 ./node_modules/.bin/eslint --init,我不喜歡這樣,好在還有 npx 這個工具,我們可以這樣做 npx eslint --init。
執(zhí)行 --init 命令后,可以選擇使用的框架、模塊類型、語言(JS/TS)等,隨后會生成一個 .eslint{yml|js|json} 文件。
$ npx eslint --init
? How would you like to use ESLint? · problems
? What type of modules does your project use? · esm
? Which framework does your project use? · react
? Does your project use TypeScript? · No / Yes
? Where does your code run? · browser
? What format do you want your config file to be in? · YAML
ESLint 支持 JavaScript、JSON 或者 YAML 等多種文件格式,這里使用的是 YAML 格式,相比于 JSON 可以寫一些注釋。
env:
browser: true
es2021: true
extends:
- eslint:recommended
- plugin:react/recommended
- plugin:@typescript-eslint/recommended
parser: '@typescript-eslint/parser'
parserOptions:
ecmaFeatures:
jsx: true
ecmaVersion: 13
sourceType: module
plugins:
- react
- '@typescript-eslint'
rules: {}
對上述文件幾個配置做一些簡單介紹:
- extends:這是一個擴展,可以集成一些社區(qū)的最佳實踐,在其基礎(chǔ)之上做一些自定義配置,下文我們介紹如何使用 airbnb。
- plugins:這里是用來加載第三方插件,在使用之前必須 NPM 安裝它。
- parserOptions:指定想要支持的 JavaScript 語言
jsx:啟用 JSX
ecmaFeatures:想使用的額外的語言特性
- rules:指定了代碼檢查的規(guī)則,參考 ESLint 官網(wǎng) Rules[1], 規(guī)則分為幾個等級,用來表示嚴重程度:
關(guān)閉:'off' 或 0,
警告級別:'warn' 或 1,不會導(dǎo)致程序退出
錯誤級別:'error' 或 2,會導(dǎo)致程序退出。
有了以上配置后,就可以執(zhí)行 npx eslint ./your_file_or_directory 命令檢測指定的文件或目錄。
忽略特定文件或目錄
在項目根目錄創(chuàng)建 .eslintignore 文件告訴 ESLint 忽略掉某些特定的文件或目錄。忽略模式可以參照 .gitignore 規(guī)范[2]。
# dependencies
/node_modules
# testing
/coverage
# production
/build
站在巨人的肩膀上 - 制定編碼規(guī)范
怎么設(shè)計一套好的編碼規(guī)范呢?也無需重頭造輪子,借助一些社區(qū)開源的編碼規(guī)范最佳實踐,可以直接引入在我們的項目中,再根據(jù)自己的需求做一些定制。
我們這里以 Airbnb 為基礎(chǔ),做一些自定義的修改,參考 Airbnb JavaScript 風(fēng)格指南[3]。
安裝 eslint-config-airbnb[4] npm i eslint-config-airbnb -D,如果你不需要 React,可以參考 eslint-config-airbnb-base[5] 這個包,在 Node.js 服務(wù)端可以使用這個包。之后添加 "extends": "airbnb" 到你的 .eslintrc 文件。
env:
browser: true
es2021: true
extends:
- airbnb
- plugin:@typescript-eslint/recommended
- plugin:react/jsx-runtime # React v17 需添加
parser: '@typescript-eslint/parser' # 解析 TS
parserOptions:
ecmaFeatures:
jsx: true
ecmaVersion: 13
sourceType: module
plugins:
- react
- '@typescript-eslint'
settings:
import/resolver:
node:
# https://github.com/import-js/eslint-plugin-import#resolvers
extensions:
- ".js"
- ".jsx"
- ".ts"
- ".tsx"
rules: {
# "@typescript-eslint/explicit-module-boundary-types": 2,
no-console: 0,
# https://stackoverflow.com/questions/55614983/jsx-not-allowed-in-files-with-extension-tsxeslintreact-jsx-filename-extensio
react/jsx-filename-extension: [
2,
{
extensions: ['.js', '.jsx', '.ts', '.tsx']
}
],
import/extensions: 0,
# https://stackoverflow.com/questions/69928061/struggling-with-typescript-react-eslint-and-simple-arrow-functions-components
react/function-component-definition: 0,
comma-dangle: 0
}
IDE 自動提示
每改動代碼都運行 npx eslint ./ 命令執(zhí)行代碼檢測,效率還是有點低效的,我們希望改動代碼后,IDE 能夠給我們自動提醒格式錯誤的代碼片段。
在 VS Code 中,需要在應(yīng)用商店安裝 ESLint 插件。
圖片
當(dāng)我沒有遵守 ESLint 規(guī)范去修改代碼后,編輯器會及時給我們一些錯誤提示。
圖片
Prettier 代碼格式化
**在使用 ESLint 時,另一個被搭配使用的工具是 ****Prettier**[6],這是一個專業(yè)的代碼格式化工具,支持 JS、TS、HTML、CSS 等眾多語言 。
Prettier 基本使用
開發(fā)時依賴安裝 Prettier,并創(chuàng)建一個空的配置文件,讓編輯器和其它工具知道正在使用 Prettier。
$ npm install --save-dev --save-exact prettier
$ echo {}> .prettierrc.json
創(chuàng)建 .prettierignore 文件,讓編輯器和 Prettier CLI 知道哪些文件不需要格式化,它建立在 .gitignore .eslintignore 之上。
$ touch .prettierignore
# Ignore artifacts:
build
coverage
手動觸發(fā)
命令行執(zhí)行 **npx prettier --write .** 手動觸發(fā),對于一個大項目來說可能會花費一些時間,因此還可以指定目錄或文件來格式化。
npx prettier --write . # 格式化當(dāng)前目錄下的所有文件
npx prettier --write src/ # 格式化指定目錄下的所有文件
npx prettier --write src/App.tsx # 格式化指定的文件
有了 ESLint 為什么還要使用 Prettier
ESLint 提供了 eslint --fix 命令用來自動修復(fù)代碼格式問題,為什么還要用 Prettier 呢?
如果我說在代碼格式化方面 Prettier 比 ESLint 更專業(yè),你可能也不會信服,畢竟沒有證據(jù)來證明這一點。下面我將通過一個例子來驗證。
下面這段 JavaScript 代碼片段,存在兩個問題,一個是 console.log(...args) 沒有縮進,另外一個是 createData() 這一行超出了最大長度限制。
const createData = (...args) => {
console.log(...args);
}
createData({ name: 'name', age: 'age', sex: 'sex', birthday: 'birthday', ctime: 'ctime', utime: 'utime' })
調(diào)整 ESLint 行的最大長度限制。
# .eslintrc.yml
rules: {
max-len: [2, { code: 80 }]
隨后控制臺執(zhí)行 eslint --fix 自動修復(fù)命令,發(fā)現(xiàn)雖然 console.log(...args) 這個縮進問題解決了,但是行的最大長度限制 max-len 還是未能解決。
圖片
接下來看看 Prettier 的效果如何呢,修改配置文件同 ESLint 保持一致。
// .prettierrc.json
{
"printWidth": 80
}
運行 prettier --write 命令后,它格式化了我們的代碼,結(jié)果是我們預(yù)期的。
圖片
解決 ESLint 與 Prettier 沖突
ESLint 與 Prettier 之間存在交集部分,一起使用就少不了沖突。例如,我的 ESLint 配置規(guī)則繼承了 airbnb,而 airbnb 字符串默認使用單引號,而 Prettier 字符串默認使用雙引號。
// src/App.tsx
const str = "編程界";
// .eslintrc.yml
extends:
- airbnb
// .prettierrc.json
{
"singleQuote": false // 字符串默認使用雙引號
}
在第一次執(zhí)行 eslint --fix 格式化后的 str 為 const str = '編程界'; 幫我修復(fù)了這個格式問題,但是又運行了 Prettier 格式化后,ESLint 提示我們錯誤又出現(xiàn)了,這樣就陷入了一個死循環(huán)。
圖片
正所謂術(shù)業(yè)有專攻,我們讓 ESLint 專注于代碼質(zhì)量檢測工作,Prettier 負責(zé)代碼的格式化工作。
解決方案第一步:開發(fā)時依賴安裝 eslint-config-prettier **npm i eslint-config-prettier -D** 插件,目的是禁用掉 ESLint 插件與 Prettier 之間沖突的規(guī)則。
# .eslintrc.yml
# extends 添加 prettier,此時沖突就已經(jīng)解決了
extends:
- airbnb
- prettier # eslint-config-prettier
解決方案第二步:賦予 eslint --fix 以 Prettier 規(guī)則格式化代碼的能力,并以 ESLint 的方式報告錯誤。開發(fā)時依賴安裝 eslint-plugin-prettier[7] npm i eslint-plugin-prettier -D 插件,按照以下規(guī)則配置。
# // .eslintrc.yml
plugins:
- prettier # eslint-plugin-prettier
rules: {
prettier/prettier: 2, # 賦予 ESLint 以 Prettier 規(guī)則格式化代碼的能力
上面的兩步操作也可以寫為一步,也是官方推薦的一個 https://github.com/prettier/eslint-plugin-prettier#recommended-configuration[8]
extends:
- plugin:prettier/recommended
Prettier 與 ESLint 結(jié)合在 VS Code 中發(fā)現(xiàn)的一個提示問題
當(dāng)把 prettier 結(jié)合到 eslint 里時,usePrettierrc 這個參數(shù)表示是否使用 .prettierrc 文件中配置信息,默認是啟用的,無需配置。
# .eslintrc.yml
prettier/prettier: ["error", {}, {
"usePrettierrc": true # 默認值
}]
prettier 規(guī)則默認箭頭函數(shù)如果只有一個參數(shù)也要加上 (),我想去掉這個規(guī)則,發(fā)現(xiàn)這個配置在 VSCode 展示有點問題無法讀取到修改后的 arrowParens 這個配置(其它配置例如 singleQuote,在 IDE 中是生效的), 編輯器會提示錯誤,但 eslint --fix 時沒有問題(此時證明 配置沒問題,手動執(zhí)行 eslint 還是生效的)。
# .prettierrc
{
"arrowParens": "avoid",
"singleQuote": true
}
圖片
沒有好的解決方案,為了讓 IDE 不報錯,首先嘗試在 ESLint 配置里加上 arrowParens 這個配置,發(fā)現(xiàn)問題解決了,但是這樣 ESLint prettier 配置中、.prettierrc 要寫兩遍總歸不是太好。
另外一種是不在 ESLint 配置里加上 arrowParens 這個配置,編輯器關(guān)閉重新打開也是沒問題的,反復(fù)的試驗了下,發(fā)現(xiàn)改完 .prettierrc 這個配置文件,VS Code 編輯器提示不會立即生效還默認是打開前的配置,如果有遇到類似問題,可以嘗試關(guān)閉下再打開看看。
prettier/prettier: ["error", {
"arrowParens": "avoid"
}, {
"usePrettierrc": true # 默認值
}]
Editorconfig 編輯器代碼風(fēng)格
Editorconfig[9] 是用來維護不同編輯器之間代碼風(fēng)格,通過配置文件 .editorconfig 設(shè)置,該配置文件的優(yōu)先級會更高,會覆蓋任一編輯器的配置。
root = true
[*]
indent_style = space # 縮進使用空格
indent_size = 2 # 每一個縮進兩個空格
end_of_line = lf # 使用 Unix 風(fēng)格的換行符
insert_final_newline = true # 每個文件尾部都有一個新的換行符
# 為 JavaScript、Python 文件設(shè)置默認字符集
[*.{js,py}]
charset = utf-8
Husky 攔截 git 操作命令
團隊中既然制定了規(guī)則,大家就要共同遵守,如果不加以限制,別人改動代碼后不做 ESLint 檢查,或者提交代碼忘記執(zhí)行代碼檢查命令,就會導(dǎo)致未經(jīng)檢測的代碼被提交進項目倉庫,代碼規(guī)則的制定也就形同虛設(shè)了。
使用 git 版本控制的項目中,默認都有一個 .git/hooks 目錄,包含了 git 在 commit、push 時的一些 hooks 腳本,結(jié)合這些可以做一些代碼的校驗操作。
$ ls .git/hooks
applypatch-msg.sample pre-commit.sample prepare-commit-msg.sample
commit-msg.sample pre-merge-commit.sample push-to-checkout.sample
fsmonitor-watchman.sample pre-push.sample update.sample
post-update.sample pre-rebase.sample
pre-applypatch.sample pre-receive.sample
使用 husky 工具,可以將 git hooks 的使用變得更加簡單,同時也能解決,.git 目錄提交無法跟蹤的問題。
在 husky 最新版本中,會生成一個 .husky/ 目錄,以往的版本中是在 package.json 中配置一個 { "husky": {"hooks": {} } } 實現(xiàn)的。
Husky 推薦在 package.json 的 scripts 里設(shè)置 prepare,因為在執(zhí)行 npm install 命令時,將會自動執(zhí)行 prepare 腳本。
注意在 npm version < 7 時 npm set-script 命令不支持,需要手動在 package.json 設(shè)置 { "scripts": { "prepare": "husky install" } } 或升級 Node.js 版本為 v16.x LTS。
$ npm install husky -D
$ npm set-script prepare "husky install"
$ npm run prepare # 設(shè)置 .husky 目錄
添加一個 hook,在 git commit 前執(zhí)行 eslint 命令檢測代碼規(guī)范。
$ npm set-script lint "eslint ./src/"
$ npx husky add .husky/pre-commit "npm run lint" # 設(shè)置在 git commit 前執(zhí)行 eslint 代碼檢查
現(xiàn)在執(zhí)行 git commit 命令如果存在不規(guī)范的代碼,本次 commit 就會被攔截。
圖片
如果本次我只修改了 src/index.tsx 這個文件,由于歷史原因最開始沒有加 ESLint 規(guī)范,之前的一些未修改的文件也被檢查了,當(dāng)看到成百上千條 ESLint 報錯提示,是不是會出現(xiàn)想收回使用 ESLint 工具的想法?這種問題在團隊開發(fā)中,如果一開始沒有設(shè)定代碼規(guī)范,后期加入代碼規(guī)范時,這種問題還是會常見的,解決方案繼續(xù)往下看。
lint-staged 僅檢查修改的文件
每次只對修改的文件做檢查,就要用到 **lint-staged**[10] 這個工具了,解決老項目的歷史包袱問題。
lint-staged 是對 git add 進入暫存區(qū)的文件進行檢查,開發(fā)時依賴安裝 npm i lint-staged -D,修改 package.json 文件。
注意,**npx eslint --fix**** 后不要指定路徑,例如 **npx eslint --fix ./src/** 會出現(xiàn)只修改了 A 文件,但是其它有問題的 B、C 等文件也被檢測了**。
// package.json
{
"srcripts": {
"precommit": "lint-staged"
},
"lint-staged": {
"*.{js,css,md,ts,tsx}": [
"npx eslint --fix"
]
}
}
修改 .husky 目錄下的 pre-commit 文件。執(zhí)行的流程是:
- git commit 命令執(zhí)行后 git hook 被觸發(fā)
- 進而執(zhí)行 precommit、該命令腳本又執(zhí)行了 lint-staged
- lint-staged 則根據(jù)配置和只對 git add 到暫存區(qū)的文件進行掃描,執(zhí)行 eslint 代碼檢測。
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run precommit
commitlint 規(guī)范提交風(fēng)格
關(guān)于 commit 如果沒有進行規(guī)范,最終每個人寫出來的風(fēng)格也是不一樣的,所以這個還得借助于工具,幫助我們規(guī)范提交的習(xí)慣。
分享兩個 git commit message 的討論:Stackoverflow - Git Commit Messages: 50/72 Formatting[11]、知乎 - 如何寫好 Git commit log?[12]
commitlint
安裝 commitlint cli 和提交信息校驗規(guī)則 @commitlint/config-conventional[13],詳細的參考文檔 commitlint.js.org[14]。
# 安裝 commitlint cli and conventional config
$ npm install --save-dev @commitlint/{config-conventional,cli}
# 創(chuàng)建 commitlint.config.js 配置文件
# @commitlint/config-conventional 是推薦的 commit message 校驗配置,
$ echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
# husky 添加 commit-msg 文件
$ npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
使用格式 git commit -m '<type>[Optional scope]: <description>',介紹一下 type 屬性的幾種類型約束。
- build:修改了構(gòu)建系統(tǒng),例如前端的 Webpack 構(gòu)建工具
- chore:
- ci:持續(xù)集成工具的修改,例如 Jenkins
- docs:文檔
- feat:新功能
- fix:Bug 修復(fù),這個用的就比較多了~
- perf:做的一些性能提升
- refactor:重構(gòu)
- revert:回滾之前某個 commit
- style:代碼格式的修改,例如通過 Prettier 格式化代碼,但不會影響邏輯
- test:測試
如果我這樣 git commit -m '添加 commitlint 工具' 提交代碼,就會收到以下錯誤提示。
圖片
正確的做法是加上類型信息 git commit -m 'feat: 添加 commitlint 工具'。
commitzen
使用 commitlint 每次都需要輸入類型,需要我們記住這些規(guī)范,于是就有了 [commitizen](https://github.com/commitizen/cz-cli "commitizen")。
全局安裝 npm install -g commitizen,之后可以使用 git cz 替代 git commit 命令。還需要為 commitizen 指定一個 Adapter,例如 cz-conventional-changelog[15] 基于 commit message 的輕量級約定,提供了一組簡單的規(guī)則來創(chuàng)建明確的提交歷史,參考 約定式提交[16]。
$ commitizen init cz-conventional-changelog --save-dev --save-exact
執(zhí)行以上命令后,在 package.json 中會生成如下配置
{
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}
對文件做一些任意修改,執(zhí)行 git cz 命令后,運行效果如下所示:
圖片
當(dāng)選擇一個類型后,終端會出現(xiàn)一個交互式的操作,包括:影響范圍、簡短的變更描述(必須)、詳細的變更描述、是否存在不兼容的變更、是否影響某些打開的 Issues,這個里面只有簡短的變更描述是必填的。
圖片
commit message 成功后,執(zhí)行 git log 看到的日志是這樣子的。
圖片
如果想自定義 Adapter 而不是使用 cz-conventional-changelog,推薦看下 cz-customizable[17] 這個開源項目,這里不再過多描述。
standard-version
結(jié)合 standard-version[18] 實現(xiàn)自動生成 CHANGELOG。
$ npm i --save-dev standard-version
// package.json
{
"scripts": {
"release": "standard-version"
}
}
以上介紹內(nèi)容,實際應(yīng)用可參考這個前后端項目 https://github.com/qufei1993/compressor[19],不同的是該項目是基于 TypeScript 編寫,正好也可以學(xué)習(xí)下。
參考資料
[1]ESLint 官網(wǎng) Rules: https://cn.eslint.org/docs/rules/
[2].gitignore 規(guī)范: https://git-scm.com/docs/gitignore
[3]Airbnb JavaScript 風(fēng)格指南: https://github.com/lin-123/javascript
[4]eslint-config-airbnb: https://www.npmjs.com/package/eslint-config-airbnb
[5]eslint-config-airbnb-base: https://www.npmjs.com/package/eslint-config-airbnb-base
[6]Prettier: https://prettier.io/
[7]eslint-plugin-prettier: https://github.com/prettier/eslint-plugin-prettier
[8]https://github.com/prettier/eslint-plugin-prettier#recommended-configuration: https://github.com/prettier/eslint-plugin-prettier#recommended-configuration
[9]Editorconfig: https://editorconfig.org/
[10]lint-staged: https://github.com/okonet/lint-staged
[11]Stackoverflow - Git Commit Messages: 50/72 Formatting: https://stackoverflow.com/questions/2290016/git-commit-messages-50-72-formatting
[12]知乎 - 如何寫好 Git commit log?: https://www.zhihu.com/question/21209619/answer/257574960
[13]@commitlint/config-conventional: https://github.com/conventional-changelog/commitlint/tree/master/@commitlint/config-conventional
[14]commitlint.js.org: https://commitlint.js.org/
[15]cz-conventional-changelog: https://github.com/commitizen/cz-conventional-changelog
[16]約定式提交: https://www.conventionalcommits.org/zh-hans/v1.0.0/
[17]cz-customizable: https://github.com/leoforfree/cz-customizable
[18]standard-version: https://github.com/conventional-changelog/standard-version
[19]https://github.com/qufei1993/compressor: https://github.com/qufei1993/compressor