阅读 10296

用一次就会爱上的cli工具开发

写在前面

最近接手任务——使用nodejs开发一个公司内部使用的cli工具,简而言之就是输入一行命令快速搭建好项目结构,也可以通过不同的命令引入不同的文件。

了解

首先要基于node环境,然后我们需要知道cli是什么?cli是command-line interface的缩写,即命令行工具,常用的vue-clicreate-react-appexpress-generator 等都是cli工具。

回顾

创建一个exercise-cli目录,并使用cmd进入该目录:

mkdir exercise-cli && cd exercise-cli
复制代码

在该目录下新建index.js:

//index.js
console.log('谢邀,人在美国,刚下飞机。');
复制代码

使用node运行index.js:

这是node的基本用法,那么如何使用自定义命令行输出这句话呢?

点火

使用npm init创建package.json,一路回车,当然你也可以配置相关信息,有兴趣可自己选择:

现在目录中自动生成一个package.json文件:

{
  "name": "exercise-cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
复制代码

现在在package.json中添加字段bin,用来存放一个可执行的文件,我们此处的可执行文件就是index.js,因此配置如下:

"bin":{
    "exercise-cli":"./index.js"
},
复制代码

此时我们配置exercise-cli命令来执行index.js文件,需要在index.js文件头部添加#!/usr/bin/env node, 让系统自己去找node的执行程序。至于这玩意具体什么,百度出这么个东西,可自行参考。

//index.js
#!/usr/bin/env node
console.log('谢邀,人在美国,刚下飞机。');
复制代码

然后在cmd输入npn linknpm install -g将当前项目安装到全局环境,这样就可以直接使用exercise-cli来运行文件了:

再学一点,在package.json的scripts字段里添加脚本名:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "exercise":"exercise-cli"
 }
复制代码

命令行输入npm run exercise,同样输出了index.js里的内容,联想起vue-clinpm run devnpm run build等会不会若有所思呢?

起飞

有点样子了,接下来让我们看看它是如何生成项目模板的,一个思路是用一个templates文件夹保存项目模板,然后通过fs.mkdir()来创建项目目录,最后把文件从templates文件夹拷贝到项目中去。本地templates目录如下图所示:

1.拷贝文件

模拟场景:将本地templates中的vue.min.js拷贝到新生成的项目模板中。在新生成的项目模板中新建public目录,该目录下新建js目录,将vue.min.js通过copyTemplate()方法从templates里拷贝到新建的js目录下面:

//index.js
#!/usr/bin/env node

var fs = require('fs');
var path = require('path');

// 复制文件
function copyTemplate (from, to) {
  from = path.join(__dirname, 'templates', from);
  console.log(from);
  write(to, fs.readFileSync(from, 'utf-8'))
}
function write (path, str, mode) {
  fs.writeFileSync(path, str)
}
// 新建目录
function mkdir (path, fn) {
  fs.mkdir(path, function (err) {
    fn && fn()
  })
}

var PATH = ".";
mkdir(PATH+'/public',function(){
	mkdir(PATH + '/public/js',function () {
		copyTemplate("/js/vue.min.js", PATH + '/public/js/vue.min.js');
	})
})

复制代码

用cmd打开任意文件夹输入exercise-cli,该文件夹下会public\js\vue.min.js:

2.拷贝文件夹

我们学会了拷贝文件,那么如何拷贝整个文件夹呢,例如我想将templates下的整个js目录全部拷贝到新生成的项目模板中,又该如何?有需求就有方案,我们可以遍历整个文件夹,对遍历到的path进行判断,如果是文件则直接拷贝,如果是文件夹则递归:

//index.js
// 复制目录
var copy=function(src,dst){
    let paths = fs.readdirSync(src); //同步读取当前目录(只能读取绝对路径,相对路径无法获取)
    paths.forEach(function(path){
        var _src=src+'/'+path;
        var _dst=dst+'/'+path;
        fs.stat(_src,function(err,stats){  //stats  该对象 包含文件属性
            if(err)throw err;
            if(stats.isFile()){ //如果是个文件则拷贝 
                let  readable=fs.createReadStream(_src);//创建读取流
                let  writable=fs.createWriteStream(_dst);//创建写入流
                readable.pipe(writable);
            }else if(stats.isDirectory()){ //是目录则 递归 
                checkDirectory(_src,_dst,copy);
            }
        });
    });
}
var checkDirectory=function(src,dst,callback){
    fs.access(dst, fs.constants.F_OK, (err) => {
        if(err){
            fs.mkdirSync(dst);
            callback(src,dst);
        }else{
            callback(src,dst);
        }
      });
};

