背景
为什么要去做代码规范这件事呢?没来公司前,前端团队5人,原本是有一套简单的代码规范文档的,随着制定人离职,加上前端团队的人员流动大,文档也就废了,这就导致了经手的人很多,项目代码混乱,代码风格不统一。
- 我想你们也应该有碰到过,一个文件七八千行的代码,逻辑混乱,像屎一样。
- 或者是代码提交之后,别人的代码格式化修改了你的代码,你每次拉下来的时候,都要格式化一遍。
- 或者是 一个文件名大写,一个文件名小写,又没有开git 大小写忽略,导致拉代码后,编译的时候报错了。
- 或者是 git 合并分支的时候误把 test 分支的代码合并到了任务分支,导致上线出问题。
这些我都遇到过,除了这些还有很多。
那如何去做代码规范呢?
使用 eslint+prettier+stylelint+husky+lint-staged+自定义git hooks
eslint+prettier+stylelint+husky+lint-staged 这个网上配置教程很多的,可以自行去了解
我们主要来讲一下 自定义 git hooks 和 如何自动化生成 规范配置
verifyCommit(校验 git commit)
// Invoked on the commit-msg git hook by husky.
import chalk from 'chalk';
import { readFileSync } from 'fs';
import type { UserConfigProps } from '../utils/getUserConfig';
import { mergeProps } from '../utils/mergeProps';
import { commitLogs } from '../utils/meta';
const defaultVerifyCommit = {
/** 默认开启 */
open: true,
/** 过滤消息 默认 过滤合并分支、lerna 发布 的校验 */
filterMsg: ['Merge branch', 'Publish'],
/** 校验规则 */
commitRE:
/^((.+) )?(feat|fix|docs|UI|refactor|perf|workflow|build|CI|typos|chore|tests|types|wip|release|deps|locale|revert)(\(.+\))?: .{1,100}/
};
export interface YOption {
/** git husky 参数 */
HUSKY_GIT_PARAMS?: string;
/** 用户配置 */
userConfig?: UserConfigProps;
}
export default function (options: YOption) {
const { HUSKY_GIT_PARAMS, userConfig } = options;
const msgPath = HUSKY_GIT_PARAMS || process.env.GIT_PARAMS || process.env.HUSKY_GIT_PARAMS;
// 不是git commit的时候
if (!msgPath) return;
const msg = readFileSync(msgPath, 'utf-8').trim();
const gitConfig = userConfig?.gitConfig || {};
const { verifyCommit: v } = gitConfig;
const verifyCommit = mergeProps(defaultVerifyCommit, v);
const { open, filterMsg, commitRE, customVerifyCommit } = verifyCommit;
// 关闭时 不校验
if (!open) {
return;
}
if (customVerifyCommit) {
const log = customVerifyCommit(msg);
console.error(log);
process.exit(1);
} else {
const filterStatus = filterMsg.some((item) => {
if (msg.indexOf(item) !== -1) {
return true;
}
});
// 过滤当前校验
if (filterStatus) {
return;
}
}
if (!commitRE.test(msg)) {
console.log();
console.error(
` ${chalk.bgRed.white(' ERROR ')} ${chalk.red(`提交日志不符合规范`)}\n${chalk.red(
` 合法的提交日志格式如下(分支名称 和 模块可选填):\n`
)}
${commitLogs}`
);
process.exit(1);
}
}
上面是一个校验 git commit的代码,通过 husky配置 在 git commit-msg 的时候触发,拿到commit信息,校验一下是否符合规范
verifyMergeBranch(校验 git merge branch)
import fs from 'fs';
import path from 'path';
import type { UserConfigProps } from '../utils/getUserConfig';
import { mergeProps } from '../utils/mergeProps';
/** 获取分支名称 */
function getTargetBranch(MERGE_MSG: string) {
const content = fs.readFileSync(MERGE_MSG, 'utf-8');
if (content) {
const branch = content.split('\n')[0]?.split(' ')[2].replace(/'/g, '');
return branch;
}
}
const defaultVerifyMergeBranch = {
/** 默认开启 */
open: true,
/** 默认禁止合并的分支 */
forbidMergeBranch: ['test', 'test2', 'dev', 'dev2']
};
export interface YOptions {
targetBranch: string;
userConfig?: UserConfigProps;
}
/**
* 校验合并的分支
*/
function verifyBranch(options: YOptions) {
const { targetBranch, userConfig } = options;
const gitConfig = userConfig?.gitConfig || {};
const { verifyMergeBranch: v } = gitConfig;
const verifyMergeBranch = mergeProps(defaultVerifyMergeBranch, v);
const { open, forbidMergeBranch, customVerifyMergeBranch } = verifyMergeBranch;
// 关闭时不校验
if (!open || !targetBranch) {
return;
}
if (customVerifyMergeBranch) {
const status = customVerifyMergeBranch(targetBranch);
if (status) {
throw new Error(`禁止将 ${targetBranch} 分支,合并到当前分支,请使用 git merge --abort 撤销合并`);
}
}
if (forbidMergeBranch.includes(targetBranch)) {
throw new Error(`禁止将 ${targetBranch} 分支,合并到当前分支,请使用 git merge --abort 撤销合并`);
}
}
export interface YVerifyMergeBranch {
/** git hooks */
hooks: 'pre-merge-commit' | 'pre-commit';
cwd: string;
userConfig?: UserConfigProps;
}
export default function (options: YVerifyMergeBranch) {
const { hooks, cwd, userConfig } = options;
if (hooks === 'pre-merge-commit') {
const { GIT_REFLOG_ACTION } = process.env;
const targetBranch = GIT_REFLOG_ACTION?.split(' ')?.[1];
if (!targetBranch) return;
verifyBranch({ targetBranch, userConfig });
} else if (hooks === 'pre-commit') {
// git merge msg 文件 存在则说明是合并请求
const MERGE_MSG = path.resolve(cwd, '.git/MERGE_MSG');
if (fs.existsSync(MERGE_MSG)) {
const targetBranch = getTargetBranch(MERGE_MSG);
if (!targetBranch) return;
verifyBranch({ targetBranch, userConfig });
}
}
}
上面代码是校验 git 分支 合并的时候,可以设置一个黑名单,比方说禁止合并 test 分支。通过 husky 配置 在 pre-merge-commit 和 pre-commit 的时候触发。
为什么要在 pre-commit 的时候也触发一下呢,因为如果合并的时候有冲突的话,pre-merge-commit 不会触发,所以在解决冲突之后,commit的时候,我们也校验一下
自动化生成 规范配置
为什么要自动化生成 规范配置,因为要在老项目里面接入的话,如果你一个一个配置的搞,可能会出现意想不到的问题,而且还麻烦。当然新项目的话,cli 模版里面一定有集成这个的
我们这个自动化生成效果应该是怎么样的,我们可以先想想一下期望效果或者是伪代码。这个在我们日常开发中,其实也是通用的,比方我们要开发一个组件,在设计 API 的时候,我们可以写一下伪代码,来使用组件,或者是先写单测再开发
期望效果:
// 运行该命令之后,自动就可以在项目里面自动生成配置
npm run pig-fabric:install
// 生成 eslint+prettier+stylelint+husky+lint-staged+自定义git hooks 配置文件
创建 install 命令
import chalk from 'chalk';
import copyfiles from 'copyfiles';
import deleteAsync from 'del';
import execa from 'execa';
import fs from 'fs';
import inquirer from 'inquirer';
import path from 'path';
import { getNpmExec } from '../utils';
import {
copyFiles,
delOriginalConfig,
devDependencies,
huskyCopyFiles,
lintStaged,
originalConfigFiles,
scripts
} from '../utils/meta';
const { prompt } = inquirer;
interface YOption {
root: string;
}
export default class Init {
root: string;
constructor(options: YOption) {
this.root = options.root;
this.install();
}
async install() {
await this.detectionConfig();
await this.setPackage();
await this.addHusky();
await this.createFiles();
console.log('✅ The fabric is successfully initialized. ');
}
// 检测 是否已经存在 eslint prettier stylelint husky 配置
async detectionConfig() {
const gitPath = path.join(this.root, '.git');
if (!fs.existsSync(gitPath)) {
const url = 'https://typicode.github.io/husky/#/?id=custom-directory';
console.log(chalk.red(`.git can't be found (see ${url})`));
process.exit(0);
}
const packagePath = path.join(this.root, 'package.json');
if (!fs.existsSync(packagePath)) {
console.log("package.json can't be found");
process.exit(0);
}
const status = originalConfigFiles.some((file) => {
const filePath = path.join(this.root, file);
if (fs.existsSync(filePath)) {
return true;
}
return false;
});
if (status) {
const { delOriginalConfig: name } = await prompt(delOriginalConfig);
if (name === 'no') {
process.exit(0);
} else {
await this.delFiles(originalConfigFiles);
}
}
}
// 删除已经存在的 配置文件
async delFiles(files: string[] | string) {
const _files = this.pathJoin(files);
await deleteAsync(_files);
}
// 路径拼接
pathJoin(files: string | string[], root: string = this.root) {
let _files = Array.isArray(files) ? files : [files];
_files = _files.map((file) => {
return path.join(root, file);
});
return _files;
}
createConfigFile(files: string[], target?: string) {
const targetSrc = target ? path.join(this.root, target) : this.root;
return new Promise((resolve, reject) => {
const copyFilesPaths = this.pathJoin(files, path.join(__dirname, '../../template'));
copyfiles([...copyFilesPaths, targetSrc], { up: true }, (err) => {
if (err) {
reject(err);
} else {
files.forEach((file) => {
const fileSrc = target ? `${target}/${file}` : file;
console.log(`create a file for ${chalk.yellow(fileSrc)}`);
});
resolve(true);
}
});
});
}
// 修改 package
setPackage() {
const packagePath = path.join(this.root, './package.json');
const packageContent = require(packagePath);
packageContent.scripts = {
...packageContent.scripts,
...scripts
};
packageContent.devDependencies = {
...packageContent.devDependencies,
...devDependencies
};
packageContent['lint-staged'] = lintStaged;
fs.writeFileSync(packagePath, JSON.stringify(packageContent, null, 4));
}
// 添加 husky
async addHusky() {
const npmExec = getNpmExec(this.root);
// 安装依赖
await execa(npmExec, ['install'], { stdio: 'inherit', shell: true });
await execa(npmExec, ['run', 'prepare'], { stdio: 'inherit', shell: true });
}
async createFiles() {
// 创建 eslint prettier stylelint 配置文件
await this.createConfigFile(copyFiles);
// 创建 husky 相关 hooks
await this.createConfigFile(huskyCopyFiles, `.husky`);
}
}
上面的核心思路就是执行 install 初始化命令的时候,把 template 里面的 eslint prettier stylelint husky 这些模版文件,复制到项目中,修改 package.json,添加依赖包 、添加 scripts 命令。
之后就是 打包,发npm包
在团队中推广落地
至此代码规范工具已经开发好了,但如何在团队中推广使用,确定下来呢?我这边是前端团队成员一起开会讨论,确定规范。比方 eslint 规则 是 关闭/警告/报错,代码缩进风格是怎么样的等等等等。让大家认同这份规范,之后形成文档,同规范工具同步,然后再接入项目中
拓展
当然了,你可以会说,上面说的文件代码太长,文件命名不规范这些还没有讲怎么处理,其实思路都是通的,你用node 写个方法,在git 提交的时候去检查文件,不就可以了吗
很简单吧,相信你也能搞。
参考资料
- @umijs/fabric github.com/umijs/fabri… (源码写的很不错,我借鉴(抄)了不少)
代码地址
源码可以看 github.com/pigjs/fabri…
npm 包可以使用 @pigjs/fabric
对细节感兴趣的,或者有疑问/不同看法的,可以加我微信私聊 MrYeZiqing,备注 前端