設定 Angular 專案使用 ESLint 進行更嚴格的程式碼撰寫風格檢查 | The Will Will Web

The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

設定 Angular 專案使用 ESLint 進行更嚴格的程式碼撰寫風格檢查

早期 Angular CLI 建立的專案都有內建 TSLintcodelyzer,但從 Angular CLI v11 開始建議大家開始轉移到 Angular ESLint,直到 Angular CLI v12 直接拔除所有 Linter 設定。本篇文章我將介紹如何從現有 TSLint 的 Angular 專案升級到改用 Angular ESLint 進行撰寫風格檢查,以及從完全沒有 ESLint 的 Angular 專案如何加入 ESLint 的完整流程。

替完全沒有 TSLint/ESLint 的 Angular 專案加入 Angular ESLint

  1. 先檢查專案有沒有設定 lint 目標

    直接執行 ng lint 命令,就可以看出有沒有設定 Angular CLI 預設的 lint 目標。如果沒有設定過,應該會出現以下訊息:

    Cannot find "lint" target for the specified project.
    
    You should add a package that implements linting capabilities.
    
    For example:
      ng add @angular-eslint/schematics
    
  2. 替 Angular 專案加入 ESLint 工具

    預設 Angular CLI v12 建立的專案並不會自動加入 ESLint 工具,所以需要額外安裝 Angular ESLint 相關工具起來。

    以下這段 ng add 命令會自動幫你產生 .eslintrc.json 設定檔,並自動新增 angular.jsonlint 設定,讓你可以透過 ng lint 驗證程式碼風格。:

    ng add @angular-eslint/schematics --skip-confirmation
    

    這份 .eslintrc.json 設定檔內容如下,第一層包含 3 段設定,分別是:

    1. root: 設定為 true 代表不會去參照上層目錄的 .eslintrc.json 設定。
    2. ignorePatterns: 不要透過 ESLint 檢查的路徑。設定為 "projects/**/*" 代表的是所有透過 ng g applicationng g library 建立的應用程式,都會被排除在 ESLint 的範圍,因為每個應用程式都可以有自己的 ESLint 定義。
    3. overrides: 特別針對 Angular 專案進行最佳化過的規則集定義,主要針對 *.ts*.html 做了一些規則集的調整。

    詳細的規則設定方式請參見 Configuration Files 文件

    {
      "root": true,
      "ignorePatterns": [
        "projects/**/*"
      ],
      "overrides": [
        {
          "files": [
            "*.ts"
          ],
          ...
        },
        {
          "files": [
            "*.html"
          ],
          ...
        }
      ]
    }
    

