团队协作开发之prettier+eslint+husky+commitlint +lint-staged

285 阅读5分钟

前言

在一个团队项目中,代码风格不统一,会大大的降低可读性,也会给后期的维护增加时间成本。如果代码风格统一、代码提交信息也简单明了,那么在后期协作以及处理bug时可以达到事半功倍的效果,所以就需要在我们使用git提交时校验eslint和提交信息的格式问题。需要用到的插件 husky、lint-staged、commitlint,prettier+eslint实现代码格式代码规范统一

一、prettier

作用

格式化工具,可以在各种编辑器找到实现它的插件,如vscode,atom,webstom等,用它统一代码格式化的规则,在代码提交前做代码格式化,防止不同的人开发由于编辑器格式化的规则不同导致代码冲突

使用

安装 prettier

yarn add prettier --save-dev

安装 eslint-plugin-prettier

yarn add eslint-plugin-prettier --save-dev

在项目下创建文件.prettierrc.js

module.exports = {
  // 代码结尾是否加分号
  semi: false,
  // 是否使用单引号
  singleQuote: true,
  // 对象大括号内两边是否加空格 { a:0 }
  bracketSpacing: true,
  // 单个参数的箭头函数不加括号 x => x
  arrowParens: 'avoid',
  // 超过多少字符强制换行
  printWidth: 200,
  // 文件换行格式 LF/CRLF
  endOfLine: 'auto',
  // 使用 4、个空格缩进
  tabWidth: 2,
  // 不使用缩进符,而使用空格
  useTabs: false,
  // 对象的 key 仅在必要时用引号
  quoteProps: 'as-needed',
  // jsx 不使用单引号,而使用双引号
  jsxSingleQuote: false,
  // 末尾不需要逗号
  trailingComma: 'none',
  // jsx 标签的反尖括号需要换行
  jsxBracketSameLine: false,
  // 每个文件格式化的范围是文件的全部内容
  rangeStart: 0,
  rangeEnd: Infinity,
  // 不需要写文件开头的 @prettier
  requirePragma: false,
  // 不需要自动在文件开头插入 @prettier
  insertPragma: false,
  // 使用默认的折行标准
  proseWrap: 'preserve',
  // 根据显示样式决定 html 要不要折行
  htmlWhitespaceSensitivity: 'css',
  // Vue 文件脚本和样式标签缩进
  vueIndentScriptAndStyle: true 
}

二、Eslint

作用

代码检查工具,eslint也可以负责一部分代码格式检查的工作,但是有prettier检查代码格式就可以了,所以只让eslint对代码错误进行检查即可

使用

安装 eslint

yarn add eslint --save-dev

安装 ts 解析器以及 ts 规则补充

yarn add @typescript-eslint/parser --save-dev
yarn add @typescript-eslint/eslint-plugin --save-dev

支持 tsx

yarn add eslint-plugin-react --save-dev

4.在项目根目录创建 .eslintrc.js

当运行 ESLint 的时候检查一个文件的时候,它会首先尝试读取该文件的目录下的配置文件,然后再一级一级往上查找,将所找到的配置合并起来,作为当前被检查文件的配置。

