众所周知,javascript一开始就是一种非常灵活的语言,随着新特性不断增加,框架层出不穷,加之个人的编程风格。前端代码的可维护性问题越来越突出。做一个有工程素养的前端的开发,我们要注重编码规范。做为架构或者团队的leader,更应该考虑编码规范的约束机制。所以做好代码风格检查(Code Linting,简称 Lint)已经成为前端团队必修课,是保障代码规范一致性的重要手段。 做好link可以有效的减少bug,提升开发效率和代码的可读性。其中,提交前lint更有效率。 如何着手呢?自然是从我们的代码管理工具。
Git hooks
现在最流行的版本管理工具非Git莫属,Git本身也增加了一些hooks(钩子),在git命令前置执行来阻止一些不规范的操作。
$ cd .git/hooks
$ ls -a
git hooks是在.git/hooks目录下的一些脚本文件,用于控制git工作的流程。- 内置的脚本示例都是
shell脚本,其中一些还混杂了Perl代码,不过,任何正确命名的可执行脚本都可以正常使用 —— 你可以用Ruby或Python,当然我们前端也可以用Node来编写。 - 钩子分为客户端钩子和服务端钩子。
- 客户端钩子:
pre-commit、prepare-commit-msg、commit-msg、post-commit等,主要用于控制客户端git的提交工作流。 - 服务端钩子:
pre-receive、post-receive、update,主要在服务端接收提交对象时、推送到服务器之前调用。 - 钩子都是以
.sample结尾的文件名。注意这些示例脚本是不会执行的,只有重命名去掉.sample后才会生效。
但是直接使用git hooks不方便在团队内推广。需要有工具自动把脚本安装到每个人的本地项目上才能生效。所以我们需要借助一些其他工具库。
Husky
前端常用的git hooks工具有 pre-commit 和 Husky。这里我只介绍 Husky(更全面一些)。
原理
husky利用 git hooks会在相关命令执行前执行的特性,取而代之。
#!/bin/sh
# husky
export HUSKY_GIT_PARAMS="$*"
node_modules/run-node/run-node ./node_modules/husky/lib/runner/bin `basename "$0"`
...
husky使用了自定义的安装过程:node lib/installer/bin install(在node_modules/husky/package.json里)。执行的时会在项目的.git/hooks目录生成所有hook的脚本(你自定义的hook脚本,husky不会覆盖)。每个
hook脚本都是一样的, 关键的部分是bashname "$0",这样可以拿到当前的hook名,如pre-commit、pre-push。最后根据
package.json的配置,执行我们定义的,相对应的hook脚本(我们可以用node写)。
安装
npm install husky --save-dev
// 或者
yarn add husky --dev
配置
//package.json文件
"husky": {
"hooks": {
"pre-commit": "eslint",
"commit-msg": "node preCommit.js" // 可以集成到自己框架的的cli中,比如:luna preCommit
}
}
- 当你
git commit的时候,会触发hook(pre-commit),husky将会执行对应配置(pre-commit)里的eslint命令,没有问题才提交。 - 当你
git commit的时候,会触发hook (commit-msg),husky将会执行对应配置(commit-msg)里的preCommit.js脚本,没有问题才提交。
实际应用场景一:Commit message格式校验
上面配置中的preCommit.js文件是按公司要求,校验提交的message必须符合规定格式的脚本,代码如下:
/*
* 功能: git commit时,自动验证提交信息是否符合规范
* 提交规范: 范式 {ir_key}: {subject_content}.例如:"STY-ABCD-TY-76379:某个功能开发"
* 主要是读取 .git/COMMIT_EDITMSG 这个文件,文件记录了当前commit之后的信息
*/
const fs = require('fs');
const chalk = require('chalk');
const warning = chalk.keyword('red');
const msg = chalk.keyword('yellow');
// const link = chalk.hex('#00bfff');
const pattern = /^((STY|DTK)-ABCD-TY-)\d{5}:[^]/;
const commitMsg = fs.readFileSync(process.env.HUSKY_GIT_PARAMS, 'utf-8').trim();
if (!pattern.test(commitMsg.toUpperCase())) {
console.log(msg(`\nYour commit message: ${commitMsg}\n`));
console.log(warning('-----------------------Git提交Message不符合规范------------------------------\n'));
console.log(msg('范式:{ir_key}: {subject_content}。“ir_key”不区分大小写,“冒号”必须半角英文\n'));
console.log(msg('示例1:DTK-ABCD-TY-76379: 某个bug fix\n'));
console.log(msg('示例2:STY-ABCD-TY-76379: 某个功能开发\n'));
console.log(warning('-----------------------------------------------------------------------------\n'));
process.exit(1);
}
process.exit(0);
实际应用场景二:Commit msg自动格式化
我们公司commit msg要求范式:{ir_key}: {subject_content}。即具体信息前面要加ir_key:(需求号)。那我们如何自动在msg前面添加{ir_key}:。只需解决两个问题:
- 如何修改
commit msg ir_key从哪里读取
解决思路:
- 从场景一代码了解到,当前
commit的msg是通过git/COMMIT_EDITMSG这个文件获取。那我们可以通过hooks修改这个文件的内容,便可以修改msg。 - 同样,我们可以参考以上
git的策略,我们只要把ir_key存在一个本地文件中,commit的时候读取即可。
具体方案:
我在项目的公共工具库项目新增了两个cli命令。
irk <ir_key>: 独立使用。后面带参数,且参数符合msg规范,则存入本地数据文件.git/msg(这样不会提交到远程,也无需再加.gitignore);无参数,则读取.git/msg,打印当前分支的ir_key存储情况。数据文件中ir_key是按分支存储的。commit-msg-init: 配合hookcommit-msg使用。首先校验当前msg是否合规,如果不合规,且存在当前分支的ir_key缓存数据,则在当前msg前拼接ir_key, 存入git/COMMIT_EDITMSG文件。
使用效果如下:
irk核心代码如下:
try {
// 获取当前分支号
const branchName = getBranchName()
// 读取本地数据缓存文件: .git/msg
let dataObj = getFileObj(tempFilePath);
// ir_key 需符合格式
const pattern = /^((STY|DTK|BUG)-ABCD-(TY|GJ)-)\d{5}/;
// 读取参数
const param = process.argv[2];
// 有参数set,无参数get
if ( param ) {
if (!pattern.test(param.toUpperCase())) {
// 不符合格式报错
console.log(warning('Wrong irk, Need to match:/^((STY|DTK|BUG)-ABCD-(TY|GJ)-)\d{5}/\n'))
} else {
// 写入或复写
dataObj[branchName] = param;
setIrk(dataObj)
}
} else {
//读取显示
if (dataObj[branchName]) {
console.log(msg(`irk: ${dataObj[branchName]} \n`))
} else {
console.log(msg("No irk for current branch"))
}
}
} catch (err) {
console.log(warning('Process err: ' + err))
}
commit-msg-init核心代码如下:
const fs = require('fs');
const chalk = require('chalk');
const { getBranchName, getFileObj, printMsgRuleLog } = require('../utils/utils')
const { tempFilePath } = require('../config/config')
// msg格式
const pattern = /^((STY|DTK|BUG)-ABCD-(TY|GJ)-)\d{5}:[^]/;
// 当前commit msg
const commitMsg = fs.readFileSync(process.env.HUSKY_GIT_PARAMS, 'utf-8');
// 获取当前分支号
const branchName = getBranchName()
// 如果当前msg不符合规范
if (!pattern.test(commitMsg.toUpperCase())) {
// 读取本地数据缓存文件: .git/msg
const fileObj = getFileObj(tempFilePath);
// 如果文件有数据,并且存在当前分支的数据
if ( fileObj && fileObj[branchName] ) {
// 拼接
const newCommitMsg = `${fileObj[branchName]}:${commitMsg}`
// 保险起见,二级校验拼接好的msg
if (pattern.test(newCommitMsg.toUpperCase())){
// 新msg写入文件
fs.writeFileSync(process.env.HUSKY_GIT_PARAMS,newCommitMsg)
console.log(chalk.green(`\n Formatted Message:${newCommitMsg}\n`))
process.exit(0);
} else {
//打印规范日志
printMsgRuleLog(commitMsg);
process.exit(1);
}
} else {
printMsgRuleLog(commitMsg);
process.exit(1);
}
}
process.exit(0);
实际应用场景三:Commit 文件校验
比如限制某些类型文件,某个特定目录的文件不允许修改,删除。
以下代码来自:https://github.com/y8n/git-hooks-node/blob/master/xgfe-ma/pre-commit.js#L45-L73
var child_process = require('child_process');
var execSync = child_process.execSync;
var spawnSync = child_process.spawnSync;
var path = require('path');
var files = getDiffFiles();
if (!files.length) {
quit();
}
var libFiles = files.filter(function (file) {
return isLibFiles(file.subpath) && ~['d', 'm', 'c', 'r'].indexOf(file.status);
});
if (libFiles.length) {
console.log('[WARNING] You cannot delete/modify/copy/rename any file in lib directory!!\n' +
'Listed below are thus files:');
var libFilePaths = libFiles.map(function (file) {
return file.subpath;
}).join('\n');
console.log(libFilePaths + '\n');
quit(1);
}
// 待检查的文件相对路径
var lintFiles = files.filter(function (file) {
return !isLibFiles(file.subpath)
&& !isDistFiles(file.subpath)
&& ~['a', 'm', 'c', 'r'].indexOf(file.status);
}).map(function (file) {
return file.subpath;
});
if (!lintFiles.length) {
quit();
}
var argv = ['lint'];
argv = argv.concat(lintFiles);
argv = argv.concat(['-c', './.lintrc']);
var result = spawnSync('xg', argv, {stdio: 'inherit'});
quit(result.status);
/**
* 获取所有变动的文件,包括增(A)删(D)改(M)重命名(R)复制(C)等
* @param [type] {string} - 文件变动类型
* @returns {Array}
*/
function getDiffFiles(type) {
var DIFF_COMMAND = 'git diff --cached --name-status HEAD';
var root = process.cwd();
var files = execSync(DIFF_COMMAND).toString().split('\n');
var result = [];
type = type || 'admrc';
var types = type.split('').map(function (t) {
return t.toLowerCase();
});
files.forEach(function (file) {
if (!file) {
return;
}
var temp = file.split(/[\n\t]/);
var status = temp[0].toLowerCase();
var filepath = root + '/' + temp[1];
var extName = path.extname(filepath).slice(1);
if (types.length && ~types.indexOf(status)) {
result.push({
status: status, // 文件变更状态-AMDRC
path: filepath, // 文件绝对路径
subpath: temp[1], // 文件相对路径
extName: extName // 文件后缀名
});
}
});
return result;
}
/**
* 是否是lib目录下的文件
*/
function isLibFiles(subpath) {
return subpath.match(/^src\/lib\/.*/i);
}
/**
* 是否是dist目录下的文件
*/
function isDistFiles(subpath) {
return subpath.match(/^dist\/.*/i);
}
/**
* 退出
* @param errorCode
*/
function quit(errorCode) {
if (errorCode) {
console.log('Commit aborted.');
}
process.exit(errorCode || 0);
}
Eslint
ESLint是一个用来识别 ECMAScript 并且按照规则给出报告的代码检测工具,使用它可以避免低级错误和统一代码的风格。它附带有大量的规则.
运行 eslint --init 之后,.eslintrc 文件会在你的文件夹中自动创建。你只要在文件的rules属性中配置你想要的规则,利用pre-commit钩子触发校验即可。它主要的特点是:
- 使用 Espree 解析 JavaScript。
- 使用 AST 去分析代码中的模式。
- 完全插件化的。每一个规则都是一个插件并且你可以在运行时添加更多的规则。
eslint中文网
Lint-staged
直接触发eslint进行代码检测有一个问题:引入初期,你只改了文件 A,但是文件 B、C、D …中也有大量错误。你基本上没有时间和勇气去fix所有lint错误。这个时候,很多同学(包括我)选择 git commit -m "fix bug" --no-verify来逃避。只是‘很负责任’的把文件A的错误解决。
如果每次提交只检查本次提交所修改的文件,上面的痛点就解决了。lint-staged的开发者就是基于这个想法,其中 staged 是Git里面的概念,指待提交区,使用 git commit -a,或者先 git add 然后 git commit 的时候,你的修改代码都会经过待提交区。
安装依赖:
npm install lint-staged --save-dev
// 或者
yarn add lint-staged --dev
引入Lint-staged之后, 之前的husky配置升级如下:
//package.json文件
"husky": {
"hooks": {
"pre-commit": "lint-staged", // 看这里
"commit-msg": "node preCommit.js"
}
},
"lint-staged": { // 和这里
"src/**/*.js": "eslint"
}
prettier
prettier 是业界主流的代码风格格式化工具。虽然用eslint —fix也可以进行代码格式化,但是eslint已经配置繁多。我们还是用eslint检查代码,用prettier来格式化代码。术业有专攻。
- 你可以在vscode安装
Prettier- Code formatter插件。默认快捷键是alt + shift + f。安装成功后,编辑器的配置setting.json会出现prettier插件的相关配置节点,同时也能看到一些默认的配置信息。 - 实际项目中推荐在根目录创建
.prettierrc文件配置(比配置在Package.json更独立)来使用,这样配置可以集成到脚手架,保证所有项目规则统一。
常用配置如下:
module.exports = {
"printWidth": 80, //一行的字符数,如果超过会进行换行,默认为80
"tabWidth": 2, //一个tab代表几个空格数,默认为80
"useTabs": false, //是否使用tab进行缩进,默认为false,表示用空格进行缩减
"singleQuote": false, //字符串是否使用单引号,默认为false,使用双引号
"semi": true, //行位是否使用分号,默认为true
"trailingComma": "none", //是否使用尾逗号,有三个可选值"<none|es5|all>"
"bracketSpacing": true, //对象大括号直接是否有空格,默认为true,效果:{ foo: bar }
"parser": "babylon" //代码的解析引擎,默认为babylon,与babel相同。
}
更多配置,可以参考官网: prettier 官网