Cli
nodejs 编写简单 cli
Command Line Interface,顾名思义是一种通过命令行来交互的工具或者说应用。SPA应用中常用的如vue-cli, angular-cli, node.js开发搭建express-generator,还有我们最常用的webpack,npm等。他们是web开发者的辅助工具,旨在减少低级重复劳动,专注业务提高开发效率,规范develop workflow。
CLI的根据不同业务场景有不同的功能,但万变不离其宗,本质都是通过命令行交互的方式在本地电脑运行代码,执行一些任务。
CLI有什么好处?
我们可以从工作中总结繁杂、有规律可循、或者简单重复劳动的工作用CLI来完成,只需一些命令,快速完成简单基础劳动。以下是我对现有工作中的可以用CLI工具实现的总结举例:
- 快速生成应用模板,如vue-cli等根据与开发者的一些交互式问答生成应用框架
- 创建module模板文件,如angular-cli,创建component,module;sequelize-cli 创建与mysql表映射的model等
- 服务启动,如ng serve
- eslint,代码校验,如vue,angular,基本都具备此功能
- 自动化测试 如vue,angular,基本都具备此功能
- 编译build,如vue,angular,基本都具备此功能
- *编译分析,利用webpack插件进行分析
- *git操作
- *生成的代码上传CDN
- *还可以是小工具用途的功能,如http请求api、图片压缩、生成雪碧图等等,只要你想做的都能做
总体而言就是一些快捷的操作替代人工重复劳动,提升开发效率。
与npm scripts的对比
npm scripts也可以实现开发工作流,通过在package.json 中的scripts对象上配置相关npm 命令,执行相关js来达到相同的目的;
但是cli工具与npm scripts相比有什么优势呢?
npm scripts是某个具体项目的,只能在该项目内使用,cli可以是全局安装的,多个项目使用;
使用npm scripts 在业务工程里面嵌入工作流,耦合太高;使用cli 可以让业务代码工作流相关代码剥离,业务代码专注业务
cli工具可以不断迭代开发,演进,沉淀。
下面就是nodejs 实现一个简单cli
hello world
nodejs的cli,本质就是跑node脚本,大家都会:1
2// index.js
console.log('hello xueqiu')
然后命令行调用
node index.js
输出:
hello world
可以做得更逼真一点,我们在package.json里面的scripts字段上添加一下脚本名:1
2
3
4
5{
"scripts":{
"hello":"node index.js"
}
}
然后命令行调用:
npm run hello
接下来就说说,如何给这个node脚本起个名字。
起名字
姑且,先把这个cli的名字命名为dw-cli,就是我们能够在命令行里面,输入dw-cli,然后它就打印一句hello xueqiu,没有node也没有npm,就是:
这里,我们需要做几步操作:
index.js文件顶部声明执行环境:
1 |
|
添加 #!/usr/bin/env node
或者 #!/usr/bin/node
,这是告诉系统,下面这个脚本,使用nodejs来执行。当然,这个系统不包括windows,因为windows下有个JScript的历史遗留物在,会让你的脚本跑不起来。
#!/usr/bin/env node的意思是让系统自己去找node的执行程序。
#!/usr/bin/node的意思是,明确告诉系统,node的执行程序在路径为/usr/bin/node。
添加package.json的bin字段。
可以在index.js当前的目录下执行npm init创建一个package.json,然后在package.json里面,添加一个bin字段:
1 | { |
bin字段里面写上这个命令行的名字,也就是dw-cli,它告诉npm,里面的js脚本可以通过命令行的方式执行,以dw-cli的命令调用。当然命令行的名字想写什么都可以:
在当前package.json目录下,打开命令行工具,执行npm link,将当前的代码在npm全局目录下留个快捷方式。
npm检测到package.json里面存在一个bin字段,它就同时在全局npm包目录下生成了一个可执行文件:
npm root -g 这个命令可以看到npm的全局位置
当我们在系统命令行直接执行dw-cli的时候,实际上就是执行这里的脚本。
因为安装node的时候,npm将这个目录配置为系统变量环境了,当你执行命令的时候,系统会先找系统命令和系统变量,然后到变量环境里面去查找这个命令名,然后找到这个目录后,发现匹配上了该命令名的可执行文件,接着就直接执行它。vue-cli也好,webpack-cli也好,都是这样执行的。
这样,你的第一个cli脚本就成功安装了,可以在命令行里面,直接敲你的cli名字,看看结果输出吧。
另外,如果你仅希望你的cli脚本仅在项目里执行,则需要在你项目里面新建一个目录,重复上述的操作,只是在第三步的时候,不要llink到全局里面去,而是使用npm i -D file:<你的脚本cli目录路径>,把它当成项目的依赖安装到node_modules里面去,如果安装成功,那么在项目的package.json你会看到多了一条依赖,这条依赖的值不是版本号,而是你脚本的路径。然后在node_modules里面会有一个.bin目录,里面就存放着你的可执行文件。
参数读取:process.argv
名字有了,输出也有了,看看我们跟那些大名鼎鼎的cli工具,在形式上还差点啥?对了,人家可以支持不同参数选项的,还可以根据输入的不同,产生不同的结果。
这样吧,我们给这个cli加一个功能,既然叫dw-cli,那不能只会hello world吧,必须要见谁就说hello才行:
dw-cli older
输出
hello older
虽然这个功能很简单,但是至少也是实现了“根据输入的不同,产生不同结果”的效果。
命令行上的参数,可以通过process这个变量获取,process是一个全局对象而不是一个包,不需要通过require引入。通过process这个对象我们可以拿到当前脚本执行环境等一系列信息,其中就包括命令行的输入情况,这个信息,保存在process.argv这个属性里。我们可以打印一下:
1 | console.log(process.argv); |
打印结果:1
hello [ '/usr/local/bin/node', '/usr/local/bin/dw-cli', 'xq']
可以看出,argv是个数组,前两位是固定的,分别是node程序的路径和脚本存放的位置,从第三位开始才是额外输入的内容。那么实现上面的功能就很简单了,只要读取argv数组的第三位,然后输出出来就可以了。
1 | //index.js |
npm社区中也有一些优秀的命令行参数解析包,比如commander.js等等
1 | const program = require('commander'); |
如果你想使用比较复杂的参数或者命令,建议还是用第三方包比较好,手写解析太耗精力了。
子进程
现在,你可以自由自在的写你自己的cli脚本了。
如果想使用phantom你需要通过node的child_process模块开启子进程,在子进程内调用命令:
1 | const { exec } = require('child_process') |
包括系统命令、其他cli命令都可以在这里执行。特别是系统命令。社区上也有一些不错的包,比如shelljs
美化输出
希望更人性化一点,比如提供一些友好的输入、提示啊,给你的输出加点颜色区分重点啊,写个简单的进度条啊等等,那么就需要美化一下你的输出了。
除了颜色这部分,不使用第三方包实现起来非常繁琐复杂,其他的功能,都可以试试自己写。
颜色部分使用第三方包colors。
其他都是由nodejs自带的readline模块实现的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
console.log('hello xq')
console.log('hello ', process.argv)
const readline = require('readline')
const unloadChar = '-'
const loadedChar = '='
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
rl.question('什么命令? ', answer => {
let i = 0
let time = setInterval(() => {
if (i > 10) {
clearInterval(time)
readline.cursorTo(process.stdout, 0, 2)
// readline.clearScreenDown(process.stdout)
console.log(`hello ${answer}`)
process.exit(0)
return
}
readline.cursorTo(process.stdout, 0, 1)
readline.clearScreenDown(process.stdout)
renderProgress('saying hello', i)
i++
}, 200)
})
function renderProgress(text, step) {
const PERCENT = Math.round(step * 10)
const COUNT = 2
const unloadStr = new Array(COUNT * (10 - step)).fill(unloadChar).join('')
const loadedStr = new Array(COUNT * step).fill(loadedChar).join('')
process.stdout.write(`${text}:【${loadedStr}${unloadStr}|${PERCENT}%】`)
}
首先,通过readline.createInterface方法创建一个interface类,这个类下面有一个方法.question,用这个方法在命令行上抛出一个问题,在第二个参数传入一个函数进行监听。一旦用户输入完毕敲下回车,就会触发回调函数。
然后我们在回调函数里面写了个计时器,假装我们在处理某些事务。
使用readline.cursorTo这个方法,可以改变命令行上的光标的位置。
readline.cursorTo(process.stdout, 0, 0);是移动到第1列第1行上
readline.cursorTo(process.stdout, 0, 1);是移动到第1列第2行上
使用readline.clearScreenDown这个方法,是让命令行从当前行开始,到最后一行结束,将这两行之间所有内容清除。
renderProgress是自己封装的一个方法,通过process.stdout.write方法输出一行看起来像是进度条的字符串到命令行上。
所以在计时器里面,当计数小于10的时候,我们让光标移到第一行上,然后清除所有输出,输出进度条字符串;当计数大于10的时候,我们关掉计时器,清除输出,打印结果。
最后不要忘记关掉进程,可以使用interface这个类的.close方法关掉readline进程,也可以直接调用process.exit退出。
绘制的思路跟canvas绘制动画一样,只不过canvas是清除画布,而命令行这里是通过readline.clearScreenDown清除输出。
扩展
实现功能为:
- 输入 new 命令从github下载一个脚手架模版,然后创建对应的app。
- 输入 create 命令可以快速的创建一些样板文件。