一、改造背景
代码风格和性格一样,每个程序员都有自己的特点 ,但对于大家协同开发的项目,还是需要力求代码风格的一致性,以减少Bug,但是目前现有项目在代码提交之前,没有进行代码规则检查能够确保进入git库的代码都是符合代码规则的。
该形式的开发模式存在以下几个缺点:
1、javascript相关文件规范依赖插件保提示或者格式,不能在提交前自动校验格式
2、styl样式文件缺少编码风格规则约束,社区相关插件比较少
3、在代码提交之前,对暂存区的文件的脚本和样式文件缺少校验与格式操作且对相关提交信息缺少约束,不利于后期排查问题
综上,当前开发对脚本、样式、提交信息的风格缺少自动化校验流程,代码风格不统一,对后期的维护迭代不利
二、改进点
针对以上问题,在项目里引入Husky、Commitlint、Lint-staged及stylelint工程化工具进行代码风格约束,以减少bug,确定提交到仓库的代码符合约定的语法规范并且对不符合规范的代码进行自动fix操作。
该方案具有以下优点:
1、代码提交钩子Husky,在代码被提交到Git仓库之前,我们可以在这里做一些预检查或者格式化,简单说就是使用Git命令会触发的函数。
2、在有了Husky赋能之后,我们有能力在Git的钩子里做一些事情,首先不得不提的是代码的提交规范和规范的校验,优雅的提交,方便团队协作和快速定位问题,首推Commitlint
3、前端文件过滤的工具Lint-staged,对于较大型的项目,文件众多,首先遇到的就是性能问题,虽然如Eslint之类的也有文件过滤配置,但毕竟还是对于匹配文件的全量遍历,如全量的.js文件,基本达不到性能要求,有时还会误格式化其他同学的代码,因此我们引入Lint-staged,一个仅仅过滤出Git代码暂存区文件(被committed的文件)的工具。
4、 StyleLint 是『一个强大的、现代化的 CSS 检测工具』, 与 ESLint 类似, 是通过定义一系列的编码风格规则帮助我们避免在样式表中出现错误。
5、ESLint 是在 ECMAScript/JavaScript 代码中识别和报告模式匹配的工具,它的目标是保证代码的一致性和避免错误。
三、具体实施
此方案主要基于node项目工程化的前端代码格式化的工具配置使用,主要包含以下组件:
-
代码提交钩子Husky;
-
代码的提交规范和规范的校验,优雅的提交Commitlint工具;
-
Git代码暂存区文件过滤工具Lint-staged
-
stylus-converter工具包,把现有styl后缀的文件转换成less文件
-
stylelint和eslint进行样式和js语法校验和修复
具体搭建步骤:
-
安装npm包husky、commitlint、Lint-staged、stylus-converter、stylelint、eslint(按照如下版本安装);
yarn add husky@7.0.2 @commitlint/config-conventional@13.1.0 @commitlint/cli@13.1.0 lint-staged@11.1.2 stylus-converter@0.8.1 stylelint@13.13.1 stylelint-order stylelint-config-standard@22.0.0 eslint@7.25.0 -D
-
生成husky配置文件(注:husky 7.x.x版本后需要添加配置文件生效),在package.json的scripts添加"addHusky": "npx husky add .husky/pre-commit 'npm test'"执行后生成husky配置文件commit-msg和pre-commit文件;
commit-ms文件代码如下:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# 参考https://github.com/leoforfree/cz-customizable
# yarn commit
npx commitlint --edit $1
pre-commit文件代码如下:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# 这里就是唤醒lint-staged
npx lint-staged
# 获取上面脚本的退出码
exitCode="$?"
exit $exitCode
-
根目录新增commitlint.config.js配置文件,详细配置如下:
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'upd', 'test', 'chore', 'revert']
],
'subject-full-stop': [0, 'never'],
'subject-case': [0, 'never']
}
}
git commit -m提交格式<type>: <subject>(注意冒号后面有空格)
常用的type类别如下:
upd:更新某功能(不是 feat, 不是 fix)
feat:新功能(feature)
fix:修补bug
docs:文档(documentation)
style: 格式(不影响代码运行的变动)
refactor:重构(即不是新增功能,也不是修改bug的代码变动)
test:增加测试
chore:构建过程或辅助工具的变动
subject作用:
subject是 commit 目的的简短描述,可以做一些配置,如最大长度限制。
例子如下:
git commit -m 'feat: 增加 xxx 功能'
git commit -m 'bug: 修复 xxx 功能'
-
根目录新增lint-staged.config.js文件过滤配置文件(一个仅仅过滤git代码暂存区文件,被git add的文件),Lint-staged不会帮你格式化任何东西,所以没有代码规则配置文件,需要自己配置一下,如:.eslintrc、.stylelintrc等。当文件变化,我们git commit它们,pre-commit钩子会启动,执行lint-staged命令,我们对于lint-staged如下文配置,对本次被commited中的所有.js、.jsx文件执行eslint --fix命令和所有.less、.scss文件执行stylelint --fix命令进行格式化;
// lint-staged.config.js配置文件
module.exports = {
'src/**/*.{js,jsx}': [
'eslint --fix'
],
'src/**/*.{less,scss}': [
'stylelint --fix'
]
}
-
利用安装好的stylus-converter工具包把.styl文件转换成less文件
命令行脚本示例如下:
stylus-conver -i '复制styl文件相对路径' -o test.less"
-
根目录新增stylelint.config.js和.stylelintignore样式规范配置文件和忽略配置文件,详细请参考官方配置
module.exports = {
plugins: ['stylelint-order'],
extends: 'stylelint-config-standard', // 这是官方推荐的方式
rules: {
'at-rule-no-unknown': [true, {
ignoreAtRules: ['extend', 'at-root', 'debug', 'warn', 'error', 'if', 'else', 'for', 'each', 'while', 'mixin', 'include', 'content', 'return', 'function']
}],
// 'rule-empty-line-before': 'never', // 禁止在 at 规则之前有空行
'no-descending-specificity': null, // 禁止特异性较低的选择器在特异性较高的选择器之后重写
'order/order': [
'custom-properties',
'declarations'
],
'order/properties-order': [
'position',
'top',
'right',
'bottom',
'left',
'z-index',
'display',
'float',
'width',
'height'
]
}
}
# Created by .ignore support plugin (hsz.mobi)
**/*.js
node_modules/
-
根目录新增.eslintrc.js配置文件
module.exports = {
env: {
browser: true,
es2021: true,
node: true
},
extends: [
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'eslint:recommended'
],
parser: 'babel-eslint',
parserOptions: {
ecmaFeatures: {
jsx: true
},
ecmaVersion: 12,
sourceType: 'module'
},
plugins: [
'react'
],
rules: {
'react/prop-types': 1,
'react/no-string-refs': 1,
'react/display-name': 1,
'react/no-unescaped-entities': 1,
'no-unused-vars': 1,
'no-empty': 1,
'no-useless-escape': 1,
'no-cond-assign': 1,
'no-extra-boolean-cast': 1,
'no-case-declarations': 1,
'handle-callback-err': 1,
'new-cap': 1,
'accessor-pairs': 2,
'arrow-spacing': [2, {
'before': true,
'after': true
}],
'block-spacing': [2, 'always'],
'brace-style': [2, '1tbs', {
'allowSingleLine': true
}],
'camelcase': [0, {
'properties': 'always'
}],
'comma-dangle': [2, 'never'],
'comma-spacing': [2, {
'before': false,
'after': true
}],
'comma-style': [2, 'last'],
'constructor-super': 2,
'curly': [2, 'multi-line'],
'dot-location': [2, 'property'],
'eol-last': 2,
'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
'generator-star-spacing': [2, {
'before': true,
'after': true
}],
'indent': [2, 2, {
'SwitchCase': 1
}],
'jsx-quotes': [2, 'prefer-single'],
'key-spacing': [2, {
'beforeColon': false,
'afterColon': true
}],
'keyword-spacing': [2, {
'before': true,
'after': true
}],
'new-parens': 2,
'no-array-constructor': 2,
'no-caller': 2,
'no-console': 'off',
'no-class-assign': 2,
'no-const-assign': 2,
'no-control-regex': 0,
'no-delete-var': 2,
'no-dupe-args': 2,
'no-dupe-class-members': 2,
'no-dupe-keys': 2,
'no-duplicate-case': 2,
'no-empty-character-class': 2,
'no-empty-pattern': 2,
'no-eval': 2,
'no-ex-assign': 2,
'no-extend-native': 2,
'no-extra-bind': 2,
'no-extra-parens': [2, 'functions'],
'no-fallthrough': 2,
'no-floating-decimal': 2,
'no-func-assign': 2,
'no-implied-eval': 2,
'no-inner-declarations': [2, 'functions'],
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': [2, {
'allowLoop': false,
'allowSwitch': false
}],
'no-lone-blocks': 2,
'no-mixed-spaces-and-tabs': 2,
'no-multi-spaces': 2,
'no-multi-str': 2,
'no-multiple-empty-lines': [2, {
'max': 1
}],
'no-native-reassign': 2,
'no-negated-in-lhs': 2,
'no-new-object': 2,
'no-new-require': 2,
'no-new-symbol': 2,
'no-new-wrappers': 2,
'no-obj-calls': 2,
'no-octal': 2,
'no-octal-escape': 2,
'no-path-concat': 2,
'no-proto': 2,
'no-redeclare': 2,
'no-regex-spaces': 2,
'no-return-assign': [2, 'except-parens'],
'no-self-assign': 2,
'no-self-compare': 2,
'no-sequences': 2,
'no-shadow-restricted-names': 2,
'no-spaced-func': 2,
'no-sparse-arrays': 2,
'no-this-before-super': 2,
'no-throw-literal': 2,
'no-trailing-spaces': 2,
'no-undef': 2,
'no-undef-init': 2,
'no-unexpected-multiline': 2,
'no-unmodified-loop-condition': 2,
'no-unneeded-ternary': [2, {
'defaultAssignment': false
}],
'no-unreachable': 2,
'no-unsafe-finally': 2,
'no-useless-call': 2,
'no-useless-computed-key': 2,
'no-useless-constructor': 2,
'no-whitespace-before-property': 2,
'no-with': 2,
'one-var': [2, {
'initialized': 'never'
}],
'operator-linebreak': [2, 'after', {
'overrides': {
'?': 'before',
':': 'before'
}
}],
'padded-blocks': [2, 'never'],
'quotes': [2, 'single', {
'avoidEscape': true,
'allowTemplateLiterals': true
}],
'semi': [2, 'never'],
'semi-spacing': [2, {
'before': false,
'after': true
}],
'space-before-blocks': [2, 'always'],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': [2, {
'words': true,
'nonwords': false
}],
'spaced-comment': [2, 'always', {
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}],
'template-curly-spacing': [2, 'never'],
'use-isnan': 2,
'valid-typeof': 2,
'wrap-iife': [2, 'any'],
'yield-star-spacing': [2, 'both'],
'yoda': [2, 'never'],
'prefer-const': 2,
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'object-curly-spacing': [2, 'always', {
objectsInObjects: false
}],
'array-bracket-spacing': [2, 'never']
}
}
-
postcss.config.js配置第一行新增require('stylelint')({})
module.exports = {
plugins: [
// 放在最前面防止插件干扰规则
require('stylelint')({}),
require('autoprefixer')
]
}
-
新增package.json的scripts脚本;
// ...
"scripts": {
"addHusky": "npx husky add .husky/pre-commit 'npm test'",
"stylelint": "stylelint 'src/**/*.less'",
"stylelintFix": "stylelint 'src/**/*.less' --fix",
"eslint": "eslint --ext .js,.jsx src/",
"eslintFix": "eslint 'src/**/*.js' --fix",
"stylusConverFile": "stylus-conver -i '复制styl相对路径' -o test.less"
},
// ...
四、有益效果
与之前开发系统相比,基于代码风格自动化的建设方案存在以下优点:
-
Husky在代码被提交到Git仓库之前,我们可以在这里做一些预检查或者格式化
-
Commitlint代码的提交规范和规范的校验,例如feat:新功能(feature)、fix:修补bug、style: 样式修改,方便问题跟踪
-
Lint-staged过滤出Git代码暂存区文件(被committed的文件)的工具,避免全量遍历和格式其他人代码
-
结合上面工具利用Stylelint和ESlint检查和帮我们自动修复css和js错误代码