module.exports = {
    root: true,
    parser: '@typescript-eslint/parser',
    plugins: ['prettier', 'react', 'react-hooks', '@typescript-eslint/eslint-plugin'],
    parserOptions: {
        ecmaVersion: 8,
        ecmaFeatures: {
            experimentalObjectRestSpread: true,
            impliedStrict: true,
            requireConfigFile: false,
            classes: true
        }
    },
    env: {
        browser: true,
        node: true,
        jest: true
    },
    globals: {
        __DEV__: false,
        __dirname: false,
        window: true,
        define: true,
        history: true,
        location: true,
        wxjs: true,
        $: true,
        WeixinJSBridge: true,
        wx: true,
        process: true,
        qq: true
    },
    settings: {
        react: {
            version: 'detect'
        }
    },
    rules: {
        'prettier/prettier': 'error',
        // '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], // 优先使用 interface 而不是 type
        '@typescript-eslint/no-explicit-any': 'off', // 禁止使用该 any 类型 quchu
        '@typescript-eslint/no-var-requires': 'off', // 不允许使用 require 语句,除了在 import 语句中
        '@typescript-eslint/no-empty-function': 'off', // 禁止空函数
        '@typescript-eslint/no-use-before-define': 'off', // 在定义之前禁止使用变量
        '@typescript-eslint/ban-ts-comment': 'off', // 禁止 @ts-<directive> 使用评论或在指令后要求描述
        '@typescript-eslint/ban-types': 'off', // 禁止使用特定类型
        '@typescript-eslint/no-non-null-assertion': 'off', // '!'不允许使用后缀运算符的非空断言
        '@typescript-eslint/explicit-module-boundary-types': 'off', // 需要导出函数和类的公共类方法的显式返回和参数类型
        '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], // 禁止未使用的变量
        '@typescript-eslint/no-empty-interface': ['error', { allowSingleExtends: false }], // 禁止声明空接口
        'react/sort-comp': [2], // 强制组件方法顺序
        'react/jsx-boolean-value': 2, // 在JSX中强制布尔属性符号
        'react/jsx-curly-spacing': [2, { when: 'never', children: true }], // 在JSX属性和表达式中加强或禁止大括号内的空格。
        'react/jsx-key': 2, // 在数组或迭代器中验证JSX具有key属性
        'react/jsx-max-props-per-line': [1, { maximum: 5 }], // 限制JSX中单行上的props的最大数量
        'react/jsx-no-duplicate-props': 2, // 防止在JSX中重复的props
        'react/jsx-no-undef': 2, // 在JSX中禁止未声明的变量
        'react/jsx-pascal-case': 0, // 为用户定义的JSX组件强制使用PascalCase
        'react/jsx-uses-react': 2, // 防止反应被错误地标记为未使用
        'react/jsx-uses-vars': 2, // 防止在JSX中使用的变量被错误地标记为未使用
        'react/no-did-mount-set-state': 2, // 防止在componentDidMount中使用setState
        'react/no-did-update-set-state': 2, // 防止在componentDidUpdate中使用setState
        'react/no-unknown-property': 2, // 防止使用未知的DOM属性
        'react/prefer-es6-class': 2, // 为React组件强制执行ES5或ES6类
        // 'react/react-in-jsx-scope': 2, // 使用JSX时防止丢失React 就是在每个页面要倒入react
        'react/self-closing-comp': [2, { component: true, html: false }], // 结束标签,组件省略闭合标签,html不省略闭合标签
        'react/no-deprecated': 2, // 不使用弃用的方法
        'react/jsx-equals-spacing': 2, // 在JSX属性中强制或禁止等号周围的空格
        'react/jsx-filename-extension': [2, { extensions: ['.ts', '.tsx'] }],
        'react-hooks/exhaustive-deps': [2], // 检查 effect 的依赖
        'react-hooks/rules-of-hooks': [2], // 检查 Hook 的规则,不允许在if for里面使用
        'no-dupe-keys': 2, // 禁止对象字面量中出现重复的 key
        'no-duplicate-case': 2, // 禁止重复的 case 标签
        'no-empty': 2, // 禁止空语句块
        'no-ex-assign': 2, // 禁止对 catch 子句的参数重新赋值
        'no-extra-boolean-cast': 2, // 禁止不必要的布尔转换
        curly: [2, 'all'], // 强制所有控制语句使用一致的括号风格
        'no-catch-shadow': 0, // 禁止 catch 子句的参数与外层作用域中的变量同名
        'no-undef-init': 2, // 禁止将变量初始化为 undefined
        'array-bracket-spacing': [2, 'never'], // 指定数组的元素之间要以空格隔开(, 后面), never参数:[ 之前和 ] 之后不能带空格,always参数:[ 之前和 ] 之后必须带空格
        'computed-property-spacing': [2, 'never'], // 以方括号取对象属性时,[ 后面和 ] 前面是否需要空格, 可选参数 never, always
        'func-call-spacing': 2, // 要求或禁止在函数标识符和其调用之间有空格
        'lines-around-comment': [2, { beforeBlockComment: true }], // 要求在注释周围有空行 ( 要求在块级注释之前有一空行)
        'max-nested-callbacks': [2, 5], // 强制回调函数最大嵌套深度 5层
        'jsx-quotes': 0, // 强制在 JSX 属性中一致地使用双引号或单引号
        'max-len': [2, 200, { ignoreUrls: true }], // 强制一行的最大长度
        'max-params': [1, 5], // 强制 function 定义中最多允许的参数数量
        'max-statements': [1, 200], // 强制 function 块最多允许的的语句数量
        'newline-per-chained-call': 1, // 要求方法链中每个调用都有一个换行符
        'no-whitespace-before-property': 2, // 禁止属性前有空白
        'no-case-declarations': ['warn'], // 禁止词法声明 (let、const、function 和 class) 出现在 case或default 子句中
        'arrow-body-style': 2, // 要求箭头函数体使用大括号
        'arrow-parens': 2, // 要求箭头函数的参数使用圆括号
        'no-class-assign': 2, // 禁止修改类声明的变量
        'no-const-assign': 2, // 禁止修改 const 声明的变量
        'no-dupe-class-members': 2, // 禁止类成员中出现重复的名称
        'no-this-before-super': 2, // 禁止在构造函数中,在调用 super() 之前使用 this 或 super
        'object-curly-spacing': ['error', 'always'], // 对象前后要加空格 { a: 1 }
        'arrow-spacing': [2, { before: true, after: true }], // 强制箭头函数的箭头前后使用一致的空格
        'no-use-before-define': 'off', // 禁止在变量定义之前使用它们
        'space-before-function-paren': 'off', // 强制在 function的左括号之前使用一致的空格
        'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
        'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
        'accessor-pairs': 2, // 强制 getter 和 setter 在对象中成对出现
        'block-spacing': [2, 'always'], // 禁止或强制在代码块中开括号前和闭括号后有空格 (要求使用一个或多个空格)
        'brace-style': [2, '1tbs', { allowSingleLine: true }], // if while function 后面的{必须与if在同一行,java风格。
        camelcase: [2, { properties: 'always' }], // 强制驼峰法命名
        'comma-spacing': [2, { before: false, after: true }], // 控制逗号前后的空格
        'comma-style': [2, 'last'], // 控制逗号在行尾出现还是在行首出现
        'dot-location': [2, 'property'],
        'eol-last': 2, // 文件末尾强制换行
        eqeqeq: [2, 'allow-null'], // 使用 === 替代 ==
        'key-spacing': [2, { beforeColon: false, afterColon: true }], // 对象字面量中冒号的前后空格
        'keyword-spacing': [2, { before: true, after: true }], // 强制在关键字前后使用一致的空格
        'new-cap': [2, { newIsCap: true, capIsNew: false }], // 函数名首行大写必须使用new方式调用,首行小写必须用不带new方式调用
        'init-declarations': 0, // 声明时必须赋初值
        'max-depth': [0, 4], // 嵌套块深度
        'no-dupe-args': 2, // 函数参数不能重复
        'no-func-assign': 2, // 禁止重复的函数声明
        'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
        'default-case': 2, // switch语句最后必须有default
        'no-var': 0, // 禁用var,用let和const代替
        'new-parens': 2, // new时必须加小括号
        'no-array-constructor': 2, // 禁止使用数组构造器
        'no-caller': 2, // 禁止使用arguments.caller或arguments.callee
        'no-empty-character-class': 0, // 正则表达式中的[]内容不能为空
        'no-eval': 0, // 禁止使用eval
        'no-extend-native': 2, // 禁止扩展native对象
        'no-extra-bind': 2, // 禁止不必要的函数绑定
        'no-extra-parens': [2, 'functions'], // 禁止非必要的括号
        'no-floating-decimal': 2, // 禁止省略浮点数中的0 .5 3.
        'no-implied-eval': 2, // 禁止使用隐式eval
        'no-label-var': 2, // label名不能与var声明的变量名相同
        'no-lone-blocks': 2, // 禁止不必要的嵌套块
        'no-multi-spaces': 2, // 不能用多余的空格
        'no-multi-str': 2, // 禁止使用多行字符串
        'no-multiple-empty-lines': [2, { max: 2 }], // 空行最多不能超过2行
        'no-new-object': 2, // 禁止使用new Object()
        'no-new-require': 2, // 禁止使用new require
        'no-new-wrappers': 2, // 禁止对 String,Number 和 Boolean 使用 new 操作符
        'no-return-assign': [2, 'except-parens'], // return 语句中不能有赋值表达式
        'no-sequences': 2, // 禁止使用逗号运算符
        'no-spaced-func': 2, // 函数调用时 函数名与()之间不能有空格
        'no-sparse-arrays': 0, // 禁止稀疏数组, [1,,2]
        'no-trailing-spaces': 2, // 一行结束后面不要有空格
        'no-unexpected-multiline': 0, // 避免多行表达式
        'no-unmodified-loop-condition': 0, // 检查引用是否在循环中被修改
        'no-unneeded-ternary': 2, // 禁止不必要的三元表达式 var isYes = answer === 1 ? true : false;
        'no-unsafe-finally': 0, // 禁止在 finally 语句块中出现控制流语句
        'no-useless-call': 2, // 禁止不必要的call和apply
        'no-useless-computed-key': 0, // 禁止在对象中使用不必要的计算属性
        'no-useless-constructor': 2, // 可以在不改变类的工作方式的情况下安全地移除的类构造函数
        'one-var': 0, // 禁止连续声明
        'operator-linebreak': [2, 'after', { overrides: { '?': 'before', ':': 'before' } }], // 换行时运算符在行尾还是行首
        'padded-blocks': 0, // 块语句内行首行尾是否要空行
        quotes: [2, 'single', { avoidEscape: true, allowTemplateLiterals: true }], // 强制使用一致的反勾号、双引号或单引号
        'semi-spacing': [2, { before: false, after: true }], // 分号前后空格
        'space-before-blocks': [2, 'always'], // 不以新行开始的块{前面要不要有空格
        'space-in-parens': [2, 'never'], // 小括号里面要不要有空格
        'space-infix-ops': 2, // 中缀操作符周围要不要有空格
        'space-unary-ops': [2, { words: true, nonwords: false }], // 一元运算符的前/后要不要加空格
        'spaced-comment': [2, 'always'], // 强制在注释中 // 或 /* 使用一致的空格
        'template-curly-spacing': [2, 'never'], // 要求或禁止模板字符串中的嵌入表达式周围空格的使用
        yoda: [2, 'never'], // 禁止尤达条件
        'no-extra-semi': 'off'
    }
}