理解 Angular ESlint 內建的 ESLint 規則集

  1. 針對所有 *.ts 檔案的規則集

    "extends": [
      "plugin:@angular-eslint/recommended",
      "plugin:@angular-eslint/template/process-inline-templates"
    ]
    

    預設規則集會 繼承(extends) 自 plugin:@angular-eslint/recommended@angular-eslint/template/process-inline-templates,一開始我還看不太懂這段設定的命名規則,後來看了 Using a configuration from a plugin 也不是非常理解,多看了幾次才真正看明白。

    官方文件說 extends 的格式為: plugin: + package name (套件名稱) + / + configuration name (設定名稱)

    還說如果套件名稱是 eslint-plugin-react 的話,就可以簡寫成 react 就好。但 Angular ESLint 的名稱是 @angular-eslint,難道簡寫成 eslint-plugin-@angular-eslint 嗎?當然不是,我知道 @angular-eslintnpm 的 Scope Name,所以這段 @angular-eslint 套件名稱,正確的展開全名是 @angular-eslint/eslint-plugin 才對!這個 package name 的預設值還要人工翻譯後才能看懂,真的相當不直覺啊!

    簡單來說,針對所有 *.ts 檔案的規則集其實是繼承以下兩個設定:

    1. @angular-eslint/eslint-plugin 套件的 recommended 設定

      以下是 @angular-eslint/recommended 設定的規則內容:

      {
        "extends": "./configs/base.json",
        "rules": {
          "@angular-eslint/component-class-suffix": "error",
          "@angular-eslint/contextual-lifecycle": "error",
          "@angular-eslint/directive-class-suffix": "error",
          "@angular-eslint/no-conflicting-lifecycle": "error",
          "@angular-eslint/no-empty-lifecycle-method": "error",
          "@angular-eslint/no-host-metadata-property": "error",
          "@angular-eslint/no-input-rename": "error",
          "@angular-eslint/no-inputs-metadata-property": "error",
          "@angular-eslint/no-output-native": "error",
          "@angular-eslint/no-output-on-prefix": "error",
          "@angular-eslint/no-output-rename": "error",
          "@angular-eslint/no-outputs-metadata-property": "error",
          "@angular-eslint/use-lifecycle-interface": "warn",
          "@angular-eslint/use-pipe-transform-interface": "error"
        }
      }
      
    2. @angular-eslint/eslint-plugin-template 套件的 process-inline-templates 設定

      這組預設規則是為了讓寫在 *.ts 中的 Inline Template 的 HTML 而設計的檢查規則!

    除了繼承預設規則外,在 .eslintrc.json 中還加入了兩條可能常會依照專案需求而客製化的 ESLint 規則,這兩條分別是規定 DirectiveComponent 的 Selector 命名規則:

    "rules": {
      "@angular-eslint/directive-selector": [
        "error",
        {
          "type": "attribute",
          "prefix": "app",
          "style": "camelCase"
        }
      ],
      "@angular-eslint/component-selector": [
        "error",
        {
          "type": "element",
          "prefix": "app",
          "style": "kebab-case"
        }
      ]
    }
    

    如果你想關閉 @angular-eslint/directive-selector 規則,只要將 "error" 修改為 "off" 即可。如果只想顯示警告訊息,可以改成 "warn"

    我通常還會額外關閉 @angular-eslint/no-empty-lifecycle-method 規則,因為我們經常會在元件中預留 ngOnInit hook 方法,所以會有很多元件暫時會時沒有方法內容的情況

    "rules": {
      "@angular-eslint/directive-selector": [
        "error",
        {
          "type": "attribute",
          "prefix": "app",
          "style": "camelCase"
        }
      ],
      "@angular-eslint/component-selector": [
        "error",
        {
          "type": "element",
          "prefix": "app",
          "style": "kebab-case"
        }
      ],
      "@angular-eslint/no-empty-lifecycle-method": ["off"]
    }
    
  2. 針對所有 *.html 檔案的規則集

    "extends": [
      "plugin:@angular-eslint/template/recommended"
    ]
    

    預設規則集會 繼承(extends) 自 @angular-eslint/eslint-plugin-template 套件的 recommended 設定:

    以下是 recommended 設定的規則內容:

    {
      "extends": "./configs/base.json",
      "rules": {
        "@angular-eslint/template/banana-in-box": "error",
        "@angular-eslint/template/eqeqeq": "error",
        "@angular-eslint/template/no-negated-async": "error"
      }
    }
    
  3. 顯示有哪些 ESLint 規則會套用在你有的原始碼檔案

    要顯示最終套用所有設定之後的 ESLint Rules 有哪些,可以執行以下命令:

    npx eslint --print-config src\main.ts
    

完美整合 Prettier 與 ESLint 規則集

其實 Prettier 與 ESLint 規則經常會打架,一邊說要做強制格式化(enforce formatting),一邊說格式不正確,你要是遇到這種狀況,肯定會覺得無所適從!

我從 Angular ESLint 的 Our philosophy on lint rules which enforce code formatting conventions 文件看到他們的設計哲學。簡言之,ESLint 團隊小、用戶多,他們決定不會提供任何跟 Code formatting (程式碼格式化) 相關的驗證規則在內。

We are a tiny team with a massive userbase so we have to pick our battles. We will therefore not be providing or supporting any code formatting related rules via our plugins in this project.

不過雖然 ESLint 專案本身不提供 Code formatting 的規則沒錯,但許多前端專案整合了許多第三方的 ESLint Plugins 套件,這些套件他們就不敢保證會不會有 Code formatting 相關的規則了。在開始設定前,你可以先用以下命令,檢查一下目前的 ESLint 設定是否跟 Prettier 有任何衝突的規則:

npx eslint-config-prettier src/main.ts

你應該可以看到 Angular ESLint "感覺" 好像都已經調整好了,預設 Angular ESLint "好像" 並不會跟 Prettier 有任何衝突的規則:

No rules that are unnecessary or conflict with Prettier were found.

但仔細研究後發現,Angular ESLint 建議的規則其實非常的 "中庸",並沒有非常嚴格,所以很多 ESLint 建議的規則都沒有啟用。當你啟用 eslint:recommended 規則集之後,就會有一堆衝突了!

