0%

前端工程化-前端规范方案

前言

Code standard

代码规范 是前端工程化落地的基石,用于约束 编码规范编码风格,它的好处是:

  • 强制规范代码风格统一,保持一样的编码习惯
  • 增加代码的可维护性和可接入性,即使有新成员的加入也能快速适应项目的架构与需求
  • 保障项目的整体质量,可减少无用代码、重复代码、错误代码和 BUG 代码的产生几率

Git Commit Message

commit message 是开发的日常操作, 写好 log 不仅有助于他人 review, 还可以有效的输出 CHANGELOG, 对项目的管理实际至关重要, 但是实际工作中却常常被大家忽略.

那如何在前端项目中统一 代码规范,以及 Commit Message 的规范?
我这边结合实际项目以及目前较受欢迎的相关工具,给大家分享一套解决方案。

使用到的工具:

  • eslint:JavaScript 代码检测工具,检测并提示错误或警告信息
  • prettier:代码自动格式化工具,更好的代码风格效果
  • husky:Git hooks 工具, 可以在执行 git 命令时,执行自定义的脚本程序
  • lint-staged:对暂存区 (git add) 文件执行脚本检测校验
  • commitlint:帮助你的团队遵守提交约定。通过支持 npm 安装的配置,它可以很容易地共享提交约定。

配置 eslint & prettier

安装

1
2
pnpm i eslint prettier @typescript-eslint/parser -D
pnpm i @typescript-eslint/eslint-plugin -D
  • 可以在 vscode 插件中安装 Prettier 和 Eslint 插件,在设置中开启保存文件则进行格式化;
  • 也可配置 scripts 命令(这会检测你指定目录的所有文件,当然会比较耗时)。
  • 另外可结合 lint-staged 只对 commited 后的文件进行格式化重写(见下面的 pre-commit git-hooks 触发)
1
2
3
4
5
6
7
// 配置 package.json 文件内容
{
"scripts": {
"lint": "eslint src --ext js,jsx,.vue,.ts,.tsx --report-unused-disable-directives --max-warnings 0",
"format": "prettier --write src/**/*/*.{js,jsx,ts,tsx}"
}
}