三、husky

作用

GitHook工具(下一篇会详细介绍GitHook)husky会在你提交前,调用pre-commit钩子,执行lint-staged,如果代码不符合prettier配置的规则,会进行格式化;然后再用eslint的规则进行检查,如果有不符合规则的且无法自动修复的,就会停止提交。

使用

先安装最新的husky

npm install husky --save-dev

启动git钩子。启用后就可以看到项目根目录下生成了.husky文件夹。

npx husky install

然后依次生成我们需要的 git hook。引号中的内容是需要执行的脚本,可以在对应的官网中找到,下面会给出地址。

npx husky add .husky/pre-commit 'lint-staged'

npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'

生成后就可以在.husky/下看到我们生成的两个git hookcommit-msg和 pre-commit 。

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
 
npm run pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged

至此husky就算安装完成了,两种方式都可以,看自己的需求,旧版是将全部的git hook都生成,新版的只取自己需要的。

四、 lint-staged

作用

lint-staged能够让lint只检测暂存区的文件,即当前提交的文件,这样可以加快速度

使用

每次检查代码我们不需要检查全项目的文件,那样只会增加时间成本,这时候就需要用到 lint-staged,一个仅仅过滤出 Git 代码暂存区文件(被 git add 的文件)的工具。依旧是先安装。

