從零開始配置 react + typescript(二):linters 和 formatter

從零開始配置 react + typescript(一):dotfiles 介紹了一些最早配置的 dotfiles,本篇將繼續介紹 lint 工具 eslintstylelint,代碼格式化工具 prettier,用 husky + lint-staged 來實現每次 commit 時只 lint 修改過的代碼,以及使用 commitlint 來規範化 commit message。javascript

項目地址:react-typescript-boilerplatecss

eslint

Find and fix problems in your JavaScript codehtml

eslint-react-hooks.png

其實社區有不少的 lint 工具,例如 eslint, stylelint, tslint, htmllint, markdownlint 等。lint 工具一方面能夠幫助維護團隊成員保持統一,良好的代碼風格,另外一面能夠幫助咱們檢測出代碼的壞味道,下降 bug 的產生的可能性,提升代碼質量。須要指出的是:lint 工具備必定的格式化能力,可是主要功能不是負責格式化代碼,格式化代碼應該交給專門的格式化工具。 咱們這個項目就將準備使用 prettier 進行代碼格式化。前端

由於是打算使用 TypeScript 來編寫 react,因此要選擇一款支持 TypeScript 的 lint 工具,最流行的支持 TypeScript 的 lint 工具備倆,tslinteslint。去年 2019 年 2 月份 tslint 團隊就宣佈了廢棄 tslint,轉而將維護一系列將 TypeScript 集成到 ESLint 的工具。具體能夠看這個 issue 和這篇博客:TSLint in 2019vue

2020 年我以爲新項目沒有任何理由還去選擇 tslinteslint 的 TypeScript 插件已經算是比較成熟了,雖然仍是有挺多的 bug。java

其實前端絕大多數構建工具都是用 node 編寫模塊來提供 API,有些也會提供命令行工具,本質上就是解析用戶輸入調用 node API,而後還能夠經過配置文件來配置選項,集成插件,而且配置還能夠經過 npm 包來共享。node

eslint 也不例外,配置 eslint 建議使用 eslint 命令行工具提供的交互式配置生成器。不少包既能夠全局安裝,也能夠本地安裝,咱們選擇本地安裝,由於你沒辦法確保別人開發這個項目的時候也全局安裝了,並且這樣還能夠保證都是使用同一版本。react

安裝 eslintwebpack

# -D 參數表示開發依賴
yarn add eslint -D
複製代碼

調用 eslint 自帶的配置生成器:git

npx eslint --init
複製代碼

npx 是 npm 5.2 自帶的一個命令,x 就是和文件類型描述符的那個 x 同樣表示 execute 執行嘛。若是本地安裝了就會用本地的 eslint,沒安裝就去找全局的,全局再沒有就在臨時目錄下載 eslint,用完就刪。用起來比 npm scripts 還方便,傳參數不用像 npm scripts 同樣要在參數前加 --。執行上面的 eslint 初始化命令後會詢問你一系列的問題,關於每個問題的詳細說明能夠看一下這篇文章 Setting up ESLINT in your JavaScript Project with VS Code,這篇文章說的很細。

  • How would you like to use ESLint?

    咱們選擇第三條:To check syntax, find problems, and enforce code style,選擇其它幾條就不會問咱們是否選擇 Google,Airbnb 仍是 Standard 風格了

  • What type of modules does your project use?

    咱們選擇 JavaScript modules (import/export),包括 webpack 配置等 node 腳本咱們都將使用 ts 來編寫,因此選擇 esm

  • Which framework does your project use?

    顯然選擇 react

  • Does your project use TypeScript?

    這一步必定要選 Y,只有告訴初始化器咱們使用 TypeScript,它纔會幫助咱們配置好 TypeScript 的 ESLint parser,相關的 plugins, 以及其它配置

  • Where does your code run?

    這裏咱們 browser 和 node 兩個都選上,由於咱們還要編寫一些 node 代碼

  • How would you like to define a style for your project?

    咱們選第一個 Use a popular style guide

  • Which style guide do you want to follow?

    選擇 Airbnb(愛彼迎)的代碼風格

  • What format do you want your config file to be in?

    咱們選擇最靈活的配置方式:javascript,雖然 js 格式的配置文件比 json 格式的更靈活,可是 js 格式無法使用 VSCode 提供的 JSON validate 功能。

  • Would you like to install them now with npm?

    選擇 Y,當即安裝依賴。雖然咱們用的是 yarn,不該該使用 npm 安裝依賴,用 npm 安裝依賴還會生成對咱們沒有用 package-lock.jsonpackage.lock.jsonyarn.lock 同樣都是用來鎖定依賴版本的。之因此這裏選擇當即安裝依賴是由於你若是不當即安裝依賴,後面你想再用 yarn 安裝依賴的時還要去查一下安裝哪幾個依賴,我以爲很麻煩。

