作为前端leader我是如何统一前端代码规范的

340 阅读5分钟

背景

为什么要去做代码规范这件事呢?没来公司前,前端团队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 提交的时候去检查文件,不就可以了吗

很简单吧,相信你也能搞。

参考资料

代码地址

源码可以看 github.com/pigjs/fabri…

npm 包可以使用 @pigjs/fabric

对细节感兴趣的,或者有疑问/不同看法的,可以加我微信私聊 MrYeZiqing,备注 前端