前言
在一家小公司實(shí)習(xí)了一段時(shí)間,接手的項(xiàng)目代碼格式及規(guī)范化不可描述,為了保證項(xiàng)目的可維護(hù)性,決定接入Typescript
,同時(shí)采用Eslint
和Prettier
進(jìn)行代碼規(guī)范化,為下一步的CodeReview
。
Typescript是強(qiáng)類型語言,接入Typescript一開始會(huì)有很大的痛點(diǎn),但是過了一陣子就可以享受到Typescript帶來的好處,bug減少了,代碼易讀了,也可維護(hù)了,好處網(wǎng)上一大把就不累贅了。
常用的代碼格式化工具主要有ESlint
、TSLint
、StandardJS
。TS官方已經(jīng)決定棄用TSLint
,全面擁抱ESLint。因此在技術(shù)選型方面將采用ESlint
。
Eslint
的主要功能包含代碼格式的校驗(yàn),代碼質(zhì)量的校驗(yàn),JS規(guī)范,如用===
而不是==
判斷相等、用駝峰命名變量而不是用下劃線。而 Prettier
是美麗的意思,只是代碼格式的校驗(yàn)(并格式化代碼),不會(huì)對代碼質(zhì)量進(jìn)行校驗(yàn),如單行代碼長度、tab長度、空格、逗號表達(dá)式等問題。
一、安裝相關(guān)依賴
1.1 安裝 Typescript
推薦使用全局安裝,可以在其他項(xiàng)目中也使用TS。
npm install -g typescript
1.2 安裝聲明文件
所需的 react
, react-dom
的聲明文件, 以及 加載TS的ts-loader
npm install --save-dev @types/react @types/react-dom ts-loader
1.3 配置 tsconfig.json
在使用Typescript
時(shí)需要根據(jù)實(shí)際項(xiàng)目的需要進(jìn)行相關(guān)規(guī)則的配置,具體配置根據(jù)項(xiàng)目而異、可參考官網(wǎng),具體看這里TS官網(wǎng)。我的配置項(xiàng)如下所示:
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"noUnusedParameters": true,
"outDir": "build/dist",
"baseUrl": ".",
"strict": true,
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true,
"forceConsistentCasingInFileNames": true,
"strictPropertyInitialization": true,
"experimentalDecorators": true,
"noImplicitReturns": true,
"moduleResolution": "node",
"strictNullChecks": true,
"esModuleInterop": true,
"noUnusedLocals": true,
"importHelpers": true,
"noImplicitThis": false,
"suppressImplicitAnyIndexErrors": false,
"skipLibCheck": true,
"noResolve": false,
"module": "es2015",
"allowJs": true,
"target": "es5",
"jsx": "react",
"lib": [
"es5",
"es2015",
"dom",
"es7",
"es2018"
],
"paths": {
"@/*": [
"./src/*"
]
}
},
"exclude": [
"node_modules",
"build",
"scripts",
"acceptance-tests",
"webpack",
"jest",
"src/setupTests.ts",
"tslint:latest",
"tslint-config-prettier"
]
}
二、ESLint+Prettier代碼規(guī)范
針對JS
項(xiàng)目遷移到TS
的項(xiàng)目,主要有兩種選擇ESLint
和TSLint
。TSLint
僅針對TS
代碼,因此如果采用TSLint
規(guī)范TS
代碼,JS
代碼需要采用其他工具。而ESLint
不僅能規(guī)范js
代碼,通過配置解析器,也能規(guī)范TS
代碼。此外由于性能問題,TypeScript
官方?jīng)Q定全面采用ESLint
。因此本項(xiàng)目采用ESLint
配合Prettier
規(guī)范化TS
和JS
代碼。
2.1 ESLint規(guī)范TS代碼
2.1.1 安裝ESLint依賴
npm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev
eslint
: ESLint的核心代碼@typescript-eslint/parser
:ESLint的解析器,用于解析Typescript文件,從而檢查和規(guī)范Typescript代碼@typescript-eslint/eslint-plugin
:這是一個(gè)ESLint插件,包含了各類定義好的檢測Typescript代碼的規(guī)范
安裝好依賴后,需要在項(xiàng)目根目錄中的.eslintrc.js中配置,包括解析器、繼承的代碼規(guī)范、插件和環(huán)境:
module.exports = {
parser: '@typescript-eslint/parser', //定義ESLint的解析器
extends: ['plugin:@typescript-eslint/recommended'],//定義文件繼承的子規(guī)范
plugins: ['@typescript-eslint'],//定義了該eslint文件所依賴的插件
env:{ //指定代碼的運(yùn)行環(huán)境
browser: true,
node: true,
}
}
2.2 規(guī)范React代碼
項(xiàng)目是以依賴于React的AntDesignPro為基礎(chǔ),因此需要安裝規(guī)范React代碼的Eslint插件。
2.2.1安裝插件
npm i eslint-plugin-react --save-dev
2.2.2 配置React規(guī)范
然后修改.eslintrc.js的配置,如下所示:
module.exports = {
parser: '@typescript-eslint/parser',
extends: [
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended'
],//使用推薦的React代碼檢測規(guī)范
plugins: ['@typescript-eslint'],
env:{
browser: true,
node: true,
},
settings: {//自動(dòng)發(fā)現(xiàn)React的版本,從而進(jìn)行規(guī)范react代碼
"react": {
"pragma": "React",
"version": "detect"
}
},
parserOptions: {//指定ESLint可以解析JSX語法
"ecmaVersion": 2019,
"sourceType": 'module',
"ecmaFeatures":{
jsx:true
}
}
rules: {
//在Rules中可以自定義你的React代碼編碼規(guī)范。
}
}
2.3接入Prettier
2.3.1 安裝依賴
npm install prettier eslint-config-prettier eslint-plugin-prettier -g
prettier
:prettier插件的核心代碼eslint-config-prettier
:解決ESLint中的樣式規(guī)范和prettier中樣式規(guī)范的沖突,以prettier的樣式規(guī)范為準(zhǔn),使ESLint中的樣式規(guī)范自動(dòng)失效eslint-plugin-prettier
:將prettier作為ESLint規(guī)范來使用
2.3.2 配置規(guī)則
在項(xiàng)目的根目錄下創(chuàng)建.prettierrc.js文件并配置prettier代碼檢查規(guī)則
module.exports = {
//最大長度80個(gè)字符
printWidth: 80,
//行末分號
semi: false,
//單引號
singleQuote: true,
//盡可能使用尾隨逗號(包括函數(shù)參數(shù))
trailingComma: 'all',
//在對象文字中打印括號之間的空格。
bracketSpacing: true,
// > 標(biāo)簽放在最后一行的末尾,而不是單獨(dú)放在下一行
jsxBracketSameLine: false,
//箭頭圓括號
arrowParens: 'avoid',
//在文件頂部插入一個(gè)特殊的 @format 標(biāo)記,指定文件格式需要被格式化。
insertPragma: false,
//縮進(jìn)
tabWidth: 4,
//使用tab還是空格
useTabs: false,
}
2.3.3 結(jié)合Prettier配置Eslint
修改.eslintrc.js文件,引入prettier
最終配置為:
module.exports = {
parser: '@typescript-eslint/parser',
extends: [
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
'prettier/@typescript-eslint',
'plugin:prettier/recommended',
],
plugins: ['@typescript-eslint', 'react'],
env: {
browser: true,
node: true,
es6: true,
},
rules: {
quotes: ['error', 'single'], //強(qiáng)制使用單引號
semi: ['error', 'never'], // 要求或禁止使用分號而不是 ASI
camelcase: 0, // 雙峰駝命名格式
eqeqeq: 2, //必須使用全等
yoda: [2, 'never'], //禁止尤達(dá)條件
strict: [2, 'never'], // 禁用嚴(yán)格模式,禁止在任何地方出現(xiàn) 'use strict'
'no-extra-boolean-cast': 2, //禁止不必要的bool轉(zhuǎn)換
'no-lone-blocks': 2, //禁止不必要的嵌套塊
'no-plusplus': 0, //禁止使用++,--
'no-proto': 2, //禁止使用__proto__屬性
'no-self-compare': 2, //不能比較自身
'no-undef': 2, //不能有未定義的變量
'no-unreachable': 2, //不能有無法執(zhí)行的代碼
'no-unused-expressions': 2, //禁止無用的表達(dá)式
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'no-alert': 2, //禁止使用alert
'no-caller': 1, //禁止使用arguments.caller或arguments.callee
'no-inline-comments': 2, //禁止行內(nèi)備注
'no-func-assign': 2, //禁止重復(fù)的函數(shù)聲明
'no-eval': 2, //禁止使用eval,
'no-empty': 2, //塊語句中的內(nèi)容不能為空
'no-const-assign': 2, //禁止修改const聲明的變量
'no-var': 2, //禁止使用var
'no-multiple-empty-lines': [1, { max: 2 }], //空行最多不能超過2行
'no-extra-semi': 'error', // 禁止不必要的分號
'array-bracket-spacing': [2, 'never'], //是否允許非空數(shù)組里面有多余的空格
'linebreak-style': ['error', 'unix'], // 強(qiáng)制使用一致的換行風(fēng)格
'brace-style': [2, '1tbs', { allowSingleLine: true }], // if while function 后面的{必須與if在同一行,java風(fēng)格。
'comma-dangle': 0, // 數(shù)組和對象鍵值對最后一個(gè)逗號, never參數(shù):不能帶末尾的逗號, always參數(shù):必須帶末尾的逗號,
'comma-spacing': [2, { before: false, after: true }], // 控制逗號前后的空格
'computed-property-spacing': [2, 'never'], // 以方括號取對象屬性時(shí),[ 后面和 ] 前面是否需要空格, 可選參數(shù) never, always
'use-isnan': 2, //禁止比較時(shí)使用NaN,只能用isNaN()
'default-case': 2, //switch語句最后必須有default
'newline-after-var': 2, //變量聲明后是否需要空一行
'max-depth': [2, 4], //嵌套塊深度最多四層
'max-params': [2, 4], //函數(shù)最多只能有4個(gè)參數(shù)
'no-else-return': 2, //如果if語句里面有return,后面不能跟else語句,禁止出現(xiàn) if (cond) { return a } else { return b },應(yīng)該寫為 if (cond) { return a } return b
'no-eq-null': 2, //禁止對null使用==或!=運(yùn)算符
'no-iterator': 2, //禁止使用__iterator__ 屬性
'no-mixed-spaces-and-tabs': [2, false], //禁止混用tab和空格
'no-new-func': 1, //禁止使用new Function
'no-new-object': 2, //禁止使用new Object()
'no-self-compare': 2, //不能比較自身
'no-unused-vars': [2, { vars: 'all', args: 'after-used' }], //不能有聲明后未被使用的變量或參數(shù)
'no-use-before-define': 0, //未定義前不能使用
'valid-typeof': 2, //無效的類型判斷
'wrap-iife': [2, 'inside'], //立即執(zhí)行函數(shù)表達(dá)式的小括號風(fēng)格
// 注釋的斜線和星號后要加空格
'spaced-comment': [
2,
'always',
{
block: {
exceptions: ['*'],
balanced: true,
},
},
],
// new, delete, typeof, void, yield 等表達(dá)式前后必須有空格,-, +, --, ++, !, !! 等表達(dá)式前后不許有空格
'space-unary-ops': [
2,
{
words: true,
nonwords: false,
},
],
'prefer-rest-params': 2, // 必須使用解構(gòu) ...args 來代替 arguments
'consistent-this': [2, 'self', 'that'], // this 的別名規(guī)則,只允許 self 或 that
curly: [2, 'multi-line', 'consistent'], // if 后必須包含 { ,單行 if 除外
'for-direction': 2, // for 循環(huán)不得因方向錯(cuò)誤造成死循環(huán)
'getter-return': [2, { allowImplicit: true }], // getter 必須有返回值,允許返回 undefined
'keyword-spacing': 2, // 關(guān)鍵字前后必須有空格
// new關(guān)鍵字后類名應(yīng)首字母大寫
'new-cap': [
2,
{
capIsNew: false, // 允許大寫開頭的函數(shù)直接執(zhí)行
},
],
'no-await-in-loop': 2, // 禁止將 await 寫在循環(huán)里
'no-class-assign': 2, // class定義的類名不得與其它變量重名
'no-dupe-args': 2, // 函數(shù)參數(shù)禁止重名
'no-duplicate-case': 2, // 禁止 switch 中出現(xiàn)相同的 case
'no-duplicate-imports': 2, // 禁止重復(fù) import
'no-empty-function': 0, // 禁止空的 function,包含注釋的情況下允許
'no-empty-pattern': 2, // 禁止解構(gòu)中出現(xiàn)空 {} 或 []
'no-ex-assign': 2, // catch 定義的參數(shù)禁止賦值
'no-extend-native': [2, { exceptions: ['Array', 'Object'] }], // 禁止擴(kuò)展原生對象
'no-extra-parens': [2, 'functions'], // 禁止額外的括號,僅針對函數(shù)體
'no-floating-decimal': 2, // 不允許使用 2. 或 .5 來表示數(shù)字,需要用 2、2.0、0.5 的格式
'no-func-assign': 2, // 禁止對函數(shù)聲明重新賦值
'no-implied-eval': 2, // 禁止在 setTimeout 和 setInterval 中傳入字符串,因會(huì)觸發(fā)隱式 eval
'no-multi-assign': 2, // 禁止連等賦值
'@typescript-eslint/explicit-function-return-type': [
'off',
{
allowExpressions: true,
allowTypedFunctionExpressions: true,
},
],
'@typescript-eslint/no-explicit-any': 0, // 特殊情況可將類型顯示設(shè)置為any
'@typescript-eslint/interface-name-prefix': 0, // 允許接口命名以I開頭
'@typescript-eslint/no-var-requires': 0, // antd中引用style需要用require
'@typescript-eslint/no-use-before-define': 0, // mapStateToProps在之前就用到(typeof推斷類型)
'@typescript-eslint/camelcase': 0, // 駝峰命名格式
'@typescript-eslint/no-empty-function': 0, // 給函數(shù)默認(rèn)值可以為空
'react/display-name': 0, // 一個(gè)莫名其妙的Bug
'react/no-find-dom-node': 0,
'@typescript-eslint/no-non-null-assertion': 0, // 允許用!斷言不為空
},
settings: {
//自動(dòng)發(fā)現(xiàn)React的版本,從而進(jìn)行規(guī)范react代碼
react: {
pragma: 'React',
version: 'detect',
},
},
parserOptions: {
//指定ESLint可以解析JSX語法
ecmaVersion: 2019,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
}
prettier/@typescript-eslint
:使得@typescript-eslint中的樣式規(guī)范失效,遵循prettier中的樣式規(guī)范。plugin:prettier/recommended
:使用prettier中的樣式規(guī)范,且如果使得ESLint會(huì)檢測prettier的格式問題,同樣將格式問題以error的形式拋出。
2.4 在VSCode中集成ESLint配置
當(dāng)在項(xiàng)目中有了如上配置,其他開發(fā)人員需要在自己的VSCode中進(jìn)行ESLint和Prettier插件的安裝配置。VScode的ESLint和Prettier會(huì)讀取項(xiàng)目的配置文件,從而達(dá)到對代碼的檢查。踩坑如下:
- 需要注意的是如果是通過工作機(jī)進(jìn)行遠(yuǎn)程工作的,一定要記得
遠(yuǎn)程的VScode安裝插件
才生效,本地安裝并沒用。 - 同時(shí)在團(tuán)隊(duì)協(xié)作過程中,插件的版本有可能不同,如穩(wěn)定版本和非穩(wěn)定版本對于eslint規(guī)則的解析不同,因此團(tuán)隊(duì)直接盡可能
安裝相同版本的插件
.
2.4.1 配置setting,保存自動(dòng)校驗(yàn)
prettier和eslint可以在保存時(shí)自動(dòng)檢查并自動(dòng)格式化一部分問題,在settings.json文件中修改其配置文件如下:
{
"eslint.enable": true, //是否開啟vscode的eslint
"eslint.options": { //指定vscode的eslint所處理的文件的后綴
"extensions": [
".js",
".vue",
".ts",
".tsx"
]
},
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": [ //確定校驗(yàn)準(zhǔn)則
"javascript",
"javascriptreact",
{
"language": "html",
"autoFix": true
},
{
"language": "vue",
"autoFix": true
},
{
"language": "typescript",
"autoFix": true
},
{
"language": "typescriptreact",
"autoFix": true
}
]
}
2.5 husky規(guī)范工作流
在項(xiàng)目遷移和規(guī)范化的過程中,我們不可能一次性將所有已經(jīng)存在的代碼遷移到TS,因此在實(shí)際過程中我們是采用JS和TS混合開發(fā),在實(shí)際做業(yè)務(wù)需求過程中將改動(dòng)的文件遷移成TS,對尚未碰見的代碼不做改動(dòng),保證項(xiàng)目的正常運(yùn)行。同樣,對Eslint的格式化也是主要集中在新開發(fā)的頁面。在開發(fā)的過程中,為了保證團(tuán)隊(duì)所有成員都能嚴(yán)格執(zhí)行Eslint規(guī)范,采用husky構(gòu)建工作流,eslint將檢查做了修改,存在stage階段尚未commit階段的代碼,在commit前進(jìn)行校驗(yàn),校驗(yàn)無誤即通過,否則不通過。
2.5.1 安裝依賴
npm install husky --save-dev
2.5.2 配置
在package.json的script中配置:
"scripts": {
"lint": "npm run lint:js && npm run lint:style && npm run lint:prettier",
"lint-staged": "lint-staged",
"lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
"lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src && npm run lint:style",
"lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
"lint:prettier": "check-prettier lint",
"lint:style": "stylelint --fix \"src/**/*.less\" --syntax less",
}
接著需要在package.json的husky中配置如下:
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
在pre-commit這個(gè)hook也就是在提交之前進(jìn)行l(wèi)int的檢測。
上述我們通過在husky的pre-comit這個(gè)hook中執(zhí)行一個(gè)npm命令來做lint校驗(yàn)。其實(shí)一般情況,我們會(huì)定義一個(gè)lint-staged,來在提交前檢測代碼的規(guī)范。使用lint-staged來規(guī)范代碼的方式如下,我們修改package.json文件為:
{
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write",
"git add ."
]
}
}
注意:這里有個(gè)坑
需要注意,如果發(fā)現(xiàn)文件eslint還報(bào)錯(cuò),居然還能提交成功,也就是husky沒有生效,那么可以cd進(jìn)入到.git/hooks文件夾,查看一下有沒有pre-commit文件(不是pre-commit.sample文件),如果沒有,那么就是git版本的原因,需要升級到2.13.0以上。
在本項(xiàng)目中采用cicd進(jìn)行持續(xù)集成,因此也可以將eslint加入到ci中,在這里不在詳細(xì)介紹。
總結(jié)
項(xiàng)目的Typescript遷移和Eslint+Prettier的代碼格式化,目前已經(jīng)上線幾個(gè)月運(yùn)行良好,至今項(xiàng)目已經(jīng)遷移一半,基本無痛點(diǎn),很爽。