npm install lint-staged

package.json中配置:

{
   "lint-staged": {
    "**/*.less": "stylelint --syntax less",
    "**/*.{js,jsx,ts,tsx}": "eslint --ext .js,.jsx,.ts,.tsx",
    "**/*.{js,jsx,tsx,ts,less,md,json}": [
      "prettier --write",
      "git add"
    ]
   }
}

五、 commitlint

作用

在代码提交前,对commit提交的信息说明进行校验,如果不符合规范则不给提交

使用

正如官方文档中所言,commitlint帮助你的团队遵守commit约定,检查你的提交消息是否符合传统的提交格式。

npm install --save-dev @commitlint/config-conventional @commitlint/cli

随后要添加配置文件,文件的格式可以是 commitlint.config.js、.commitlintrc.js、.commitlintrc、 .commitlintrc.json、 .commitlintrc.yml文件,或 package.json中的 commitlint字段中定义配置。可以自己创建,也可以按照官网的方式使用下面这行代码生成,这需要在项目的git命令行中操作(通常是在项目目录中使用右键选择Git Bash Here)。

module.exports = {
    extends: ['@commitlint/config-conventional'],
    rules: {
        'type-enum': [
            2,
            'always',
            [
                'start', // 开始做某事,比如创建分支等
                'end', // 结束做某事,比如删除分支等
                'bump', // 修改某个版本号
                'del', // 删除功能
                'feat', // 新功能、新特性
                'fix', // 修改 bug
                'perf', // 更改代码,性能优化
                'refactor', // 代码重构,没有加新功能或者修复bug(在不影响代码内部行为、功能下的代码修改)
                'docs', // 文档修改
                'style', // 代码格式修改, 注意不是 css 修改(例如分号修改)
                'test', // 测试用例新增、修改
                'conf', // 配置文件修改
                'revert', // 恢复上一次提交
                'ci', // 持续集成相关文件修改
                'chore', // 改变构建流程、或者增加依赖库、工具等
                'release', // 发布新版本
                'workflow' // 工作流相关文件修改
            ]
        ],
        'subject-full-stop': [0, 'never'],
        'subject-case': [0, 'never']
    }
}

配置package.json

  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "commit-msg": "commitlint -e $HUSKY_GIT_PARAMS"
    }
  },
  "lint-staged": {
    "*.{js,jsx,tsx,ts,less,scss}": [
      "npm run lint",
      "git add"
    ]
  }

提交示列 git commit -m 'fix: 修改xxxbug'