安裝完以後,把 node_modules, package-lock.json, yarn.lock 都刪掉,使用 yarn 從新安裝依賴,再升級到最新版本:

# 安裝依賴
yarn
# 升級到最新版本
yarn upgrade --latest
複製代碼

經過 eslint 自帶的配置生成器咱們生成了 .eslintrc.js

// 格式化後的 .eslintrc.js
module.exports = {
  env: {
    browser: true,
    es6: true,
    node: true,
  },
  extends: ['plugin:react/recommended', 'airbnb'],
  globals: {
    Atomics: 'readonly',
    SharedArrayBuffer: 'readonly',
  },
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 2018,
    sourceType: 'module',
  },
  plugins: ['react', '@typescript-eslint'],
  rules: {},
};
複製代碼

能夠看到相對於非 TypeScript 項目,使用 @typescript-eslint/parser 替換掉了默認的 parser,並添加了 @typescript-eslint 插件。

咱們先作如下修改:

  • 查看 eslint-config-airbnb 的說明,裏面提到,若是要開啓 react hooks 檢查,須要添加 "extends": "airbnb/hooks".eslintrc.js

  • 修改 parserOptions.ecmaVersion 爲 2020,爭作新時代的弄潮兒 😂

  • 查看 @typescript-eslint/eslint-plugin 文檔,裏面提到咱們能夠經過添加 extends: 'plugin:@typescript-eslint/recommended'來開啓它推薦的一些 rules。

  • 爲了讓 eslint-plugin-import 可以正確解析 ts, tsx, json 後綴名,咱們還需指定容許的後綴名,添加 setttings 字段,加入如下配置:

    // .eslintrc.js
    {
        settings: {
            'import/resolver': {
                node: {
                    // 指定 eslint-plugin-import 解析的後綴名
                    extensions: ['.ts', '.tsx', '.js', '.json'],
                },
            },
        },
    }
    複製代碼
  • 爲了讓 eslint-plugin-import 可以正確解析 tsconfig.json 中的 paths 映射,咱們須要安裝 eslint-import-resolver-typescript

    yarn add eslint-import-resolver-typescript -D
    複製代碼

    修改 settings 字段:

    // .eslintrc.js
    {
        settings: {
            'import/resolver': {
                typescript: {
                    // 配置 eslint-import-resolver-typescript 讀取 tsconfig.json 的路徑
                    // 目前用不着,先註釋掉
                    // directory: [resolve('./src/tsconfig.json'), resolve('./scripts/tsconfig.json')],
                },
            },
        },
    }
    複製代碼

添加一些社區中優秀的 eslint 插件:

yarn add eslint-plugin-eslint-comments eslint-plugin-promise eslint-plugin-unicorn -D
複製代碼

eslint-plugin-eslint-comments 是用於 lint eslint 指令註釋,例如檢測出無用的 eslint-disable 註釋。eslint-plugin-promise 按照最佳實踐 lint 你的 promise 代碼,eslint-plugin-unicornsindresorhus 大佬開發的一個 eslint 插件,提供了循環依賴檢測,文件名大小寫風格約束等很是實用的規則集合。

在個人使用中我發現,目前 eslint-plugin-importTypeScript 搭配仍是存在不少的 bug,其中的一個不能忍的 bug 就是import/extensions 這個規則不能正確處理文件後綴名:

import-extension.png

eslint-plugin-import github issue 搜索關鍵字 import/extensions typescript 能夠搜到不少相關的 issues。目前我採用的解決方案是修改 import/extension 的規則配置:

