前端Vuer,請給你的項目加上 ESLint

語言: CN / TW / HK

theme: devui-blue

1 ESLint 是什麼

ESLint 是一個插件式的 JavaScript / JSX 代碼檢查工具,用於檢測和修復 JavaScript 代碼中的問題,目標是讓代碼更一致並避免錯誤。

2 在 Vue 項目中引入 ESLint

使用 Vue CLI 搭建的 Vue2 項目已經自帶 ESLint,就不贅述,我們看下 Vite 搭建的 Vue3 項目中怎麼引入 ESLint。

使用以下命令搭建一個 Vue3 項目: npm create vite@latest vue3-project

創建之後,啟動起來: npm i npm run dev

效果如下:

image.png

2.1 引入 ESLint

執行以下命令: npm init @eslint/config

進入交互式界面,可通過上下方向鍵選擇,通過按回車鍵確定。

第一個問題是: - 你希望用 ESLint 來幹嘛? - 我們選擇最全面的那個:檢查語法,發現問題,並強制統一代碼樣式

$ npm init @eslint/config ? How would you like to use ESLint? … To check syntax only To check syntax and find problems ❯ To check syntax, find problems, and enforce code style

第二個問題是: - 你的項目用的是什麼模塊系統? - 因為是運行在瀏覽器端,選擇 ESModule

? What type of modules does your project use? … ❯ JavaScript modules (import/export) CommonJS (require/exports) None of these

第三個問題是: - 你用的什麼框架?(居然沒有 Angular) - 選擇 Vue

? Which framework does your project use? … React ❯ Vue.js None of these

第四個問題是: - 你是否使用 TypeScript? - 選擇 Yes

? Does your project use TypeScript? › No / Yes

第五個問題是: - 你的代碼運行在什麼環境?(這個可以多選) - 選擇 Browser 瀏覽器環境

? Where does your code run? … (Press <space> to select, <a> to toggle all, <i> to invert selection) ✔ Browser ✔ Node

第六個問題是: - 你想定義怎樣的代碼風格? - 選擇使用一個流行的代碼風格

? How would you like to define a style for your project? … ❯ Use a popular style guide Answer questions about your style

第七個問題是: - 你想使用哪個樣式風格? - Airbnb 用的人比較多,就選這個吧

? Which style guide do you want to follow? … ❯ Airbnb: https://github.com/airbnb/javascript Standard: https://github.com/standard/standard Google: https://github.com/google/eslint-config-google XO: https://github.com/xojs/eslint-config-xo

第八個問題是: - 配置文件用什麼格式? - 就選 JavaScript 吧(生成 eslintrc.js 文件)

? What format do you want your config file to be in? … ❯ JavaScript YAML JSON

完成!是不是超級簡單!

看下我們都選了哪些配置:

✔ How would you like to use ESLint? · style ✔ What type of modules does your project use? · esm ✔ Which framework does your project use? · vue ✔ Does your project use TypeScript? · Yes ✔ Where does your code run? · browser ✔ How would you like to define a style for your project? · guide ✔ Which style guide do you want to follow? · airbnb ✔ What format do you want your config file to be in? · JavaScript

主要給我們安裝了以下依賴: - [email protected] - [email protected] - [email protected] - [email protected] - @typescript-eslint/[email protected] - @typescript-eslint/[email protected]

並生成了一個 eslintrc.cjs 配置文件: ```js module.exports = { env: { browser: true, es2021: true, }, extends: [ 'plugin:vue/vue3-essential', 'airbnb-base', ], parserOptions: { ecmaVersion: 'latest', parser: '@typescript-eslint/parser', sourceType: 'module', }, plugins: [ 'vue', '@typescript-eslint', ],

// 自定義 rules 規則 rules: { }, }; ```

2.2 ESLint 配置

2.3 執行 ESLint 代碼檢查

在 package.json 文件的 scripts 中配置 lint 腳本命令:

``` "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", "preview": "vite preview",

// 配置 lint 腳本命令 "lint": "eslint --ext .vue,.ts src/" }, ```

執行 lint 腳本命令:

npm run lint

出現了一堆報錯:

``` /vue3-project/src/App.vue 4:53 error Missing semicolon semi

/vue3-project/src/components/HelloWorld.vue 2:26 error Missing semicolon semi 4:31 error Missing semicolon semi 6:21 error Missing semicolon semi

/vue3-project/src/main.ts 1:32 error Missing semicolon semi 2:21 error Missing semicolon semi 3:28 error Missing semicolon semi 5:29 error Missing semicolon semi

/vue3-project/src/vite-env.d.ts 4:3 error Expected 1 empty line after import statement not followed by another import import/newline-after-import 4:45 error Missing semicolon semi 5:48 error Missing semicolon semi 6:27 error Missing semicolon semi

✖ 12 problems (12 errors, 0 warnings) 12 errors and 0 warnings potentially fixable with the --fix option. ```

大部分都是説句尾沒有分號,因為我們選擇的是 Airbnb 代碼規範,所以會有這個報錯提示,不同的代碼規範,內置的檢查規則不一定完全相同。

2.4 自動修復 ESLint 問題

在 scripts 中增加自動修復 ESLint 問題的腳本命令: ``` "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", "preview": "vite preview", "lint": "eslint --ext .vue,.ts src/",

// 自動修復 ESLint 問題腳本命令 "lint:fix": "eslint --ext .vue,.ts src/ --fix" }, ```

執行: npm run lint:fix

執行自動修復的命令之後,所有分號都加上了,未使用的變量也自動移除了。

再次執行: npm run lint

沒有再報錯。

3 配置 husky 和 PR 門禁

3.1 配置 husky 門禁

為了確保每次提交(git commit)之前代碼都通過 ESLint 檢查,我們增加一個 pre-commit 門禁。

  • 第一步:安裝 husky 和 lint-staged

npm i lint-staged husky -D

  • 第二步:在 package.json 的 scripts 中增加 prepare 腳本命令

```json "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", "preview": "vite preview", "lint": "eslint --ext .vue,.ts src/", "lint:fix": "eslint --ext .vue,.ts src/ --fix",

// 在 npm install 之後自動執行,生成.husky目錄。 "prepare": "husky install" }, ```

  • 第三步:執行 prepare 腳本

npm run prepare

該命令執行完會在項目根目錄自動生成.husky目錄。

  • 第四步:增加 pre-commit 鈎子

執行以下命令,會在.husky目錄自動生成pre-commit文件鈎子。

npx husky add .husky/pre-commit "npx lint-staged"

  • 第五步:增加 lint-staged 配置

"lint-staged": { "src/**/*.{vue,ts}": "eslint --fix" },

通過以上五個步驟,以後每次使用git commit命令提交提交代碼,都會: - 被 pre-commit 鈎子攔截 - 執行 npx lint-staged 命令 - 進而執行 eslint --fix 命令,對本次提交修改的代碼涉及的文件進行代碼檢查,並自動修復能修復的錯誤,不能修復的錯誤會提示出來,只有所有 ESLint 錯誤都修復了才能提交成功

3.2 配置 PR 門禁

如果你在做自己的開源項目,並且非常幸運,有一羣志同道合的小夥伴願意一起參與貢獻,這時為了統一大家的代碼風格,讓貢獻者們專注於特性開發,不用擔心代碼格式規範問題,並通過 ESLint 工具提示貢獻者,哪些代碼可能帶來潛在的風險,你就有必要給提交的 PR 加上 ESLint 門禁。

我們已經增加了本地的 ESLint 命令:

json "scripts": { "lint": "eslint --ext .vue,.ts src/", },

我們需要在本目錄創建一個.github/workflows/pull-request.yml文件,在該文件中寫入以下內容:

```yml name: Pull Request

on: push: branches: [ dev, main ] pull_request: branches: [ dev, main ]

jobs: build:

runs-on: ubuntu-latest

strategy:
  matrix:
    node-version: [16.x]

name: "ESLint"
steps:
  - name: Checkout
    uses: actions/checkout@v2

  - name: Install pnpm
    uses: npm/action-setup@v2
    with:
      version: 6

  - name: Use Node.js ${{ matrix.node-version }}
    uses: actions/setup-node@v2
    with:
      node-version: ${{ matrix.node-version }}

  - name: Install deps
    run: npm i

  - name: ESLint
    run: npm run lint

```

這樣只要 PR 是往 dev 或 main 分支合入的,都會跑一遍這個 Github Actions 工作流任務,ESLint 檢查不通過的話,PR 的 checks 裏面會提示,攔截該 PR 的合入。

PR 的提交者看到提示,也可以點到任務裏面去看是哪裏報錯,修改掉這些 ESLint 問題,PR 就會變成綠色,項目的管理員就可以順利合入 PR 到目標分支啦🎉

4 常見的 ESLint 問題及修復案例

接下來跟大家分享 Vue DevUI 開源 Vue3 組件庫 ESLint 問題修復過程中遇到的典型問題。

4.1 案例1:warning Unexpected any. Specify a different type @typescript-eslint/no-explicit-any

該問題出現頻率比較高,原因是有些類型寫了any,需要明確的類型。

比如Pagination組件的單元測試文件pagination.spec.ts中:

`` const wrapper = mount({ components: { DPagination }, template:` }, globalOption);

const btns = wrapper.findAll('a.devui-pagination-link');

expect(btns.map((ele: any) => ele.text()).join()).toEqual('<,1,...,4,5,6,...,16,>'); ```

其中的ele: any就屬於這類問題。

解決辦法是給ele加上明確的類型,看邏輯是<button>元素,由於是@vue/test-utils庫的包裹元素,因此需要包一層DOMWrapper

``` import { DOMWrapper } from '@vue/test-utils';

expect(btns.map((ele: DOMWrapper) => ele.text()).join()).toEqual('<,1,...,4,5,6,...,16,>'); ```

4.2 案例2:'xxx' was used before it was defined no-use-before-define

這也是一個比較常見的問題,在聲明之前使用變量或方法,解決辦法也很簡單,只需要調整下代碼的順序即可,將變量或方法的聲明放在調用的語句之前。

比如Pagination組件的pagination.tsx中:

``` // 極簡模式下,可選的下拉選擇頁碼 const litePageOptions = computed(() => liteSelectOptions(totalPages.value));

// 當前頁碼
const cursor = computed({
  get() {
    // 是否需要修正錯誤的pageIndex
    if (!props.showTruePageIndex && props.pageIndex > totalPages.value) {
      emit('update:pageIndex', totalPages.value || 1);
      return totalPages.value || 1;
    }
    return props.pageIndex || 1;
  },
  set(val: number) {
    emit('update:pageIndex', val);
  }
});

// 總頁數
const totalPages = computed(() => Math.ceil(props.total / props.pageSize));

```

其中的totalPages的聲明在比較靠後的位置,但是卻在聲明之前在litePageOptionscursor變量中都使用了totalPages,所以提示 ESLint 問題。

解決的方法就是將totalPages的聲明放在litePageOptionscursor之前。

``` // 總頁數 const totalPages = computed(() => Math.ceil(props.total / props.pageSize));

// 極簡模式下,可選的下拉選擇頁碼
const litePageOptions = computed(() =>  liteSelectOptions(totalPages.value));

// 當前頁碼
const cursor = computed({ ... });

```

4.3 案例3:warning Missing return type on function @typescript-eslint/explicit-module-boundary-types

該問題是因為函數缺少返回類型,比如Fullscreen組件utils.ts文件的launchImmersiveFullScreen方法中:

export const launchImmersiveFullScreen = async (docElement: any) => { let fullscreenLaunch = null; if (docElement.requestFullscreen) { fullscreenLaunch = docElement.requestFullscreen(); } else if (docElement.mozRequestFullScreen) { fullscreenLaunch = docElement.mozRequestFullScreen(); } else if (docElement.webkitRequestFullScreen) { fullscreenLaunch = Promise.resolve(docElement.webkitRequestFullScreen()); } else if (docElement.msRequestFullscreen) { fullscreenLaunch = Promise.resolve(docElement.msRequestFullscreen()); } return await fullscreenLaunch.then(() => !!document.fullscreenElement); };

先看下launchImmersiveFullScreen方法的參數問題,docElement用了any,也缺失了返回類型,docElement其實就是document對象,可以使用HTMLElement類型,但是launchImmersiveFullScreen這個方法是用來啟動沉浸式全屏的,為了實現瀏覽器兼容,比如使用了docElement.mozRequestFullScreen兼容火狐,而這些方法在HTMLElement中是沒有的,會報TS類型錯誤,所以需要做一些改造。

interface CompatibleHTMLElement extends HTMLElement { mozRequestFullScreen?: () => void; webkitRequestFullScreen?: () => void; msRequestFullscreen?: () => void; }

這裏定義了一個CompatibleHTMLElement的類型,繼承了HTMLElement,並增加了一些自定義的方法。

export const launchImmersiveFullScreen = async (docElement: CompatibleHTMLElement) => { ... }

再來看下launchImmersiveFullScreen方法的返回類型問題。

return await fullscreenLaunch.then(() => !!document.fullscreenElement);

該方法返回了一個Promise對象,它的類型是一個泛型,我們需要傳入具體的類型:

export const launchImmersiveFullScreen = async (docElement: CompatibleHTMLElement): Promise<boolean> => { ... return await fullscreenLaunch.then(() => !!document.fullscreenElement); };

4.4 案例4:'xxx' is already declared in the upper scope @typescript-eslint/no-shadow

這個問題是由於嵌套的作用域中定義了相同的變量名,比如Tree組件的use-checked.ts文件中:

export default function useChecked(...) { const onNodeClick = (item: TreeItem) => { // 這裏定義了 id 變量 const { id } = item; ... filter 裏面又定義了一個 id 參數 const currentSelectedItem = flatData.filter(({ id }) => currentSelected[id] && currentSelected[id] !== 'none'); ... } }

修改方式就是將其中一個 id 的名字改了,比如把裏面的 id 改成 itemId:

const currentSelectedItem = flatData.filter(({ id: itemId }) => currentSelected[itemId] && currentSelected[itemId] !== 'none');

歡迎在評論區分享你在項目中遇到的 ESLint 問題👏👏

5 感謝

Vue DevUI 組件庫 ESLint 問題清零之路上,不得不提的一位小夥伴就是 @linxiang07 同學,他從2022年3月到7月,持續4個多月,累計修復40多個組件的100多個 ESLint 問題,直到7月5日 ESLint 清零,並給 PR 添加 ESLint 門禁,後續 PR 如果未通過 ESLint 檢查將無法合入。

感謝 linxiang 同學的付出!

image.png

以下是 linxiang 同學提交的部分PR:

image.png

linxiang 同學也因此成為 Vue DevUI 組件庫 TOP3 的貢獻者,併成為我們的 Committer 和管理員🎉🎉

image.png

值得一提的是 linxiang 同學還是我們的 VirtualList 虛擬列表組件和 Tree 組件等多個組件的田主和貢獻者,並且完善了多個組件的單元測試,能力很強,是非常活躍和積極的貢獻者。

歡迎加入我們,和優秀的開發者一起打造高質量的開源組件庫項目!感興趣可以加助手:devui-official

我正在參與掘金技術社區創作者簽約計劃招募活動,點擊鏈接報名投稿