通过配置文件,统一代码格式化的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// .eslintrc.cjs 文件,常规配置方案。 PS:后缀.js可能会报CommonJS 语法错误(如下),将后缀改为.cjs即可
module.exports = {
env: {
browser: true,
es2021: true
},
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
plugins: ['@typescript-eslint'],
rules: {}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// .pretterrc.cjs 文件 PS:后缀.js可能会报CommonJS 语法错误(如下),将后缀改为.cjs即可
module.exports = {
semi: true, // 强制在语句末尾使用分号。
trailingComma: 'none', // 不允许在多行结构的最后一个元素或属性后添加逗号。\
singleQuote: true, // 使用单引号而不是双引号来定义字符串。
printWidth: 120, // 指定每行代码的最大字符宽度,超过这个宽度的代码将被换行
arrowParens: 'avoid' // 箭头函数参数只有一个时是否要有小括号(avoid - 省略)

// 其他更多配置
// bracketSpacing: true, // 在对象,数组括号与文字之间加空格 "{ foo: bar }"
// disableLanguages: ['vue'], // 不格式化vue文件,vue文件的格式化单独设置
// endOfLine: 'auto', // 结尾是 \n \r \n\r auto
// eslintIntegration: false, //不让prettier使用eslint的代码格式进行校验
// htmlWhitespaceSensitivity: 'ignore',
// ignorePath: '.prettierignore', // 不使用prettier格式化的文件填写在项目的.prettierignore文件中
// jsxBracketSameLine: true, // 在jsx中把'>' 是否单独放一行
// jsxSingleQuote: true, // 在jsx中使用单引号代替双引号
// parser: 'babylon', // 格式化的解析器,默认是babylon
// requireConfig: false, // Require a 'prettierconfig' to format prettier
// stylelintIntegration: false, //不让prettier使用stylelint的代码格式进行校验
// trailingComma: 'es5', // 在对象或数组最后一个元素后面是否加逗号(在ES5中加尾逗号)
// tslintIntegration: false // 不让prettier使用tslint的代码格式进行校验=
};

Commit Message 规范

我们在使用 Git 托管代码时,规范化的 Commit Message 可以帮助大家直观清晰地理解每次修改的内容,不仅能帮助别人 Review,还可以有效地输出 ChangeLog。那么要想前端工程化项目更易于维护,最好有一套 Git 提交说明的 规范化模板

目前最受开发人员肯定的规范是前端框架 Angular 提出的 Angular 提交信息规范,包含 页眉(header)、正文(body)和页脚(footer),每次提交必须包含页眉内容,每次提交的信息不超过 100 个字符,提交格式如下:

1
2
3
4
5
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

页眉设置

页眉的格式指定为提交类型(type)、作用域(scope,可选)和主题(subject)

提交类型(type)

提交类型指定为下面其中一个:

  • feat:新增功能
  • fix:Bug 修复
  • docs:文档更新
  • ci:脚本更新,修改了 CI 配置文件或脚本
  • pref:性能优化,提高性能的代码更改
  • build:更新构建,构建系统或者外部依赖项进行了修改、更新
  • test:新增测试,增加确实的测试或者矫正已存在的测试
  • refactor:代码重构,非新增功能也非修复缺陷
  • chore:事务变动,改动其他不影响代码的事务
  • revert:代码回滚,撤销某次代码提交
  • merge:分支合并,合并分支代码到其他分支
  • style:格式变动,不影响代码逻辑
  • release:版本发布

作用域(scope,可选)

用于说明 commit 的影响范围(选填)
(比如按下面这些划分方案填写)

  • 按功能划分(如:数据层 - Data、视图层 - View 和 控制层 - Control)
  • 按交互层划分(如:组件 - Component、布局 - Layout、流程 - Flow、视图 - View 和 页面 - Page)
  • 按改动文件划分(如:某个文件 - Button.tsx,全部改动 -*)

主题(subject)

用于说明 commit 的细节描述(选填)。以精炼简洁的文字(中文还是英文就看各自规定了,一般推荐使用英文)说明提交的改动,比如可以遵循这些规则:

  • 以动词开头(如:update,更新)
  • 使用第一人称现在时
  • 首字母无需大写,重点区分的(如某组件)首字母无需大写
  • 不以句号(. / 。)结尾

commit 信息示例:

1
2
3
feat(Component): add Layout component
feat(Button.tsx): change the default size of the button
fix(EmitEvent): handle event on blur (closes #28)

正文设置

和主题设置类似,使用命令式、现在时态

应该包含修改的动机以及和之前行为的对比

页脚设置

Breaking changes

不兼容修改指的是本次提交修改了不兼容之前版本的 API 或者环境变量

所有不兼容修改都必须在页脚中作为中断更改块提到,以 BREAKING CHANGE:开头,后跟一个空格或者两个换行符,其余的信息就是对此次修改的描述,修改的理由和修改注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BREAKING CHANGE: isolate scope bindings definition has changed and
the inject option for the directive controller injection was removed.

To migrate the code follow the example below:

Before:

。。。
。。。

After:

。。。
。。。

The removed `inject` wasn't generaly useful for directives so there should be no code using it.

引用提交的问题

如果本次提交目的是修改 issue 的话,需要在页脚引用该 issue

以关键字 Closes 开头,比如

1
Closes #234

如果修改了多个 bug,以逗号隔开

1
Closes #123, #245, #992

回滚设置

当此次提交包含回滚(revert)操作,那么页眉以”revert:”开头,同时在正文中添加”This reverts commit hash”,其中 hash 值表示被回滚前的提交

1
2
3
4
5
6
revert:<type>(<scope>): <subject>
<BLANK LINE>
This reverts commit hash
<other-body>
<BLANK LINE>
<footer>



方案实现: husky+commitlint

安装

1
2
pnpm i husky lint-staged @commitlint/cli @commitlint/config-conventional -D

初始化 husky,创建 .husky 文件夹(本质就是创建要触发的 git-hooks)

1
2
3
4
5
6
7
8
9
10
11
12
npx husky install
# 安装 git hooks,创建 .husky 目录
# 结果:husky - Git hooks installed

# 执行
npx husky-init

# 它会在 package.json 文件的 scripts 字段中
# 创建 "prepare": "husky install" 命令
# 同时在 .husky 目录下创建 pre-commit 文件
# 结果:husky - created .husky/pre-commit

修改 .husky/pre-commit 文件:

1
2
3
4
5
6
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# npm test # 这句删掉,改为下面这行,进行代码格式化检测
npx lint-staged

配置 package.json 文件内容

1
2
3
4
5
6
7
8
9
{
/* lint-staged 配置 */
"lint-staged": {
"*.{js,jsx,vue,ts,tsx}": [
"prettier --write",
"eslint --fix --ext .js,.jsx,.vue,.ts,.tsx"
]
}
}

创建 .eslintrc.cjs 文件和 .pretterrc.cjs 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// .eslintrc.cjs 文件
module.exports = {
env: {
browser: true,
es2021: true
},
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
plugins: ['@typescript-eslint'],
rules: {}
};
1
2
3
4
5
6
7
8
// .pretterrc.cjs 文件
module.exports = {
semi: false;
singleQuote: true;
printWidth: 80;
trailingComma: 'none';
arrowParens: 'avoid';
}

在 .husky 目录下添加 commit-msg hook

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

1
2
3
4
5
6
7
8
# 结果:husky - created .husky/commit-msg
# .husky/commit-msg 文件内容:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx --no -- commitlint --edit ""

创建 commitlint.config.cjs 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*
规范commit日志
https://commitlint.js.org
*/

const types = [
'build', // 主要目的是修改项目构建系统(例如glup,webpack,rollup的配置等)的提交
'ci', // 修改项目的持续集成流程(Kenkins、Travis等)的提交
'chore', // 构建过程或辅助工具的变化
'docs', // 文档提交(documents)
'feat', // 新增功能(feature)
'fix', // 修复 bug
'pref', // 性能、体验相关的提交
'refactor', // 代码重构
'revert', // 回滚某个更早的提交
'style', // 不影响程序逻辑的代码修改、主要是样式方面的优化、修改
'test' // 测试相关的开发,
],
typeEnum = {
rules: {
'type-enum': [2, 'always', types]
},
value: () => {
return types;
}
};

module.exports = {
extends: ['@commitlint/config-conventional'],
/*
Level [0..2]: 0 disables the rule. For 1 it will be considered a warning for 2 an error.
https://commitlint.js.org/#/reference-rules
*/
rules: {
'type-enum': typeEnum.rules['type-enum'],
'subject-full-stop': [0, 'never'],
'subject-case': [0, 'never']
}
};

配置完毕,测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
➡️ git commit -m "tes: 错误示例 🙅<200d>♂️"

✔ Preparing lint-staged...
✔ Running tasks for staged files...
✔ Applying modifications from tasks...
✔ Cleaning up temporary files...

Commit Msg: tes: 错误示例 🙅‍♂️

ERROR invalid commit message format.

Proper commit message format is required for automated changelog generation. Examples:

feat(compiler): add 'comments' option
fix(input): handle events on blur (close #28)

See .github/commit-convention.md for more details.

husky - commit-msg hook exited with code 1 (error)

当然,整体来说也只是一个推荐性的规范方案,commit 同样可以通过 -n 或者是 -no-verify 直接绕开 commit 检测

添加 ChangeLog

安装

1
2
pnpm i conventional-changelog-cli -D

配置 scripts (packages.json)

1
2
3
4
5
{
"scripts": {
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
}
}

conventional-changelog-cli 不会覆盖任何以前的变更日志。 新增的日志基于自上一个 commit 的 “Feature”, “Fix”, “Performance Improvement” 或 “Breaking Changes”

另外:结合 husky 和自定义 verifyCommits.js 来实现

主要处理逻辑是,修改.husky/commit-commit文件中的执行语句

1
2
3
4
5
6
7
8
# scripts/verifyCommits.js 即为我们自定义的校验文件
# .husky/commit-msg 文件内容:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# npx --no -- commitlint --edit "" 将此语句改为下面直接node执行脚本。
node scripts/verifyCommits.js "$1"

$1 必须在 .husky/commit-msg 文件中写上(在我们使用 git commit -m "[msg]" 时,会传递给 $1 参数),否则我们执行自定义校验 commit 文件时是无法拿到 commit 信息 的(当时花费好长时间去踩这个问题点)

创建 scripts/verifyCommits.js 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const chalk = require('chalk');
// const msgPath = process.env.GIT_PARAMS // 此为 undefined 拿不到 msg 信息
// process.env 对象中没有任何 commit 信息,process.argv 中本来也没有
// pre-commit 里的 $1 必填,这里才能拿到本次的 CommitMsg
const msgPath =
process.env.GIT_PARAMS || process.env.HUSKY_GIT_PARAMS || process.argv[2]; //使用环境变量获取到commit message
// console.log(process.argv,'process.argv');
const msg = require('fs').readFileSync(msgPath, 'utf-8').trim();
console.log(); // 空行,下面打印提示信息
console.log(chalk.bgBlueBright.white('Commit Msg: '), msg);
console.log(); // 打印提示信息后空行

const commitRE =
/^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|ci|chore|types|build)(\(.+\))?: .{1,50}/;

if (!commitRE.test(msg)) {
// 下面是一些错误日志提醒
console.error(
` ${chalk.bgRed.white(' ERROR ')} ${chalk.red(
`提交日志不符合规范`
)}\n\n${chalk.red(
` 合法的提交日志格式如下(emoji 和 模块可选填):\n\n`
)}
${chalk.green(`[<emoji>] [revert: ?]<type>[(scope)?]: <message>\n`)}
${chalk.green(`💥 feat(模块): 添加了个很棒的功能`)}
${chalk.green(`🐛 fix(模块): 修复了一些 bug`)}
${chalk.green(`📝 docs(模块): 更新了一下文档`)}
${chalk.green(`🌷 UI(模块): 修改了一下样式`)}
${chalk.green(`🏰 chore(模块): 对脚手架做了些更改`)}
${chalk.green(`🌐 locale(模块): 为国际化做了微小的贡献\n`)}
${chalk.green(
`其他提交类型: refactor, perf, workflow, build, CI, typos, tests, types, wip, release, dep\n`
)}
${chalk.red(
`See https://github.com/vuejs/core/blob/main/.github/commit-convention.md\n`
)}`
);
process.exit(1); // 如果不规范退出运行进程
}

// const childProcess = require('child_process');

// // 提交记录
// const commitStages = childProcess.execSync('git diff --name-status HEAD~3', {
// encoding: 'utf8',
// });
// console.log('提交记录:', commitStages);