最近公司的项目的文件名要进行规范,首先是同事写了一个检查不规范文件名的脚本,然后我又写了一个全局搜索替换不规范文件名的脚本,接下来首先进行node的学习。
一、node模块学习
本章针对不熟悉node模块的同学,已经熟悉的可以跳过。
1.1 fs和path模块
/**
* fs 文件系统
* 分为同步和异步 推荐使用异步
* 异步函数的最后一个参数是一个回调函数,回调函数的参数第一个是报错信息,第二个是数据。
*
* 常用函数:
*
* readdir(path,callback) 异步 读取目录的内容
* readdirSync(path) 同步
*
* readFile(path,function(err,data)) 异步 读取文件的内容
* readFileSync(path) 同步
*
* writeFile(filename, data[, options], callback) 异步 写入文件内容 写入方式默认是w,覆盖原有的内容
* writeFileSync(filename, data[, options]) 同步
*
* stat(path,callback) 异步 path文件路径 如果直接写路径要写绝对路径,否则使用__dirname,
* 回调函数有err,stats两个参数,stats是fs.Stats返回的对象
* 返回的stats实例有方法如:
* isFile() 是否是文件
* isDirectory() 是否是目录
* statSync(path) 同步
*
* stat和readFile区别,stat用来初步解析目录中的文件和文件夹,而readFile用来读取具体文件的内容。
*
* path模块
* path.join(path1,path2) 把传入的路径进行连接
* path.resolve([from],to) 把传入的to参数,结合前面的from参数,解析为一个绝对路径。
* path.dirname(p) 路径中文件夹部分,即当前执行脚本所在的文件夹的路径
* path.basename(p) 路径的最后一段,即当前脚本的文件名
* path.extname(p) 文件的扩展名
*/
- 首先公共的引入部分
//引入文件系统
const fs = require("fs");
//引入处理文件路径
const path = require('path');
//连接文件路径
var filepath = path.join(__dirname,'/test.txt');
//test.txt内容为content
- readdir()示例
//遍历目录
//同步遍历
fs.readdir(__dirname,function(err,files){
if(err){
console.log(err)
}else{
files.forEach(function(file){
console.log(file)
})
}
})
//异步遍历
var files = fs.readdirSync(__dirname);
console.log(“异步”+ files)
//执行结果
异步 [ 'test.js', 'test2.js', 'test.txt', 'test1' ]
test.js
test2.js
test.txt
test1
- readFile()、readFileSync()示例
// 异步读取
fs.readFile(filepath,function(err,data){
if(err){
console.error(err);
}else{
console.log('异步读取',data.toString());
}
})
// 同步读取
var data = fs.readFileSync(filepath);
console.log("同步读取: " + data.toString());
console.log("程序执行完毕。");
//执行结果:
同步读取: content
程序执行完毕。
异步读取:content
- stat()示例
//解析目录中的文件或文件夹
fs.stat(filepath,function(err,stats){
if(err){
console.error(err)
}else{
console.log("读取的文件内容"+stats);//stats是一个对象
console.log("是否是文件",stats.isFile()); //true
console.log("是否是目录",stats.isDirectory()); //false
//如果是文件就可以进一步读取文件的内容
if(stats.isFile()){
console.log('文件内容为',fs.readFileSync(filepath,'utf-8'));
}
}
})
//执行结果:
读取的文件内容[object Object]
是否是文件 true
是否是目录 false
文件内容为 content
writeFile()示例
//写入文件
fs.writeFile(filepath,'newcontent',function(err){
if(err){
console.error(err);
}
console.log('写入数据成功')
fs.readFile(filepath,function(err,data){
console.log("读取新数据",data.toString())
})
})
//执行结果:
写入数据成功
读取新数据 newcontent
test.txt内容变为newcontent
- 最后结合起来的用法
* 遍历文件步骤为:
* readdir遍历目录(传入的是一个目录),
* stat对每个文件或文件夹进行解析(传入的通常是一个通过path.join组合或者path.resolve解析成的目录和每个文件的路径),
* 判断如果是文件的话用readFile读取文件的内容,
* 通过正则匹配到内容进行替换,然后writeFile写入文件内容。
fs.readdir(__dirname,function(err,files){
if(err){
console.log(err)
}else{
//遍历每一个文件
files.forEach(function(file){
console.log(file)
//构造文件的路径
var filePath = path.join(__dirname,file);
//解析文件
fs.stat(filePath,function(err,stats){
if(err){
console.error(err)
}else{
//如果是文件就可以进一步读取文件的内容
if(stats.isFile()){
console.log('文件内容为',fs.readFileSync(filePath,'utf-8'));
//写入新内容
fs.writeFile(filePath,'newcontent')
}else{
//继续遍历
}
}
})
})
}
})
//执行结果就是newcontent
1.2 minimist模块
process.argv 从node命令行中读取传入的参数
如执行命令 :node gitHooks/check-filename.js --scope=client/src/baseStore --module=all
打印process.argv结果如下:
是一个数组,第一个参数是node程序所在位置,第二个参数是js脚本所在位置,之后的参数就是输入的命令行参数了。
[ 'C:\\Program Files\\nodejs\\node.exe',
'D:\\storm\\gitHooks\\check-filename.js',
'--scope=client/src/baseStore',
'--module=all' ]
要想使用这些参数,需要通过minimist 命令行参数解析引擎,对参数进行处理,
const minimist = require('minimist');
const argvs = minimist(process.argv.slice(2))
//打印结果如下,数组被转换成对象的形式
{ _: [], scope: 'client/src/baseStore', module: 'all' }
就可以通过argvs.scope/argv.module 获取到传入的参数的值
1.3 child_process模块
child_process子进程模块,用来创建一个子进程,并执行任务,例如shell命令,可以在js中直接调用shell命令去执行一些操作如git操作。
exec(command,[, options][, callback]) 异步 要执行的命令
execSync(command,[, options]) 同步
//获取到git add 提交到暂存区的文件名列表
let filesList = execSync("git diff --cached --name-only").toString('utf8').trim().split("\n");
//执行结果
[ 'client/src/baseStore/LoadBaseParams.js',
'client/src/baseStore/actions.js' ]
1.4 export-excel模块
用来导出excel表格。用法如下:
var express = require('express');
var nodeExcel = require('excel-export');
var app = express();
app.get('/Excel', function(req, res){
var conf ={};
conf.stylesXmlFile = "styles.xml";
conf.name = "mysheet";
conf.cols = [{
caption:'string',
type:'string',
beforeCellWrite:function(row, cellData){
return cellData.toUpperCase();
},
width:28.7109375
},{
caption:'date',
type:'number',
}];
conf.rows = [
['pi', 3.14],
["e", 2.7182],
["M&M<>'", 1.61803],
["null date", 1.414]
];
var result = nodeExcel.execute(conf);
res.setHeader('Content-Type', 'application/vnd.openxmlformats');
res.setHeader("Content-Disposition", "attachment; filename=" + "Report.xlsx");
res.end(result, 'binary');
});
二、检查文件命名规范脚本
通过以上的几个模块的学习,就能读懂检查文件命名规范的脚本啦,如下所示:
运行命令示例 node gitHooks/check-filename.js --scope=src --module=all
--scope可以设置从项目根目录的哪里进行检测,module可以设置all对所有文件进行检测,以及commit只对本次提交的文件名进行检测。
commit的实现是通过git的钩子函数pre-commit来执行的,
首先安装
npm i yorkie lint-staged@8.1.0 --save
然后在package.json中写法如下,在我们git commit 时,会执行到pre-commit,然后会关联到lint-staged,就会执行这条命令来进行检查,添加参数--noVerify可以跳过检查。
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*": [
"node gitHooks/check-filename.js --noVerify --scope=client --module=commit"
]
}
具体脚本如下:
const { execSync } = require("child_process");
const fs = require('fs')
const path = require('path')
const minimist = require("minimist")
const rootPath = path.join(__dirname, '..') //工程根目录路径
const rootDirName = rootPath.slice(rootPath.lastIndexOf('/') + 1) //工程文件夹名
const fileReg = /^[a-z]+[-0-9a-z]{0,}[^-]$/; //数字小写字母中华线组成字符串,且不以数字中华线开头,不以中华线结尾
const imgReg = /^[a-z]+[-@0-9a-z]{0,}[^-@]$/; //图片正则z
const imgExtArr = ['png','jpg','jpeg','gif']
let errorName = [];
//检查模式参数
let argv = 'commit';
//解析命令行参数
let argvParams = minimist(process.argv.slice(2))
if (argvParams['noVerify']) {
console.log('-------no-verify 跳过检查------')
process.exit(0);
return
}
if (!argvParams['scope']) {
console.log('-------no scope 请检查检测范围------')
process.exit(-1);
return
}
if (argvParams.module == 'all' || argvParams.module == 'commit') {
argv = argvParams.module;
}
console.log('====检测模式参数====:' + argv)
if (argv == 'commit') {
console.log('-------检测提交文件---------')
let filesList = getFilenamesByCommand("git diff --cached --name-only").split("\n");
filesList = filesList.filter(filePath=>{
return filePath.indexOf(`${argvParams['scope']}`) != -1
})
console.log('待检测文件数量:' + filesList.length)
errorName = getErrorFileName(filesList);
} else {
console.log('---------检测全部文件--------')
const rootDirs = path.resolve(path.join(__dirname, `../${argvParams['scope']}`))
let filesList = getAllFiles(rootDirs);
console.log('待检测文件数量:' + filesList.length)
errorName = getErrorFileName(filesList);
}
//获取command命令对应的文件
function getFilenamesByCommand(command) {
return execSync(command).toString('utf8').trim();
}
function getAllFiles(dirs) {
let files = [];
let fileList = fs.readdirSync(dirs);
for (let file of fileList) {
const currentPath = path.resolve(dirs, file)
const stat = fs.statSync(currentPath)
const isDirectory = stat.isDirectory();
if (isDirectory) {
files = files.concat(getAllFiles(currentPath))
} else {
files.push(currentPath)
}
}
return files
}
//获取不符合规范的文件路径
function getErrorFileName(fileList) {
let fileNames = [];
for (let i = 0; i < fileList.length; i++) {
const filePath = fileList[i];
const arr = filePath.split("/");
const filename = arr[arr.length - 1];
const extname = path.extname(filePath).slice(1);
let isMatch = false;
if(imgExtArr.indexOf(extname) == -1){
isMatch = fileReg.test(filename.split('.')[0]);
}else{
isMatch = imgReg.test(filename.split('.')[0]);
}
if (!isMatch) {
//路径截取
const index = filePath.indexOf(rootDirName);
fileNames.push(filePath.slice(index < 0 ? 0 : index));
}
}
return fileNames;
}
if (errorName.length) {
console.log('不符合规范文件数量:' + errorName.length + '\n');
console.log('文件规则:数字、小写字母、中华线 组成字符串,且不以数字中华线开头,不以中华线结尾' + '\n');
console.log('图片规则:数字、小写字母、中华线、@符 组成字符串,且不以数字中华线开头,不以中华线、@符结尾' + '\n');
console.log('不符合规范文件:' + '\n');
console.log(errorName.join('\n'))
process.exit(-1);
} else {
console.log('is ok to commit')
process.exit(0);
}
但是在使用过程中发现一个问题,就是在windows系统上对all所有文件进行检测的时候脚本检测的不正确,原因是在windows上通过 const currentPath = path.resolve(dirs, file) 获取到的路径再经过 files.push(currentPath) 处理就会变成 这种双斜杠的形式,'D:\\storm\\client\\src\\baseStore\\actions.js',所以在对文件路径进行分割出文件名时const arr = filePath.split("/");,就不能分割出正确的文件名,导致判断错误。对于commit情况,获取的路径是正确的,就可以判断正确。
使用os模块或者process.platform()函数就可以判断当前的操作系统,如果是windows就用\\来分割,就能正确检测了。
三、搜索替换不规范文件名脚本实现
大致思路是:
- 首先搜索出输入的搜索范围内的不符合规范的文件,生成一个文件名数组。
- 对文件名数组进行遍历,并根据另一个搜索范围,这个范围是搜索引入了此不规范文件的文件,如import、require。
- 根据正则匹配,对文件内容匹配以斜杠开头的不规范文件名,因为这样的一般都是引入的文件,如/searchInput。(目前没有发现以数字中划线开头,以中划线结尾的文件,而且不知道要改成什么,所以只针对驼峰命名形式的进行了修改。)
- 再次根据正则匹配,对此驼峰字符串转换成用中划线连接的字符串,如/search-input。
- 最后把此不规范的文件的文件名进行修改。
需要注意的是:
- 两个搜索范围最好设置成一样的,它会搜索出范围内所有的不规范的名字,然后匹配每一个去全局搜索替换,这样不容易有疏漏。
- 搜索范围不要指定到项目根目录下,因为像一些.git目录下的文件,fs是打不开的,会报错,最好指定到项目核心代码目录下。
- 根目录下的bulid、config等文件,在等全局替换完之后,要进行检查,可能会有引用scss文件的,需要手动矫正一下。
- 一些html中使用组件时,可能会有例一这样的写法,工具无法做到面面俱到,这些个别的情况目前需要手动矫正一下。
- 对于例二这样的写法,也是个别情况,也需要手动矫正。
- 图片经过fs的阅读会导致损坏,需要重新替换一下。
- 如例三,不规范的文件名和所在目录名重名的,会导致把目录也替换掉,需要手动矫正一下。
- 目前脚本还不太完善,只能对一般情况进行修改,降低人工劳动成本
//例一
<MultipleSelectDeptAndEe
>
<div>
</div>
</MultipleSelectDeptAndEe>
//例二
if (applyType == 5) {
//外出申请/
return t.re('OutboundApply');
} else if (applyType == 9) {
//加班
return t.re('OvertimeApply');
}
//例三
import MultipleSelectDeptAndEe from 'com/MultipleSelectDeptAndEe ';
//目录名和里面的MultipleSelectDeptAndEe.vue同名,导致误改。
import MultipleSelectDeptAndEe from 'com/multiple-select-dept-and-ee';
如何使用:
- 可在package.json中的script中添加npm快捷命令使用:
"rename": "node gitHooks/change-filename.js --searchScope=client --replaceScope=client"
npm run rename
2.直接使用:node gitHooks/change-filename.js --searchScope=client --replaceScope=client
3.其中 searchScope 和replaceScope默认是client ,可省略、也可自定义
坑:一开始改文件名使用的是fs.rename() 在本地可以看到像Index、List,已经修改为了index、list,但当提交到git上后,查看文件名还是Index、List,
查了方法 说配置一下区分大小写就行,git config core.ignorecase false,然后git提交发现,保留了之前的大写Index,并新增了全部是我修改的index文件,这样肯定不行。
所以最后使用的是git rename ,这样git就能识别到文件是重命名了。
具体脚本如下:
const fs = require('fs')
const path = require('path')
const minimist = require("minimist")
const express = require('express');
const nodeExcel = require('excel-export');
const {execSync} = require('child_process');
const app = express();
const rootPath = path.join(__dirname, '..') //工程根目录路径
const rootDirName = rootPath.slice(rootPath.lastIndexOf('/') + 1) //工程文件夹名
const fileReg = /^[a-z]+[-0-9a-z]{0,}[^-]$/; //数字小写字母中华线组成字符串,且不以数字中华线开头,不以中华线结尾
//检测的文件类型
const checkExtArr = ['vue','js','ts','scss','css'];
//不符合规范的文件名路径
let errorName = [];
//不符合规范的文件名
const fileNameArr = [];
//替换次数
let index = 0;
//excel数据
var temp = [];
//解析命令行参数
let argvParams = minimist(process.argv.slice(2))
//不填写范围默认client下
let searchScope = 'client';
let replaceScope = 'client';
//示例命令行写法: node gitHooks/change-filename.js --searchScope=client --replaceScope=client
if (argvParams['searchScope']) {
searchScope = argvParams['searchScope'];
}
if (argvParams['replaceScope']) {
replaceScope = argvParams['replaceScope']
}
//获取所有的文件
function getAllFiles(dirs) {
let files = [];
let fileList = fs.readdirSync(dirs);
for (let file of fileList) {
const currentPath = path.resolve(dirs, file)
const stat = fs.statSync(currentPath)
const isDirectory = stat.isDirectory();
if (isDirectory) {
files = files.concat(getAllFiles(currentPath))
} else {
files.push(currentPath)
}
}
return files
}
//获取不符合规范的文件路径
function getErrorFileName(fileList) {
let fileNames = [];
for (let i = 0; i < fileList.length; i++) {
//完整的路径
const filePath = fileList[i];
let arr = [];
//因为windows获取的文件路径有问题,判断如果是windows并且是all的情况就用\\分割 其他windows情况未测
if(process.platform == 'win32'){
arr = filePath.split("\\");
}else{
arr = filePath.split("/");
}
//获取到文件名
const filename = arr[arr.length - 1];
const fileName = filename.split('.')[0];
//文件扩展名
const extname = path.extname(filePath).slice(1);
let isMatch = false;
//只对vue、js、ts、scss、css文件进行检测
if(checkExtArr.indexOf(extname) != -1){
isMatch = fileReg.test(fileName);
}else{
isMatch = true;
}
if (!isMatch) {
//路径截取
const index = filePath.indexOf(rootDirName);
fileNames.push(filePath.slice(index < 0 ? 0 : index));
//组成不规范文件名数组
fileNameArr.push(fileName)
}
}
return fileNames;
}
(async function(){
async function searchAndReplace(scope,filename,errorName) {
let file = await fs.readdirSync(scope);
//遍历每一个文件
for(let i = 0; i < file.length; i++){
//构造文件的路径
var filePath = path.join(scope,file[i]);
let stats = fs.statSync(filePath);
//如果是文件就可以进一步读取文件的内容
if(stats.isFile()){
//读取文件内容
var fileContent = fs.readFileSync(filePath,'utf-8');
//设置匹配 /filename 形式的规则
var reg = eval(`/\\/${filename}\\b/g`);
//替换生成新内容
var newContent = fileContent.replace(reg,function(word){
index++;
//把驼峰的替换成中划线连接的
var result = word.replace(/[A-Z]/g,'-$&').toLowerCase().replace('/-','/');
console.log('修改的文件为:',filePath,'其中不规范文件名为:',word,'重命名为:',result," ",filename,"的修改次数为:",index)
//构造excel数据
temp.push([errorName,filePath,word,result,index])
return result;
})
//新内容写入文件
fs.writeFileSync(filePath,newContent)
}else{
//如果是文件夹继续遍历
await searchAndReplace(path.join(scope, file[i]),filename,errorName)
}
}
}
//生成excel函数,不需要可注掉。
async function exportExcel(){
app.get('/Excel', function(req, res){
var conf ={};
conf.name = "mysheet";
conf.cols = [{
caption:'不规则文件名路径',
type:'string'
},{
caption:'目标修改的文件路径',
type:'string'
},{
caption:'要修改的不规范字符串',
type:'string'
},{
caption:'修改完的字符串',
type:'string'
},{
caption:'不规则文件名修改的次数',
type:'number'
}];
conf.rows = temp;
var result = nodeExcel.execute(conf);
res.setHeader('Content-Type', 'application/vnd.openxmlformats;charset=utf-8');
res.setHeader("Content-Disposition", "attachment; filename=" + "Report.xlsx");
res.end(result, 'binary');
});
app.listen(3000);
console.log('Listening on port 3000');
}
//1.搜索不规范文件
const rootDirs = path.resolve(path.join(__dirname, `../${searchScope}`))
let filesList = getAllFiles(rootDirs);
//获取到不符合规范的文件名路径
errorName = getErrorFileName(filesList);
//2.进行文件内容和文件名修改
for(let i = 0; i < fileNameArr.length; i++){
//2.1搜索引入此不规范文件的文件,并对其内容进行修改
const scope = path.resolve(path.join(__dirname, `../${replaceScope}`))
await searchAndReplace(scope,fileNameArr[i],errorName[i]);
index = 0;
//2.1对此不符合规范的文件的文件名进行修改
//构建规范的文件名
var newfileNameArr = fileNameArr[i].replace(/[A-Z]/g,'-$&').toLowerCase().replace(/^-/,'');
//构建新的文件路径
var reg = eval(`/${fileNameArr[i]}\\./`);
var newFileName = errorName[i].replace(reg,newfileNameArr+'.')
//进行文件名修改
// fs.rename(errorName[i],newFileName,function(err){
// if(err){
// throw err;
// }
// })
execSync(`git mv ${errorName[i]} ${newFileName} `)
}
//最后导出excel表格
exportExcel();
})()
导出统计结果:
- 工程: 检测文件数量:260 不符合规范数量:127
- 通过使用excel-export模块,就可以把结果生成一个excel,在浏览器访问 http://localhost:3000/Excel ,就能下载生成的表格。
本人能力有限,写的脚本不是很精炼、完美,欢迎提出宝贵意见和问题。