所以我個人還是建議你就先設定好,以免日後遇到衝突不知所措。官方建議你可以安裝 eslint-config-prettier 套件,其安裝的步驟如下:

  1. 安裝套件

    npm install --save-dev eslint-config-prettier
    
  2. 套用 prettier 預設規則到 .eslintrc.jsonextends 的最後一個元素

    "extends": [
      "some-other-config-you-use",
      "prettier"  // <-- 加上這一行
    ]
    

    請注意 prettier 一定要設定到 extends 的最後面,而且 Angular 專案的 .eslintrc.json 有兩個地方(*.ts*.html)都要設定!

    這部分設定其實不是把 Prettier 的規則加入到 ESLint 的規則之中,反而是將以下這幾個常見的 ESLint Plugins 中的部分規則停用,主要是停用跟程式碼格式化相關的規則而已,避免 ESLint 與 Prettier 發生衝突!

Angular ESLint 官網其實還有另一個選項是安裝 eslint-plugin-prettier 套件,這個套件會直接將 Prettier 的規則直接整合到 ESLint 檢查規則中,而不是上面這種把 Prettier 的規則剔除。我不太喜歡讓 ESLint 來檢查程式碼排版風格,因為我們的 Prettier 已經在 git commit 的時候都強制套用了,沒必要再檢查一次。

將 Angular 12 之前的 TSLint 設定改用 Angular ESLint 進行程式碼風格檢查

  1. 替 Angular 專案加入 ESLint 工具

    以下這段 ng add 命令會幫你安裝 Angular ESLint 相關套件,但他會自動偵測你的專案有沒有已經使用的 tslint.json 檔案,如果有的話就不會自動幫你產生 .eslintrc.json 設定檔:

    ng add @angular-eslint/schematics
    
  2. 將 Angular 專案的 TSLint 設定移轉到 ESLint 設定

    這段命令會自動幫你產生 .eslintrc.json 並移除 tslint.json 設定檔,過程中還會將你修改過的 tslint.json 設定內容也一併移轉到 .eslintrc.json 喔!

    ng g @angular-eslint/schematics:convert-tslint-to-eslint --remove-tslint-if-no-more-tslint-targets --ignore-existing-tslint-config
    

    這個 Schematics 工具不只幫你建立 .eslintrc.json 設定檔而已,還會自動幫你把現有程式碼中的 magic comment 從 tslint 改成 eslint,非常貼心的!

    例如原本這段註解:

    // tslint:disable-next-line:directive-selector
    

    升級之後會自動變成:

    // eslint-disable-next-line @angular-eslint/directive-selector
    
  3. 注意 angular.json 的變化

    在執行 TSLint 移轉到 ESLint 的過程中,你的 angular.json 也會一併被修改,主要的修改有兩個部分:

    1. 修改 ng lint 的 Angular Builder 設定

      "lint": {
        "builder": "@angular-eslint/builder:lint",
        "options": {
          "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
        }
      }
      
    2. 修改預設 ng generate 的預設 Schematics (defaultCollection)

      "cli": {
        "defaultCollection": "@angular-eslint/schematics"
      }
      

    至於會需要修改預設 ng generate 預設 Schematics (defaultCollection) 的原因,最主要是因為當你啟用 ESLint 之後,你日後所建立的 applicationlibrary 通常也需要啟用 ESLint 才對。改用 @angular-eslint/schematics 之後,未來在執行 ng g appng g lib 就會全自動幫你設定好 ESLint 相關設定。預設透過 Angular CLI 的 ng g appng g lib 建立的應用程式會被放在 projects/ 目錄下。

    詳情請見 Using ESLint by default when generating new Projects within your Workspace 說明。