'import/extensions': [
    '2,
    'ignorePackages',
    {
        ts: 'never',
        tsx: 'never',
        json: 'never',
        js: 'never'
    },
],
複製代碼

另一個要提的 bug 就是這個 issue: no-useless-constructor: Cannot read property 'body' of null,簡單來講就是目前在 eslint 搭配 typescript 相關插件時,若是 .d.ts 聲明文件中若是使用了 constructor 就會報這個錯。例如:

declare module 'size-plugin' {
    import { Plugin } from 'webpack';

    interface SizePluginOptions {
        writeFile?: boolean;
    }

    class SizePlugin extends Plugin {
        // 使用了 constructor 就報錯:no-useless-constructor: Cannot read property 'body' of null
        constructor(options?: SizePluginOptions);
    }

    export = SizePlugin;
}
複製代碼

目前我採用的解決辦法時是添加下面兩個規則:

rules: {
    'no-useless-constructor': 'off',
    '@typescript-eslint/no-useless-constructor': 'error',
},
複製代碼

針對 .d.ts 文件咱們還須要要禁用一些規則,咱們後續會在 script 文件夾中實現和 webpack 相關的 node 腳本,針對這個文件夾也調整一些規則:

// .eslintrc.js
{
  overrides: [
        {
            files: ['**/*.d.ts'],
            rules: {
                'import/no-duplicates': OFF,
            },
        },
        {
            files: ['scripts/**/*.ts'],
            rules: {
                'import/no-extraneous-dependencies': OFF,
            },
        },
    ],
}
複製代碼

其它一些我的習慣的規則調整我就不提了,讀者能夠直接去看最終的配置:.eslintrc.js

目前這個配置還存在一些問題,例如不少 rules 會和 prettier 衝突,後面咱們會一一解決這些問題。

stylelint

A mighty, modern linter that helps you avoid errors and enforce conventions in your styles

stylelint

對於 stylelint,我通常都是直接參考 ant design 的 stylint 配置。添加 .stylelintrc.json 到項目根路徑,copy 過來簡單修改一下,:

// .stylelintrc.json
{
    "extends": [
        "stylelint-config-standard",
        "stylelint-config-rational-order",
        "stylelint-config-prettier"
    ],
    "plugins": [
        "stylelint-order",
        "stylelint-declaration-block-no-ignored-properties",
        "stylelint-scss"
    ],
    "rules": {
        "comment-empty-line-before": null,
        "declaration-empty-line-before": null,
        "function-name-case": "lower",
        "no-descending-specificity": null,
        "no-invalid-double-slash-comments": null
    },
     // 加 "**/typings/**/*" 的緣由:https://github.com/stylelint/vscode-stylelint/issues/72
    "ignoreFiles": ["node_modules/**/*", "src/assets/**/*", "dist/**/*", "**/typings/**/*"]
}
複製代碼

src/assets 文件夾準備用來保存一些資源文件,例如第三方的 css 庫,並不須要 lint。VSCode 的 stylelint 插件目前有個 bug,默認竟然會 lint .d.ts 文件而後報錯,因此我也添加了 "**/typings/**/*" 來忽略 .d.ts 文件:

vscode stylint bug

根據上面的配置文件,咱們須要安裝對應的 npm 包:

yarn add stylelint stylelint-config-standard stylelint-config-rational-order stylelint-config-prettier stylelint-order stylelint-declaration-block-no-ignored-properties stylelint-scss -D
複製代碼

和 eslint 同樣,會與 prettier 存在衝突。

prettier

An opinionated code formatter

opinionated 能夠理解爲 專斷專行自覺得是,其實就是說這個格式化器(formatter)不給用戶作選擇,就按照一套社區共識,最佳實踐,最好看的的代碼風格來格式化。具體表現就是提供的選項不多,我數了一下總共恰好 20 個選項。

首先咱們得安裝 prettier

yarn add prettier -D
複製代碼

添加 .prettierrc 到項目根路徑:

{
    "trailingComma": "all",
    "tabWidth": 4,
    "semi": true,
    "singleQuote": true,
    "endOfLine": "auto",
    "printWidth": 100,
    "overrides": [
        {
            "files": "*.md",
            "options": {
                "tabWidth": 2
            }
        }
    ]
}
複製代碼

簡單說明下一些選項這樣配置的緣由:

  • "trailingComma": "all",支持在函數參數中也插入逗號

    prettier-trailing-comma.png

  • "semi": true,我的習慣

  • "singleQuote": true,,我的習慣,少敲一下 shift 難道很差嗎?

  • "endOfLine": "auto",和 editorconfig 同樣,按照操做系統默認的換行符來就好了

  • "printWidth": 100,我以爲默認的最大行寬 80 過短了,浪費編輯器空間

  • 之因此設置 markdown 文件格式化 "tabWidth": 2,是目前 prettier 在格式化 markdown 文件時,會在無序列表中插入多餘的空格

    正常的無序列表應該格式化成:

    - 1
    - 2
    - 3
    複製代碼

    可是不配置 tabWidth 的話, prettier 會格式化成:

    -   1
    -   2
    -   3
    複製代碼

    巨醜 😤

linters 和 prettier 的衝突

這部份內容強烈建議先閱讀 prettier 官方文檔 Integrating with Linters 部分,官方文檔每每是更新最及時,也是最權威的。

咱們知道 lint 工具是用來檢查代碼風格的, prettier 是用來格式化代碼的。想一想看,若是 prettier 設置縮進爲 4 個空格,而咱們配置的 eslint 是要求縮進爲 2 個空格,這確定會致使咱們格式化代碼以後,eslint 會報縮進錯誤。

conflict

這部份內容就是爲了解決 linters 規則和 prettier 的衝突問題,其實,原理很簡單,就是禁用掉那些會和 prettier 格式化起衝突的規則。

安裝 eslint 插件 eslint-config-prettier,這個插件會禁用全部會和 prettier 起衝突的規則。

yarn add eslint-config-prettier -D
複製代碼

添加 'prettier''prettier/react''prettier/@typescript-eslint'extends 配置:

// .eslintrc.js
{
    extends: [
        'airbnb',
        'airbnb/hooks',
        'plugin:eslint-comments/recommended',
        'plugin:import/typescript',
        'plugin:react/recommended',
        'plugin:@typescript-eslint/recommended',
        'plugin:unicorn/recommended',
        'prettier',
        // 專門支持了 eslint-plugin-react
        'prettier/react',
        // 專門支持了 @typescript-eslint/eslint-plugin
        'prettier/@typescript-eslint',
    ],
}
複製代碼

這裏注意要把 prettier 放最後面,由於這樣才能讓 prettier 有機會禁用前面全部的 extends 中配置的會起衝突的規則。

stylelint 也是同樣,先安裝插件 stylelint-config-prettier

yarn add stylelint-config-prettier -D
複製代碼

再將 "stylelint-config-prettier" 添加到 extends 數組最後面:

// .stylelintrc.json
{
    "extends": [
        "stylelint-config-standard",
        "stylelint-config-rational-order",
        "stylelint-config-prettier"
    ],
}
複製代碼

lint-staged

Run linters on git staged files

git-stage.png

咱們每次提交代碼都要對代碼先進行 lint 和格式化,確保團隊的代碼風格統一。爲了達到每次 lint 和格式化時只處理咱們修改了的代碼,也就是保存在 git stage 區(暫存區)的代碼。社區比較流行的方案有倆:

  1. pretty-quick
  2. lint-staged

咱們選擇使用 lint-staged,由於 pretty-quick功能單一,只是提供了 prettier 格式化 stage 區代碼的功能,無法配 eslint 和 stylelint 使用,還不能經過配置文件來配置。lint-satged 更靈活,經過它咱們能夠同時配置 eslintstylelintprettier

爲了達到在咱們每次 commit 的時候,都自動 lint 和格式化,咱們須要給 git commit 掛個鉤子,使用 husky 能夠很輕鬆的給 git 配置鉤子。

先安裝 husky 和 lint-staged:

yarn add husky lint-staged -D
複製代碼

在 package.json 配置 git commit 時的鉤子操做:

// package.json
{
    "husky": {
        "hooks": {
            // 在執行 git commit 調用 lint-staged 命令,lint-staged 會讀取 package.json 中 lint-staged 的配置
            "pre-commit": "lint-staged"
        }
    },
}
複製代碼

再在 package.json 中 "ling-staged" 字段配置 lint-staged:

// package.json
{
    "lint-staged": {
        // 對於 ts,tsx,js 文件調用 eslint
        "*.{ts,tsx,js}": [
            "eslint -c .eslintrc.js"
        ],
        // 對於 css,less,scss 文件調用 stylelint
        "*.{css,less,scss}": [
            "stylelint --config .stylelintrc.json"
        ],
        // prettier 支持不少類型文件的格式化
        "*.{ts,tsx,js,json,html,yml,css,less,scss,md}": [
            "prettier --write"
        ]
    },
}
複製代碼

prettier 的 --write 參數是幹嗎用的呢?舉個 🌰 來講,命令行調用 prettier a.js 默認只會輸出格式化後的代碼到控制檯,不會修改原文件,加上 --write 纔會將格式化後的代碼寫到 a.js。須要注意的一點是,可能大家看別人的教程或者一些項目中他們配置 lint-staged 還加了一個 git add 步驟,而後控制檯會有警告:

⚠ Some of your tasks use git add command.

緣由很簡單:lint-staged 從 V10 版本開始,任何被修改了的原 staged 區的文件都會被自動 git add,因此咱們不須要本身添加 git add 。

commitlint

commitlint helps your team adhering to a commit convention. By supporting npm-installed configurations it makes sharing of commit conventions easy.

commitlint 是一個用來 lint commit message 的工具。看官網的例子:

commitlint

我知道有些人提交代碼喜歡直接來三個點 ...,這是很很差的習慣,這樣你就徹底沒有利用到 commit message,很不利於項目管理。規範化的編寫 commit message 有不少好處,能夠方便咱們檢索提交歷史,配合 conventional-changelog 直接生成 changelog,關聯 github issue 等。

咱們能夠經過 husky + commlint 實如今 commit 的時候先檢查 commit message 的規範性,若是不符合規範直接終止 commit。

安裝須要的依賴:

yarn add @commitlint/cli @commitlint/config-conventional -D
複製代碼

@commitlint/config-conventional 是 commitlint 官方推薦的一個 angular 風格的 commitlint 配置,提供了少許的 lint 規則,相似於 eslint 的 extend。

它默認支持的提交類型爲:

["build", "ci", "chore", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test"]
複製代碼

添加 commlint 的配置到項目根目錄的 .commitlintrc.js

// .commitlintrc.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      // 比默認值多了個 deps,用於表示依賴升級,降級,新增等提交
      ['build', 'ci', 'chore', 'deps', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test'],
    ],
  },
};
複製代碼

添加 git commit-msg 鉤子:

// package.json
{
    "husky": {
        "hooks": {
            "pre-commit": "lint-staged",
            "commit-msg": "commitlint -c .commitlintrc.js -E HUSKY_GIT_PARAMS"
        }
    },
}
複製代碼

當調用 commit-msg 鉤子的時候,環境變量 HUSKY_GIT_PARAMS 會被臨時設置爲保存 commit messsge 的文件的路徑,而後 commitlint 就會去 lint 這個文件中的 commit message。

若是你想在命令行中交互式的編輯 commit message,能夠了解一下 commitizen ,咱們這個項目就不配了,主要仍是以爲要配置的話就要根據具體的業務去配,咱們這個通用目的的模板項目就算了。我看了一下 angularvue-next lint commit message 的作法,它們 commitlint 和 commitizen 倆都沒配,只是在 git commit-msg 時調用了下 node 腳本校驗 commit message 。

咱們接着再配置自動生成 changelog,本地安裝 conventional-changelog-cli

yarn add conventional-changelog-cli -D
複製代碼

添加一個 npm script:

// package.json
"scripts": {
    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
}
複製代碼

這樣咱們就能夠經過 npm run changelog 生成 angular 風格的 changelog 了,conventional-changelog 會讀取提交歷史中 fix, feat 等 type 的 commit message 自動生成 changelog。