mkdir(PATH+'/public',function(){
	mkdir(PATH + '/public/js',function () {
		checkDirectory('C:/Users/Administrator/Desktop/vue-3.0/nodeTest/exercise/templates/js',PATH+'/public/js',copy);
	})
})
复制代码

依然在找一个文件夹打开cmd输入exercise-cli,该文件夹下会生成public目录,该目录下面会生成templates下的整个js文件:

3.接收命令行参数

平常我们使用命令行工具时都会用到参数,如webpack -p, express -e 等,在此我们为exercise-cli配置-l,当使用exercise-cli -l时,添加layerJS。

我们可以使用process.argv来获取命令行参数,process.argv是一个参数数组,第一项为node.exe的绝对路径,第二项为执行该js的绝对路径,使用process.argv.slice(2)即可获取输入的参数数组。

//index.js
console.log(process.argv);
复制代码

通过遍历参数数组来检查命令中输入了哪些参数。如果输入了预设的参数,就为config对象添加对应的属性,在生成文件时根据config判断是否将模板文件拷贝到项目中。

var config = {};
process.argv.slice(2).forEach(item=>{
	if(item=="-l"){
		config.layer = true;
	}
})
var PATH = ".";
mkdir(PATH+'/public',function(){
	mkdir(PATH + '/public/js',function () {
		// copyTemplate("/js/vue.min.js", PATH + '/public/js/vue.min.js');
		checkDirectory('C:/Users/Administrator/Desktop/vue-3.0/nodeTest/exercise/templates/js',PATH+'/public/js',copy);
		if(config.layer){
			checkDirectory('C:/Users/Administrator/Desktop/exercise-cli/templates/layer',PATH+'/public/js',copy);
			//此处注意layerJS存放在templates中的路径。
		}
	})
})
console.log('拷贝成功');
复制代码

在任意文件夹打开cmd输入exercise-cli -l,执行成功,js目录中多出了layerJS目录:

加速

初始commander.js

其实node中有一款工具包可以快速开发命令行工具,它就是commander.js

首先全局安装一下:

npm install commander -g
复制代码

看个例子:

var program = require('commander');
program
    .version('1.0.0','-v, --version')
    .command('check [checkname]')
    .alias('c')
    .description('yo yo check now')
    .option('-a, --name [moduleName]', '模块名称')
    .action((checkname,option) => {
        console.log('指令 install 后面跟的参数值 checkname: ' + checkname);
        console.log(option);
        // 获得了参数,可以在这里做响应的业务处理
    })
    	//自定义帮助信息
    .on('--help', function() {
        console.log('  下面我随便说两句:')
        console.log('')
        console.log('$ 人有多大胆,母猪多大产,i love xx')
        console.log('$ 广阔天地,大有所为,呱~')
    })
program.parse(process.argv)
复制代码

命令行执行:

看完输出一脸懵逼,别急,这就带您瞧瞧这都是些什么东西:

  • version - 定义命令程序的版本号,.version('0.0.1', '-v, --version'),第一个参数版本号必须,第二个参数可省略,默认为 -V 和 --version
  • command – 定义命令行指令,后面可跟上一个name,用空格隔开,如 .command('app [name]')
  • alias – 定义一个更短的命令行指令 ,如执行命令$ exercise-cli c 与之是等价的
  • description – 描述,它会在help里面展示
  • option – 定义参数。它接受四个参数,在第一个参数中,它可输入短名字 -a和长名字–name ,使用 | 或者,分隔,在命令行里使用时,这两个是等价的,区别是后者可以在程序里通过回调获取到;第二个为描述, 会在 help 信息里展示出来;第三个参数为回调函数,他接收的参数为一个string,有时候我们需要一个命令行创建多个模块,就需要一个回调来处理;第四个参数为默认值
  • action – 注册一个callback函数,这里需注意目前回调不支持let声明变量
  • parse – 用于解析process.argv,设置options以及触发commands,用法示例:.parse(process.argv)

看到这,多多少少对如何编写命令行工具有个大体的认知了,光说不练嘴把式,自我实践:用commander.js完成上个段落3.接收命令行参数中的例子。


分割线(以下深入和浅出部分于2019.4.30 更)

深入inquirer.js

创建脚手架的时候我们会发现很多脚手架都需要我们和命令行频繁交互,就像我们开始使用npm init的时候一样,那么是如何实现和命令行交互的呢?此时inquirer.js闪亮登场。

