前言
在一家小公司实习了一段时间,接手的项目代码格式及规范化不可描述,为了保证项目的可维护性,决定接入Typescript
,同时采用Eslint
和Prettier
进行代码规范化,为下一步的CodeReview
。
Typescript是强类型语言,接入Typescript一开始会有很大的痛点,但是过了一阵子就可以享受到Typescript带来的好处,bug减少了,代码易读了,也可维护了,好处网上一大把就不累赘了。
常用的代码格式化工具主要有ESlint
、TSLint
、StandardJS
。TS官方已经决定弃用TSLint
,全面拥抱ESLint。因此在技术选型方面将采用ESlint
。
Eslint
的主要功能包含代码格式的校验,代码质量的校验,JS规范,如用===
而不是==
判断相等、用驼峰命名变量而不是用下划线。而 Prettier
是美丽的意思,只是代码格式的校验(并格式化代码),不会对代码质量进行校验,如单行代码长度、tab长度、空格、逗号表达式等问题。
一、安装相关依赖
1.1 安装 Typescript
推荐使用全局安装,可以在其他项目中也使用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
时需要根据实际项目的需要进行相关规则的配置,具体配置根据项目而异、可参考官网,具体看这里TS官网。我的配置项如下所示:
{
"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代码规范
针对JS
项目迁移到TS
的项目,主要有两种选择ESLint
和TSLint
。TSLint
仅针对TS
代码,因此如果采用TSLint
规范TS
代码,JS
代码需要采用其他工具。而ESLint
不仅能规范js
代码,通过配置解析器,也能规范TS
代码。此外由于性能问题,TypeScript
官方决定全面采用ESLint
。因此本项目采用ESLint
配合Prettier
规范化TS
和JS
代码。
2.1 ESLint规范TS代码
2.1.1 安装ESLint依赖
npm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev
eslint
: ESLint的核心代码@typescript-eslint/parser
:ESLint的解析器,用于解析Typescript文件,从而检查和规范Typescript代码@typescript-eslint/eslint-plugin
:这是一个ESLint插件,包含了各类定义好的检测Typescript代码的规范
安装好依赖后,需要在项目根目录中的.eslintrc.js中配置,包括解析器、继承的代码规范、插件和环境:
module.exports = {
parser: '@typescript-eslint/parser', //定义ESLint的解析器
extends: ['plugin:@typescript-eslint/recommended'],//定义文件继承的子规范
plugins: ['@typescript-eslint'],//定义了该eslint文件所依赖的插件
env:{ //指定代码的运行环境
browser: true,
node: true,
}
}
2.2 规范React代码
项目是以依赖于React的AntDesignPro为基础,因此需要安装规范React代码的Eslint插件。
2.2.1安装插件
npm i eslint-plugin-react --save-dev
2.2.2 配置React规范
然后修改.eslintrc.js的配置,如下所示:
module.exports = {
parser: '@typescript-eslint/parser',
extends: [
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended'
],//使用推荐的React代码检测规范
plugins: ['@typescript-eslint'],
env:{
browser: true,
node: true,
},
settings: {//自动发现React的版本,从而进行规范react代码
"react": {
"pragma": "React",
"version": "detect"
}
},
parserOptions: {//指定ESLint可以解析JSX语法
"ecmaVersion": 2019,
"sourceType": 'module',
"ecmaFeatures":{
jsx:true
}
}
rules: {
//在Rules中可以自定义你的React代码编码规范。
}
}
2.3接入Prettier
2.3.1 安装依赖
npm install prettier eslint-config-prettier eslint-plugin-prettier -g
prettier
:prettier插件的核心代码eslint-config-prettier
:解决ESLint中的样式规范和prettier中样式规范的冲突,以prettier的样式规范为准,使ESLint中的样式规范自动失效eslint-plugin-prettier
:将prettier作为ESLint规范来使用
2.3.2 配置规则
在项目的根目录下创建.prettierrc.js文件并配置prettier代码检查规则
module.exports = {
//最大长度80个字符
printWidth: 80,
//行末分号
semi: false,
//单引号
singleQuote: true,
//尽可能使用尾随逗号(包括函数参数)
trailingComma: 'all',
//在对象文字中打印括号之间的空格。
bracketSpacing: true,
// > 标签放在最后一行的末尾,而不是单独放在下一行
jsxBracketSameLine: false,
//箭头圆括号
arrowParens: 'avoid',
//在文件顶部插入一个特殊的 @format 标记,指定文件格式需要被格式化。
insertPragma: false,
//缩进
tabWidth: 4,
//使用tab还是空格
useTabs: false,
}
2.3.3 结合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'], //强制使用单引号
semi: ['error', 'never'], // 要求或禁止使用分号而不是 ASI
camelcase: 0, // 双峰驼命名格式
eqeqeq: 2, //必须使用全等
yoda: [2, 'never'], //禁止尤达条件
strict: [2, 'never'], // 禁用严格模式,禁止在任何地方出现 'use strict'
'no-extra-boolean-cast': 2, //禁止不必要的bool转换
'no-lone-blocks': 2, //禁止不必要的嵌套块
'no-plusplus': 0, //禁止使用++,--
'no-proto': 2, //禁止使用__proto__属性
'no-self-compare': 2, //不能比较自身
'no-undef': 2, //不能有未定义的变量
'no-unreachable': 2, //不能有无法执行的代码
'no-unused-expressions': 2, //禁止无用的表达式
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'no-alert': 2, //禁止使用alert
'no-caller': 1, //禁止使用arguments.caller或arguments.callee
'no-inline-comments': 2, //禁止行内备注
'no-func-assign': 2, //禁止重复的函数声明
'no-eval': 2, //禁止使用eval,
'no-empty': 2, //块语句中的内容不能为空
'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'], //是否允许非空数组里面有多余的空格
'linebreak-style': ['error', 'unix'], // 强制使用一致的换行风格
'brace-style': [2, '1tbs', { allowSingleLine: true }], // if while function 后面的{必须与if在同一行,java风格。
'comma-dangle': 0, // 数组和对象键值对最后一个逗号, never参数:不能带末尾的逗号, always参数:必须带末尾的逗号,
'comma-spacing': [2, { before: false, after: true }], // 控制逗号前后的空格
'computed-property-spacing': [2, 'never'], // 以方括号取对象属性时,[ 后面和 ] 前面是否需要空格, 可选参数 never, always
'use-isnan': 2, //禁止比较时使用NaN,只能用isNaN()
'default-case': 2, //switch语句最后必须有default
'newline-after-var': 2, //变量声明后是否需要空一行
'max-depth': [2, 4], //嵌套块深度最多四层
'max-params': [2, 4], //函数最多只能有4个参数
'no-else-return': 2, //如果if语句里面有return,后面不能跟else语句,禁止出现 if (cond) { return a } else { return b },应该写为 if (cond) { return a } return b
'no-eq-null': 2, //禁止对null使用==或!=运算符
'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' }], //不能有声明后未被使用的变量或参数
'no-use-before-define': 0, //未定义前不能使用
'valid-typeof': 2, //无效的类型判断
'wrap-iife': [2, 'inside'], //立即执行函数表达式的小括号风格
// 注释的斜线和星号后要加空格
'spaced-comment': [
2,
'always',
{
block: {
exceptions: ['*'],
balanced: true,
},
},
],
// new, delete, typeof, void, yield 等表达式前后必须有空格,-, +, --, ++, !, !! 等表达式前后不许有空格
'space-unary-ops': [
2,
{
words: true,
nonwords: false,
},
],
'prefer-rest-params': 2, // 必须使用解构 ...args 来代替 arguments
'consistent-this': [2, 'self', 'that'], // this 的别名规则,只允许 self 或 that
curly: [2, 'multi-line', 'consistent'], // if 后必须包含 { ,单行 if 除外
'for-direction': 2, // for 循环不得因方向错误造成死循环
'getter-return': [2, { allowImplicit: true }], // getter 必须有返回值,允许返回 undefined
'keyword-spacing': 2, // 关键字前后必须有空格
// new关键字后类名应首字母大写
'new-cap': [
2,
{
capIsNew: false, // 允许大写开头的函数直接执行
},
],
'no-await-in-loop': 2, // 禁止将 await 写在循环里
'no-class-assign': 2, // class定义的类名不得与其它变量重名
'no-dupe-args': 2, // 函数参数禁止重名
'no-duplicate-case': 2, // 禁止 switch 中出现相同的 case
'no-duplicate-imports': 2, // 禁止重复 import
'no-empty-function': 0, // 禁止空的 function,包含注释的情况下允许
'no-empty-pattern': 2, // 禁止解构中出现空 {} 或 []
'no-ex-assign': 2, // catch 定义的参数禁止赋值
'no-extend-native': [2, { exceptions: ['Array', 'Object'] }], // 禁止扩展原生对象
'no-extra-parens': [2, 'functions'], // 禁止额外的括号,仅针对函数体
'no-floating-decimal': 2, // 不允许使用 2. 或 .5 来表示数字,需要用 2、2.0、0.5 的格式
'no-func-assign': 2, // 禁止对函数声明重新赋值
'no-implied-eval': 2, // 禁止在 setTimeout 和 setInterval 中传入字符串,因会触发隐式 eval
'no-multi-assign': 2, // 禁止连等赋值
'@typescript-eslint/explicit-function-return-type': [
'off',
{
allowExpressions: true,
allowTypedFunctionExpressions: true,
},
],
'@typescript-eslint/no-explicit-any': 0, // 特殊情况可将类型显示设置为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, // 给函数默认值可以为空
'react/display-name': 0, // 一个莫名其妙的Bug
'react/no-find-dom-node': 0,
'@typescript-eslint/no-non-null-assertion': 0, // 允许用!断言不为空
},
settings: {
//自动发现React的版本,从而进行规范react代码
react: {
pragma: 'React',
version: 'detect',
},
},
parserOptions: {
//指定ESLint可以解析JSX语法
ecmaVersion: 2019,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
}
prettier/@typescript-eslint
:使得@typescript-eslint中的样式规范失效,遵循prettier中的样式规范。plugin:prettier/recommended
:使用prettier中的样式规范,且如果使得ESLint会检测prettier的格式问题,同样将格式问题以error的形式抛出。
2.4 在VSCode中集成ESLint配置
当在项目中有了如上配置,其他开发人员需要在自己的VSCode中进行ESLint和Prettier插件的安装配置。VScode的ESLint和Prettier会读取项目的配置文件,从而达到对代码的检查。踩坑如下:
- 需要注意的是如果是通过工作机进行远程工作的,一定要记得
远程的VScode安装插件
才生效,本地安装并没用。 - 同时在团队协作过程中,插件的版本有可能不同,如稳定版本和非稳定版本对于eslint规则的解析不同,因此团队直接尽可能
安装相同版本的插件
.
2.4.1 配置setting,保存自动校验
prettier和eslint可以在保存时自动检查并自动格式化一部分问题,在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": [ //确定校验准则
"javascript",
"javascriptreact",
{
"language": "html",
"autoFix": true
},
{
"language": "vue",
"autoFix": true
},
{
"language": "typescript",
"autoFix": true
},
{
"language": "typescriptreact",
"autoFix": true
}
]
}
2.5 husky规范工作流
在项目迁移和规范化的过程中,我们不可能一次性将所有已经存在的代码迁移到TS,因此在实际过程中我们是采用JS和TS混合开发,在实际做业务需求过程中将改动的文件迁移成TS,对尚未碰见的代码不做改动,保证项目的正常运行。同样,对Eslint的格式化也是主要集中在新开发的页面。在开发的过程中,为了保证团队所有成员都能严格执行Eslint规范,采用husky构建工作流,eslint将检查做了修改,存在stage阶段尚未commit阶段的代码,在commit前进行校验,校验无误即通过,否则不通过。
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这个hook也就是在提交之前进行lint的检测。
上述我们通过在husky的pre-comit这个hook中执行一个npm命令来做lint校验。其实一般情况,我们会定义一个lint-staged,来在提交前检测代码的规范。使用lint-staged来规范代码的方式如下,我们修改package.json文件为:
{
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write",
"git add ."
]
}
}
注意:这里有个坑
需要注意,如果发现文件eslint还报错,居然还能提交成功,也就是husky没有生效,那么可以cd进入到.git/hooks文件夹,查看一下有没有pre-commit文件(不是pre-commit.sample文件),如果没有,那么就是git版本的原因,需要升级到2.13.0以上。
在本项目中采用cicd进行持续集成,因此也可以将eslint加入到ci中,在这里不在详细介绍。
总结
项目的Typescript迁移和Eslint+Prettier的代码格式化,目前已经上线几个月运行良好,至今项目已经迁移一半,基本无痛点,很爽。