手動執行 ESLint 程式碼風格檢查

  1. 使用 Angular CLI 的 ng lint 檢查

    如果你在 CI 的時候執行 ng lint 失敗,其 Exit Code 將會是 1,也意味著 CI 的 Pipelines 會自動報錯! 👍

    ng lint
    

    這個命令還有個「快取」功能,可以大幅提高每次執行 ng lint 的速度,讓你唯有在檔案變更的時候才會再次重新執行驗證程式碼風格:

    ng lint --cache
    

    預設快取檔會儲存在與 .eslintrc.json 相同目錄下,並建立一個 .eslintcache 快取檔案,建議將此檔案加入到 .gitignore 檔案中!

    除此之外 ng lint 還有個隱藏版的秘技,可以讓你快速看出 ng lint 從你的程式碼中看出了多少 Rule 違規的情況,以及每個違規狀況的耗用豪秒數,你可以藉此判斷哪些規則要關閉,所以非常非常實用啊!

    • Command Prompt

      set TIMING=1 && ng lint
      
    • PowerShell

      $env:TIMING=1 ; ng lint
      
    • bash / zsh / fish / sh

      TIMING=1 ng lint
      
  2. 使用 ESLint 命令手動檢查

    其實用 Angular 根本不需要用到 eslint 命令。但如果你想玩玩看自己執行 eslint 的話,可以參考以下命令:

    npx eslint .
    

    不過,這種執行方式會連 dist/ 目錄或 src/assets/ 目錄下的檔案也都一併掃描,所以可能會掃描出一些意外的結果,建議可以加入這些目錄到 .eslintignore 檔案中,例如:

    dist
    **/assets/**/*
    jest.config.js
    fixtures
    coverage
    __snapshots__
    

    如果想執行 projects/* 目錄下的 ESLint 檢查,你必須在專案根目錄這樣執行:

    npx eslint projects/lib1
    
  3. 使用 ESLint 自動修復「可修復」的問題

    ESLint 有很多內建的規則包含了自動修復原始碼的能力,你可以從 List of available rules 搜尋 🔧 這個 Emoji 文字,就可以找到所有包含自動修復的規則!

    透過 Angular CLI 自動修復程式碼,只要執行以下命令即可:

    ng lint --fix
    

    執行 ng lint 的時候,結果會顯示有多少問題是可以透過 --fix 自動修復的,有顯示的時候再來執行即可。

    如果你也想直接跑跑看 eslint 的話,一般我會先用 --fix-dry-run 試跑看看:

    npx eslint . --fix-dry-run
    

    然後再實際執行修復動作(會寫入修正後的內容到原始碼)

    npx eslint --fix
    

調整 Visual Studio Code 設定

  1. 安裝 ESLint 擴充套件

    當啟用 ESLint 之後,你當然希望我們在 Visual Studio Code 裡面就可以享受即時的開發提示,所以為了讓團隊成員都能享受這個功能,我們要在專案的 .vscode/extensions.json 加入 ESLint 擴充套件,讓大家都能記得要安裝起來!

    如果連同我在 設定 Angular 專案使用 husky 簡化 Git hooks 設定並用 Prettier 統一風格 文章提到的 Prettier 設定,你的 .vscode/extensions.json 應該會變成這樣:

    {
        "recommendations": [
            "EditorConfig.EditorConfig",
            "esbenp.prettier-vscode",
            "dbaeumer.vscode-eslint"
        ]
    }
    
  2. 儲存檔案時自動修復程式碼

    除此之外,你可能也會希望在存檔的時候讓 ESLint 自動幫你修復程式碼,此時你可以這樣設定:

    {
      "editor.codeActionsOnSave": {
        "source.fixAll.eslint": true
      }
    }
    

    如果加上 Prettier 的設定,搭配特定程式語言進行的設定範例如下:

    {
      "[typescript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode",
        "editor.codeActionsOnSave": {
          "source.fixAll.eslint": true
        }
      },
      "[javascript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode",
        "editor.codeActionsOnSave": {
          "source.fixAll.eslint": true
        }
      },
      "[html]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[json]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[jsonc]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "editor.formatOnSave": true
    }
    

加入更嚴格的 TypeScript 檢查規則

  1. 調整 .eslintrc.json 設定檔

    *.tsoverrides 區段加入 eslint:recommended@typescript-eslint/recommended 規則集,但是加入的順序非常重要!

    "extends": [
      "eslint:recommended", // <-- 可以擺在第一位
      "plugin:@angular-eslint/recommended",
      "plugin:@typescript-eslint/recommended", // <-- 一定要放在 @angular-eslint 後面
      "plugin:@angular-eslint/template/process-inline-templates",
      "prettier" // <!-- 一定要在最後關閉 Code formatting 相關的規則
    ],
    
  2. 試跑看看 ng lint 看是不是洋洋灑灑的一堆錯誤與警告,結果通常出人意料! 😅

後記

這篇文章花了好多時間研究與撰寫,雖然都是簡單的東西,但是研究下來還是覺得很多細節與陷阱,跟我前幾天研究 Prettier 的時候一樣,網路上的資訊超超超超級亂,一堆魔鬼般的細節也一直冒出來,不反覆測試真的不能安心的使用。

這篇文章可說是整理得相當完整,更多細節與更多規則的說明,我都放在底下相關連結處,有需要的時候在去查看即可! 👍

相關連結