//命令行安装
npm install inquirer
//index.js引入
var inquirer = require('inquirer');
复制代码
  1. 基本语法
var inquirer = require('inquirer');
inquirer.prompt([/* Pass your questions in here */]).then(function (answers) {
    // Use user feedback for... whatever!! 
})
复制代码
  1. 参数详解
  • type:表示提问的类型,包括:input, confirm, list, rawlist, expand, checkbox, password, editor;
  • name: 存储当前问题回答的变量;
  • message:问题的描述;
  • default:默认值;
  • choices:列表选项,在某些type下可用,并且包含一个分隔符(separator);
  • validate:对用户的回答进行校验;
  • filter:对用户的回答进行过滤处理,返回处理后的值;
  • transformer:对用户回答的显示效果进行处理(如:修改回答的字体或背景颜色),但不会影响最终的答案的内容;
  • when:根据前面问题的回答,判断当前问题是否需要被回答;
  • pageSize:修改某些type类型下的渲染行数;
  • prefix:修改message默认前缀;
  • suffix:修改message默认后缀。
  1. 实例分析

.action的回调函数里输入以下内容:

// 获得了参数,可以在这里做响应的业务处理
var prompList = [
	{
		type:'input',
		message:'姓名',
		name:'name'
	},{
		type:'input',
		message:'手机号',
		name:'phone',
		validate:val=>{
			if(val.match(/\d{11}/g)){
				return true
			}
			return '请输入11位数字'
		}
	},{
		type:'confirm',
		message:'是否参加本次考核?',
		name:'assess',
		prefix:'前缀'
	},{
		type:'confirm',
		message:'是否同意本次考核须知?',
		name:'notice',
		suffix:'后缀',
		when:answers=>{
			return answers.assess
		}
	},{
		type:'list',
		message:'欢迎来到本次考核,请选择学历:',
		name:'eductionBg',
		choices:[
			"大专",
			"本科",
			"本科以上"
		],
		filter:val=>{//将选择的内容后面加学历
			return val+'学历'
		}
	},{
		type:'rawlist',
		message:'请选择你爱玩的游戏:',
		name:'game',
		choices:[
			"LOL",
			"DOTA",
			"PUBG"
		]
	},{
		type:'expand',
			message:'请选择你喜欢的水果:',
			name:'fruit',
			choices: [
			{
				key: "a",
				name: "Apple",
				value: "apple"
			},
			{
				key: "O",
				name: "Orange",
				value: "orange"
			},
			{
				key: "p",
				name: "Pear",
				value: "pear"
			}
		]
	},{
		type:'checkbox',
		message:'请选择你喜欢的颜色:',
		name:'color',
		choices:[
			{
				name: "red"
			},
			new inquirer.Separator(), // 添加分隔符
			{
				name: "blur",
				checked: true // 默认选中
			},
			{
				name: "green"
			},
			new inquirer.Separator("--- 分隔符 ---"), // 自定义分隔符
			{
				name: "yellow"
			}
		]
	},{
		type:'password',
		message:'请输入你的游戏密码:',
		name:'pwd'
	}
	
]
inquirer.prompt(prompList).then(answers=>{
	console.log(answers);
})
复制代码

命令行交互如下:

浅出chalk.js

最后我们引入chalk这个美化命令行的模块,它具有轻量级、高性能、学习成本低等特点。继续在以上例子中引入chalk进行输出:

//命令行安装
npm install chalk
//index.js引入
var chalk = require('chalk');
复制代码

在inquirer里打印如下:

inquirer.prompt(prompList).then(answers=>{
	console.log(answers);
	console.log(chalk.green('考核完成'))//字体绿色
	console.log(chalk.blue('你最棒了'))//字体蓝色
	console.log(chalk.blue.bgRed('五一放假喽')) //支持设置背景
	console.log(chalk.blue(answers))
})
复制代码

命令行最终显示如下:

感兴趣的话还是自己敲一下吧。

着陆

想让别人来安装你的cli工具,你需要把它发布到npm上,先在npm官网创个账号(注意需要邮件验证),在命令行输入npm adduser,依次填上你注册的username、password、email。接着输入npm publish即可:

输入npm install -g exercise-clinpm install exercise-cli安装一下你的cli感受它的魅力吧。

代码已上传至我的GitHub,欢迎Fork。

感谢

关注下面的标签,发现更多相似文章
评论