工欲善其事必先利其器(前端程式碼規範)

語言: CN / TW / HK

theme: channing-cyan highlight: atelier-cave-dark


前言

工欲善其事,必先利其器。 —— 《論語》

具體到寫程式碼則是需要一套趁手的工具和完整的工作流程,簡單講就是程式碼產出規範以及自動幫我們進行規範約束的工具

準備

新建專案資料夾,新增index.js寫入以下內容 javascript alert('test') console.log('test') eval()

進入資料夾終端,執行以下命令 shell npm i -g pnpm pnpm init

統一包管理器

package.jsonscripts 項增加 preinstall 鉤子限制專案使用的包管理器,如 npx only-allow pnpm -y 限制使用 pnpm

關於 scripts 鉤子可以參考官網文件,對 only-allow 感興趣的可以看這篇分析:only-allow 原始碼學習 ```json "scripts": { "preinstall": "npx only-allow pnpm -y" }

```

ESlint

安裝 eslint shell pnpm i -D eslint 新增 .eslintrc 檔案,寫入以下 rules json { // rules 取值 0、1、2,分別是不處理、警告、禁止 "rules": { "no-eval": 2, "no-alert": 1, // 禁止使用alert confirm prompt "no-console": 0 } } 執行 pnpm eslint "**/*.js",得到以下校驗結果 ``` 1:1 warning Unexpected alert no-alert 3:1 error eval can be harmful no-eval

✖ 2 problems (1 error, 1 warning) ```

Typescript 支援

shell pnpm i -D typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin 新增 main.ts檔案,輸入以下內容 typescript const toString = Object.prototype.toString export function is(val: unknown, type: any): boolean { return toString.call(val) === `[object ${type}]` } alert('test') console.log('s') eval('') .eslintrc修改如下 json { "parser": "@typescript-eslint/parser", "plugins": [ "@typescript-eslint" ], "rules": { "@typescript-eslint/no-explicit-any": 2,// 禁用any "no-eval": 2, "no-alert": 1, // 禁止使用alert confirm prompt "no-console": 0 } } 執行 pnpm eslint "**/*.ts" 得到以下校驗結果 ``` 2:40 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 5:1 warning Unexpected alert no-alert 7:1 error eval can be harmful no-eval

✖ 3 problems (2 errors, 1 warning) `` 從上面的例子可以看到ESLint` 還是挺容易配置的,為了方便後面配置,先來了解一些常用配置項

常用配置詳解

詳細說明請參考 ESLint 配置

parser 和 parserOptions 解析器及其配置

parser作為主解析器使用,parserOptions則是針對解析器的一些補充配置,如parserOptions.parser則可以配置子解析器,也可以針對不同型別檔案配置不同的解析器

.vue 檔案來舉例,使用 vue-eslint-parser 進行解析,檔案內不同的內容再使用不同解析器 JSON { "parser": "vue-eslint-parser", "parserOptions": { "parser": { "js": "espree", "ts": "@typescript-eslint/parser", "<template>": "espree", } } } 有些庫因其內部做了處理只需要配置 parser 即可,比如parser 指定為 @typescript-eslint/parser 也可以處理 js,比如前面的例子在配置 Typescript 支援後執行 pnpm eslint "**/*.js" 命令同樣可以得到檢測結果

parserOptions.project

由於專案中可能會使用別名(resolve.alias)或者有其特殊配置,所以需要指定對應配置才能正確解析

比如下面的配置 @typescript-eslint/parser 解析器會根據 project 指定的 tsconfig.json 的配置對 ts 進行解析,這樣對於 ts 檔案內路徑別名 @ 才能正確解析其路徑 ```json // .eslintrc "plugins": ["@typescript-eslint"], "parserOptions": { "parser": "@typescript-eslint/parser", "project": "./tsconfig.json" },

// tsconfig.json { "compilerOptions": { "baseUrl": "./", "paths": { "@/": ["src/"] } }, "include": ["src"], "exclude": ["node_modules"] } 還有一些常用的 `parserOptions` 配置如 `ecmaVersion`指定按照哪個 `ecma` 版本規範對`js`解析, `sourceType` 用來指定按照哪種模組規範解析`js`模組 "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" } ```

extends 和 plugins

不想自己配置繁瑣的規則可以使用 extends 去繼承對應外掛的規則,extends可以是字串也可以是字串陣列,關於ESLint是如何解析和應用extends欄位的可以參看原始碼applyExtends

eslint: 開頭的如 eslint:recommended 表示使用 eslint 推薦規則,也有無字首的如 extends: "airbnb",這是繼承 eslint-config-airbnb規則的意思

plugin: 開頭的則表示使用外掛拓展規則,如 plugin:jsdoc/recommended 表示使用 jsdoc 外掛的推薦規則

需要自定義規則或者extends對應規則時一般來講需要配置對應 plugins 以拓展規則,外掛名稱可以省略 eslint-plugin- 字首

如果 rules或者extends 配置了外掛的規則而 plugins 沒指定對應外掛,觸發 lint 時可能會因讀取不到對應規則而提示失敗。

以 eslint-plugin-jsdoc 為例

shell pnpm i -D eslint-plugin-jsdoc .eslintrc 修改如下 json { "parser": "@typescript-eslint/parser", "plugins": [ "@typescript-eslint" ], "extends": [ "plugin:jsdoc/recommended" ], "rules": { "no-eval": 2, "no-alert": 1, // 禁止使用alert confirm prompt "no-console": 0, "@typescript-eslint/no-explicit-any": 2 } } 執行 pnpm eslint "**/*.ts" 得到校驗結果可以看到看到 jsdoc的規則提示

``` 2:8 warning Missing JSDoc comment jsdoc/require-jsdoc 2:40 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 5:1 warning Unexpected alert no-alert 7:1 error eval can be harmful no-eval

✖ 4 problems (2 errors, 2 warnings) 0 errors and 1 warning potentially fixable with the --fix option. `` 不過這裡有個小細節就是plugins配置項沒有配置jsdoc對應的外掛只配置了extends,這是plugins機制,自動載入以eslint-plugin-` 為字首的外掛

overrides

overrides通過 files 可以指定不同檔案或資料夾進行鍼對性配置,且其擁有和基礎配置項幾乎一樣的配置,如parserOptionsextendsplugins

舉個例子,比如只針對utils.ts設定禁用 anymain.ts不限制,utils.ts內容和main.ts一樣

.eslintrc 修改如下 json { "parser": "@typescript-eslint/parser", "overrides": [ { "files": [ "utils.ts" ], "extends": [ "plugin:jsdoc/recommended" ], "rules": { "@typescript-eslint/no-explicit-any": 2, "no-alert": 1 } } ], "plugins": [ "@typescript-eslint" ], "rules": { "no-eval": 2, "no-alert": 1 } } 執行 pnpm eslint "**/*.ts" 結果如下

``` // main.ts 5:1 warning Unexpected alert no-alert 7:1 error eval can be harmful no-eval

// utils.ts 2:8 warning Missing JSDoc comment jsdoc/require-jsdoc 2:40 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 5:1 warning Unexpected alert no-alert 7:1 error eval can be harmful no-eval

✖ 6 problems (3 errors, 3 warnings) 0 errors and 1 warning potentially fixable with the --fix option. 在 `utils.ts` 正確書寫才能通過校驗,[jsdoc 文件](https://www.jsdoc.com.cn/)javascript /* * 型別檢測 * * @param {any} val 檢測物件 * @param {string} type 型別 * @returns {boolean} 是否屬於 type 型別 / export function is(val: unknown, type: string): boolean { return toString.call(val) === [object ${type}] } ```

rules

rules有多種寫法 - 單取數字值: 0、1、2,分別是關閉、警告、禁止 - 字串:"off"、"warn"、"error"和上面0、1、2對應 - 陣列,第一項可取前兩種取值,第二項為該規則的具體配置,不同規則可能支援不同拓展寫法,更新詳細說明請參照ESlint rules 文件以及對應規則說明 ```JSON "rules": { "no-alert": 1, "quotes": ["error", "double"], "no-console": [ "error", { "allow": ["log", "warn", "error", "info"] } ], }

```

globals

有時可能需要一些全域性變數的設定以處理 JSON "globals": { "__DEV__": false, "__dirname": false, "define": true, "history": true, "location": true, "wxjs": true, "$": true, "WeixinJSBridge": true, "wx": true, "process": true, "qq": true }, 另外 ESLint 還提供 .eslintignore 檔案用來遮蔽不需要校驗的檔案

Stylelint

Stylelint的配置和 ESLint 實際上是一個思路,這裡就不詳細介紹了,詳細的請參照Stylelint 文件

css

shell pnpm i -D stylelint stylelint-config-standard 新增index.css,新增 .stylelintrc 檔案,寫入以下配置 ```json { "extends": [ "stylelint-config-standard" ], "rules": { "no-empty-first-line": true } }

```

執行 pnpm stylelint "**/*.css" 得到校驗結果 ``` index.css 1:1 ✖ Unexpected empty source no-empty-source

1 problem (1 error, 0 warnings) ```

Scss & Less

shell pnpm i -D stylelint-scss stylelint-config-recommended-scss pnpm i -D stylelint-less stylelint-config-recommended-less

.stylelintrc 檔案增加 overrides 配置 ```json { "extends": ["stylelint-config-standard"], "overrides": [ { "extends": "stylelint-config-recommended-scss", "files": ["/*.scss"] }, { "extends": "stylelint-config-recommended-less", "files": ["/*.less"] } ], "rules": { "no-empty-first-line": true }, }

分別建立對應檔案執行`pnpm stylelint "**/*.{css,scss,less}"`得到以下結果 When linting something other than CSS, you should install an appropriate syntax, e.g. "postcss-less", and use the "customSyntax" option

index.css 1:1 ✖ Unexpected empty source no-empty-source

index.less 1:1 ✖ Unexpected empty source no-empty-source

index.scss 1:1 ✖ Unexpected empty source no-empty-source

3 problems (3 errors, 0 warnings) `` 提示less需要在配置項customSyntax配置postcss-less`轉換器

安裝postcss-less並更改配置如下

json { "extends": [ "stylelint-config-standard" ], "overrides": [ { "extends": "stylelint-config-recommended-scss", "files": [ "**/*.scss" ] }, { "extends": "stylelint-config-recommended-less", "customSyntax": "postcss-less", "files": [ "**/*.less" ] } ], "rules": { "no-empty-first-line": true } } 再次執行 pnpm stylelint "**/*.{css,scss,less}" 關於customSyntax的配置提示消失

customSyntax是自定義css語法轉換外掛的配置,比如 scss也有 postcss-scss,詳細說明參照customSyntax 文件

stylelint-config-recess-order

stylelint的規範推薦加上自動排序規則stylelint-config-recess-order shell pnpm i -D stylelint-config-recess-order .stylelintrc 更改如下 ```json { "extends": ["stylelint-config-standard", "stylelint-config-recess-order"], "overrides": [ { "extends": ["stylelint-config-recommended-scss", "stylelint-config-recess-order"], "files": ["/*.scss"] }, { "extends": ["stylelint-config-recommended-less", "stylelint-config-recess-order"], "customSyntax": "postcss-less", "files": ["/*.less"] } ], "rules": { "no-empty-first-line": true } }

`` 從這裡的配置可以看到overrides裡每一項的extends` 需要單獨配置完整的繼承關係

ESLint 一樣,Stylelint 提供 .stylelintignore 檔案用來遮蔽不需要校驗的檔案

prettier

ESLintStylelint 是對jscss進行語法規範,程式碼風格則可以交給prettier來處理 shell pnpm i -D prettier eslint-config-prettier eslint-plugin-prettier 新增 .prettierrc 檔案 json { "printWidth": 140, "singleQuote": true, "semi": false, "trailingComma": "none", "bracketSameLine": true, "arrowParens": "avoid", "htmlWhitespaceSensitivity": "ignore", "overrides": [ { // rc 檔案按照 json進行格式化 "files": [".prettierrc", ".eslintrc", ".stylelintrc", ".lintstagedrc"], "options": { "parser": "json" } } ] } prettier 同樣提供 .prettierignore 檔案用來遮蔽不需要格式化的檔案

VScode 配置

當然以上各種 Lint手動校驗實在太累,為了方便可以配合編輯器設定自動格式化,以 VScode 為例

安裝對應外掛 - Prettier - Code formatter - ESLint - Stylelint - Vetur(vue2) - Vue Language Features (Volar) - TypeScript Vue Plugin (Volar)(如果使用 Vue3 + TypeScript開發 )

新增 .vscode 資料夾,建立 settings.json 配置檔案,寫入如下簡單配置

注意: eslint.workingDirectories 配置項最好指定當前專案的eslint配置檔案,這樣編輯器的eslint才會根據專案配置進行校驗提示 ```json { "extensions.ignoreRecommendations": false, "editor.tabSize": 2, "typescript.updateImportsOnFileMove.enabled": "always", "javascript.updateImportsOnFileMove.enabled": "always", "javascript.format.insertSpaceBeforeFunctionParenthesis": true, "editor.fontSize": 14, "editor.formatOnType": true, "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": true, "source.fixAll.stylelint": true }, "editor.defaultFormatter": "esbenp.prettier-vscode", "[vue]": { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode" }, "prettier.semi": false, //結尾分號 "prettier.trailingComma": "none", //結尾逗號 "prettier.singleQuote": true, "stylelint.validate": ["css", "less", "postcss", "scss", "vue", "sass"], "eslint.workingDirectories": [".eslintrc", { "mode": "location" }],// 這裡需要指定下 .eslintrc ,讓編輯器的eslint根據配置去執行校驗提示 "eslint.validate": ["vue", "typescript", "javascript", "javascriptreact"]

} ```

Vue

vue 生態鏈中提供了eslint-plugin-vue庫,eslint-plugin-vue 官網 也有詳細配置說明,根據說明這裡直接選擇 vue-eslint-parser shell pnpm i -D vue-eslint-parser eslint-plugin-vue .eslintrcoverrides 配置中新增對應 vue的配置

```JSON { "parser": "vue-eslint-parser", "parserOptions": { "ecmaVersion": "latest", "sourceType": "module", "ecmaFeatures": {"jsx": true } }, "plugins": ["@typescript-eslint"], "extends": ["prettier"], "overrides": [ { "files": ["/*.{ts,tsx}"], "extends": ["plugin:jsdoc/recommended"], "parserOptions": { "sourceType": "module", "parser": "@typescript-eslint/parser" }, "rules": { "@typescript-eslint/no-explicit-any": 2, "no-alert": 1 } }, { "files": "/*.vue", "extends": ["plugin:vue/vue3-recommended", "prettier"], "rules": { "@typescript-eslint/no-explicit-any": 2, "no-alert": 2 }, "parserOptions": { "sourceType": "module", "parser": "@typescript-eslint/parser" } } ], "rules": { "no-eval": 2, "no-alert": 1 } }

`` *注意*:執行pnpm eslint /*.vue`進行測試,配置沒錯的情況下如果有 Failed to load plugin xxx ... Class extends** 之類的報錯大概率是版本相容問題

目前 stylelint 是 14.x 版本,在 vue3.vue 檔案中有 Unknown word (CssSyntaxError)錯誤,解決方案 - stylelint降級到 13 版本以下 ,但是相關的外掛都需要處理,相對麻煩 - 新增 stylelint-config-html外掛來處理

第二種方案比較簡單,安裝相應外掛 shell pnpm i -D postcss-html stylelint-config-html

.stylelintrc 新增 overrides 配置 ```json { "extends": [ "stylelint-config-standard", "stylelint-config-recommended-less", "stylelint-config-recommended-scss", "stylelint-config-html/vue", "stylelint-config-recess-order" ], "customSyntax": "postcss-html", "files": [ "*/.vue" ] }

`` 這裡直接配置lessscss支援,如不需要都支援只需把extends`中對應的規則刪除即可

vue有許多社群模板可以參考

React

既然 Vue的配置搞懂了 React 的自然不在話下,所以就不再介紹了,參照React ESLint 外掛文件eslint-plugin-react 配置即可

css-in-js相關的,如styled-component對應的 lint 有 - stylelint-processor-styled-components(已棄用) - eslint-plugin-styled-components-a11y - eslint-plugin-styled-components-css

規範程式碼提交

程式碼提交也是很重要的一環,這個環節主要是保證提交上來的程式碼是符合規範的,包括前文配置的lint規範、提交日誌規範、測試用例等,為此需要定製git hooks在程式碼提交上來之前處理好

lint-staged

lint-staged可以來處理不同檔案型別需要執行的lint或其他命令 shell pnpm i -D lint-staged package.json 中新增lint-staged內容,比如下面針對 less等專案檔案的格式化和規範處理 ```json "lint-staged": { "/*.less": "stylelint --syntax less", "(|!test)/*.{jsx,js,tsx,ts,less,md,json,vue}": [ "prettier --write", "git add" ] },

也可以使用 `.lintstagedrc` 檔案來配置,如json { "/src//*.{js,jsx,ts,tsx,vue}": ["eslint --cache --fix", "prettier --write"], "/src//.{css,scss,less}": ["stylelint --cache --fix", "prettier --write"], "/.md": "prettier --write" }

```

commitlint

規範提交日誌對問題定位和程式碼回滾有較大的意義

首先當前專案未初始化 git 的先執行 git init 初始化 git

commitizen

使用commitizencz-conventional-changelog來規範日誌格式 ```shell

全域性安裝 commitizen,可能需要根據錯誤提示執行 pnpm setup ,需重新載入終端或編輯器

pnpm i -g commitizen

初始化 cz,會在 package.json 生成commitizen的配置

commitizen init cz-conventional-changelog --pnpm --save-dev --save-exact

需要中文的新增 cz-conventional-changelog-zh

pnpm i -D cz-conventional-changelog-zh `commitizen init cz-conventional-changelog --pnpm --save-dev --save-exact` 會在 `package.json` 生成如下配置json "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" } } `` 使用cz-conventional-changelog-zh的將path改為./node_modules/cz-conventional-changelog-zh`即可

執行 git cz 會有如下提示(這裡使用了中文包),根據提示選擇、輸入內容即可 ``` ? 選擇您要提交的更改型別: (Use arrow keys)

feat: 一個新功能 fix: 一個bug docs: 文件增刪改 style: 樣式修改(空白、格式、缺少分號等) refactor: 既不修復bug也不新增新功能的更改 perf: 效能優化 test: 增加測試 (Move up and down to reveal more choices) ```

自定義提交規範

新增 commitizen 配置檔案 .cz-config.js

這裡講講自實現方式,新增 commit-rules.js 寫入自定義規則如下

```javascript

var configLoader = require('commitizen').configLoader;

const types = [ { "key": "new-type", "description": "新型別", "title": "new-type" }, { "key": "feat", "description": "一個新功能", "title": "Features" }, { "key": "fix", "description": "一個bug", "title": "Bug Fixes" }, ] var config = configLoader.load() || {}; /* * 互動收集 commit 資訊外掛 * * @param cz 提供基礎的互動能力 * @param commit 相當於 git commit / function prompter (cz, commit) { var length = types.length var defaultType = process.env.CZ_TYPE || config.defaultType var defaultScope = process.env.CZ_SCOPE || config.defaultScope var choices = types.map(function ({ key, description }) { return { name: (key + ':').padEnd(length) + ' ' + description, value: key }; }); cz.prompt([ { type: 'list', name: 'type', message: '選擇您要提交的更改型別:', choices: choices, default: defaultType }, { type: 'input', name: 'scope', message: '這個變化的範圍是什麼(例如元件或檔名):(按回車鍵跳過)', default: defaultScope, filter: function (value) { return value.trim(); } }, { type: 'maxlength-input', name: 'subject', message: '寫一個簡短的修改描述(最多20個字元):\n', maxLength: 20, validate: function (subject, answers) {

    return subject.length == 0
      ? '缺少修改描述'
      : subject.length <= 20
        ? true
        : '描述內容的長度必須小於或等於20'
  },
}

]).then(answers => { const { type, scope, subject } = answers var messageScope = scope ? '(' + scope + ')' : ''; const message = ${type}${messageScope}: ${subject} time: ${new Date().getTime()}

commit(message)

})

} module.exports = { prompter } ```

.cz-config.js 寫入如下內容

```json { "path": "./commit-rules" }

另外專案可能要求只需要本地安裝即可,這樣 `git cz` 命令就沒法使用,可以換成當前專案內安裝`commitizen`, `package.json` 新增 `scripts`json "scripts": { "am": "git add . & git-cz", "cm": "git-cz" },

執行 `pnpm am` 得到自定義規則互動提示

git add . & git-cz

[email protected], [email protected]

? 選擇您要提交的更改型別: (Use arrow keys)

new-type: 新型別 feat: 一個新功能 fix: 一個bug `` 使用git czpnpm git-cz代替git commit` 用來生成符合規範的 Commit message

husky

然後就是定製git hooks流程,在 .git/hooks 目錄下已經有許多鉤子的例子,只需去掉 .sample 字尾鉤子便可生效

當然使用 husky 則方便很多

```shell

安裝 husky

pnpm i -D husky

未初始化git的需要先初始化git,否則husky會初始化失敗

git init

初始化husky

pnpm husky install

新增鉤子檔案

npx husky add .husky/pre-commit npx husky add .husky/commit-msg `commit-msg` 檔案寫入以下內容

!/bin/sh

. "$(dirname "$0")/_/husky.sh"

校驗通過互動收集到的 commit 是否符合規範

pnpm commitlint --edit $1 新增 `commitlint.config.js` 寫入如下內容,因為前面自定義規範的時候格式改動規則需要相應變動,否則會不通過javscript module.exports = { "extends": ['@commitlint/config-conventional'], "rules": { 'subject-empty': [0, 'never'], 'type-empty': [0, 'never'], "type-enum": [ 2, "always", [ "new-type" ] ] } } `` 此時執行pnpm am` 根據互動提示最終通過自定義規則可以提交成功

pre-commit可以參考如下內容 ```

!/bin/sh

. "$(dirname "$0")/_/husky.sh"

執行 postinstall 鉤子

pnpm run postinstall

校驗lint

npx lint-staged

執行測試用例

pnpm test

`` 最後是測試lint-staged`

先把pre-commit內暫時不存在的命令或鉤子移除,執行 pnpm am,會看到觸發了 lint-staged 指令碼對檔案進行了校驗,有些不需要檢驗的檔案可以在.eslintignore.stylelintignore.prettierignore增加遮蔽配置或者完善lint-staged匹配規則即可

到此基本過完了所有規範制定和工具整合配置,還是相當繁瑣的,尤其是各種外掛的組裝可能會有各種莫名其妙的報錯(大概率是版本問題)

嚴格還是寬鬆,如何選擇?

有了整個架子就可以按照規範需要去設定對應規則了,至於一個專案要配置多嚴格的規範,我覺得這要看團隊的共識,總的來說有以下兩個方向

20230119-154715.jpg

R-C.png

最終選擇感覺主要還是看leader的態度。

還有些更嚴格的會配置進webpack等開發工具中時時影響開發過程,個人認為那是在添堵

附錄

json "devDependencies": { "@typescript-eslint/eslint-plugin": "^5.49.0", "@typescript-eslint/parser": "^5.49.0", "cz-conventional-changelog": "^3.3.0", "cz-conventional-changelog-zh": "^0.0.2", "eslint": "^8.32.0", "eslint-config-prettier": "^8.6.0", "eslint-plugin-jsdoc": "^39.6.8", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-vue": "^9.9.0", "husky": "^8.0.3", "lint-staged": "^13.1.0", "postcss-html": "^1.5.0", "postcss-less": "^6.0.0", "prettier": "^2.8.3", "stylelint": "^14.16.1", "stylelint-config-html": "^1.1.0", "stylelint-config-recess-order": "^3.1.0", "stylelint-config-recommended-less": "^1.0.4", "stylelint-config-recommended-scss": "^8.0.0", "stylelint-config-standard": "^29.0.0", "stylelint-less": "^1.0.6", "stylelint-scss": "^4.3.0", "typescript": "^4.9.4", "vue-eslint-parser": "^9.1.0" }, "dependencies": { "vue": "^3.2.45" }, "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog-zh" } }

下一篇:工欲善其事必先利其器(配置Vue3 + ts專案模板)