咱們接着討論一個使用了 commilint 後如何插入 emoji 的問題,咱們知道 commit message 的格式是這樣的:

// 整行叫 header
<type>(<scope>): <subject>
// 空一行
<body>
// 空一行
<footer>

// 舉個例子,某次提交的 commit message 是:feat(component): add component Navbar
// feat 是 type
// component 是 scope
// 'add component Navbar' 是 subject
// 這裏沒有 body 和 footer
複製代碼

咱們知道 git emoji 的格式是:

:emoji_string:
複製代碼

若是你使用下面的帶 emoji 的 commit message 提交:

git commit -m ':bug: fix: xxx'
複製代碼

commitlint 等工具在解析的時候應該是將第一個冒號以前的內容解析爲 type,也就是說會把 emoji 左邊冒號以前的內容解析爲 type,那這樣解析的話 type 就是空字符串了,因此使用上面的 commit message 提交會報錯說你沒有填寫 type。

若是不修改 commilint 的 type 配置是沒法經過 commitlint 的,解決辦法之一是添加一個 type :bug: fix,可是這樣的話 conventional-changelog-cli 不會將 commit mesage 提取到 changelog,它只認 fix: xxx 不認 :bug: fix: xxx。所以,在當前配置下,咱們若是要插入 emoji,建議使用下圖的方式,雖然我以爲這樣很差看,但目前來講是比較折中的方案。

git commit -m "chore: :memo: improve docs and config json"
複製代碼

commitlint git emoji

second commit

添加幾個經常使用用於 lint 的 npm scripts:

{
    "scripts": {
        "lint": "yarn run lint-eslint && yarn run lint-stylelint",
        "lint-eslint": "eslint -c .eslintrc.js --ext .ts,.tsx,.js {src,scripts}/**/*.{ts,tsx,js}",
        "lint-stylelint": "stylelint --config .stylelintrc.json src/**/*.scss --syntax scss",
    }
}
複製代碼

能夠看到我配置 eslint 和 stylelint 的 script 是用 前綴-參數 的形式,有些項目配置帶參數的 script 名是用 前綴:參數 的形式,也就是用冒號作分隔符。我以爲那樣很差,由於有些工具支持 yarn:scriptName 的形式來執行 npm scripts,例如 concurrently

假設你有多個 npm scripts,分別是:yarn:watch-nodeyarn:watch-nodeyarn:watch-css,這個工具支持一條命令來並行執行它們:

concurrently yarn:watch-node yarn:watch-js yarn:watch-css
複製代碼

那你說若是用冒號來作分隔符,那要寫就是:

concurrently yarn:watch:node yarn:watch:js yarn:watch:css
複製代碼

看起來就很迷,不瞭解的人可能還覺得後面的冒號也是 concurrently 的參數呢,因此表示帶參數的 npm script 不要用冒號作分隔符

最後再來一發 yarn upgarde --latest,養成天天升級依賴的好習慣,避免之後同時升級不少依賴出了都搞不清楚是哪一個依賴升級致使的。不過公司的項目千萬別這樣搞,容易致使出 bug 連續加班。

到這裏,從零開始配置 react + typescript 系列第二篇算是差很少了,再一次提交代碼:

git add -A
git commit -m 'build: integrate eslint, stylelint, prettier, lint-staged, commi tlint'
# 上次 push 的時候使用 -u 參數關聯了 master 分支和 github 遠程倉庫,這裏就能夠直接 push
git push
複製代碼

第二篇到此結束,第三篇關於 webpack 配置的文章將是四篇中乾貨最多,估計也是最長的一篇。將介紹使用 TypeScript 來編寫 express + webpack devServer 中間件 做爲 devServer,集成一些實用和酷炫的 webpack 插件,優化 babel 配置,生產環境打包優化等內容。

要想了解更多細節,建議直接看源碼,項目地址:react-typescript-boilerplate。若是以爲本文對你有用,不妨賞顆 star 😁。對本文內容有疑問或者有什麼改進的地方歡迎經過評論和郵件交流。

本文爲原創內容,首發於我的博客,轉載請註明出處。

相關文章
相關標籤/搜索