前端平常开发中,会碰见各类各样的cli,好比一行命令帮你打包的webpack,一行命令帮你生成vue项目模板的vue-cli,还有建立react项目的create-react-app等等等等。这些工具极大地方便了咱们的平常工做,让计算机本身去干繁琐的工做,而咱们,就能够节省出大量的时间用于学习、交流、开发、逛steam。javascript
可是有时候一些十分特别的需求,咱们是找不到适合的cli工具去作的。好比说,你的项目十分庞大,你给项目添加一个新的路由,要通过建立目录 -> 建立.vue文件 -> 更新vue-router的路由列表
这一趟流程,就算快捷键建立目录文件用得再熟悉,也比不过你一行命令来得快,特别是路由目录嵌套深,.vue文件初始化模板复杂的时候。html
因此呢,何不为本身项目写一个cli?就专门作这些繁琐的活?前端
nodejs的cli,本质就是跑node脚本嘛,基本上每位前端er都会:vue
// index.js console.log('hello world') 复制代码
而后命令行调用java
> node index.js ## 输出: > hello world 复制代码
能够作得更逼真一点,咱们在package.json里面的scripts字段上添加一下脚本名:node
{ "scripts":{ "hello":"node index.js" } } 复制代码
而后命令行调用:react
> npm run hello 复制代码
可是,看到这里你确定会说,人家webpack还有vue-cli都是“有名字”的!什么vue-cli init app
、webpack -p
的,多漂亮,看看这个命令行,node index.js
,还npm run hello
,谁不会啊,丑不拉几的,怕又不是来水文章的哦?差评!!webpack
别急啊各位大人,接下来就说说,如何给这个node脚本起个名字。git
姑且,先把这个cli的名字命名为hello-cli
,就是咱们可以在命令行里面,输入hello-cli
,而后它就打印一句hello world
,没有node
也没有npm
,就是: github
// index.js #!/usr/bin/env node console.log('hello world') 复制代码添加
#!/usr/bin/env node
或者#!/usr/bin/node
,这是告诉系统,下面这个脚本,使用nodejs来执行。固然,这个系统不包括windows,由于windows下有个JScript的历史遗留物在,会让你的脚本跑不起来。#!/usr/bin/env node
的意思是让系统本身去找node的执行程序。#!/usr/bin/node
的意思是,明确告诉系统,node的执行程序在路径为/usr/bin/node
。npm init
建立一个package.json,而后在package.json里面,添加一个bin字段:{ "name": "hello-test", "version": "1.0.0", "bin":{ "hello-cli":"index.js" } } 复制代码bin字段里面写上这个命令行的名字,也就是
hello-cli
,它告诉npm,里面的js脚本能够经过命令行的方式执行,以hello-cli
的命令调用。固然命令行的名字你想写什么都是你的自由,好比:
npm link
,将当前的代码在npm全局目录下留个快捷方式。hello-cli
的时候,实际上就是执行这里的脚本。这样,你的第一个cli脚本就成功安装了,能够在命令行里面,直接敲你的cli名字,看看结果输出吧。
另外,若是你仅但愿你的cli脚本仅在项目里执行,则须要在你项目里面新建一个目录,重复上述的操做,只是在第三步的时候,不要llink到全局里面去,而是使用npm i -D file:<你的脚本cli目录路径>
,把它当成项目的依赖安装到node_modules里面去,若是安装成功,那么在项目的package.json你会看到多了一条依赖,这条依赖的值不是版本号,而是你脚本的路径。而后在node_modules里面会有一个.bin目录,里面就存放着你的可执行文件。
局部安装建议用
npm i -D file:xxx
,这样它会在package.json留条记录,方便其余小伙伴看到。天然,你的脚本最好也是放进项目目录里面。
固然,这样安装的cli脚本,必须在项目的package.json的scripts字段上声明脚本命令,而后经过npm run
的方式执行。
哦?这样子使用的话不就回到最最最开始的时候那种原始的npm run hello
同样么。
是的,可是有质的区别。使用node index.js
这种方式调用的话当然简单灵活,可是严重依赖脚本路径,一旦目录结构发生变更,写在scripts的命令就要更改一次;可是使用npm安装以后,本地的cli脚本就被拉到node_modules里面,目录结构变更对其影响不大。其次是不利于分享与发布,若是你想把你的cli脚本发布出去,那么有一个好听响亮的名字,比起在说明文档里面告诉使用者如何找到你的脚本路径再用node执行它,简直好上那么一万倍不是么?
这里也给咱们提供了一个cli开发流程思路:
node index.js
来看效果。npm link
的方式进行安装测试。名字有了,输出也有了,看看咱们跟那些大名鼎鼎的cli工具,在形式上还差点啥?对了,人家能够支持不一样参数选项的,还能够根据输入的不一样,产生不一样的结果。
这样吧,咱们给这个cli加一个功能,既然叫hello-cli
,那不能只会hello world
吧,必需要见谁就说hello
才行:
> hello-cli older ## 输出 > hello older 复制代码
虽然这个功能很简单,可是至少也是实现了“根据输入的不一样,产生不一样结果”的效果。
命令行上的参数,能够经过process
这个变量获取,process
是一个全局对象而不是一个包,不须要经过require
引入。经过process
这个对象咱们能够拿到当前脚本执行环境等一系列信息,其中就包括命令行的输入状况,这个信息,保存在process.argv
这个属性里。咱们能够打印一下:
//index.js console.log(process.argv); 复制代码
打印结果:
//index.js console.log(`hello ${process.argv[2]||'world'}`) 复制代码
npm社区中也有一些优秀的命令行参数解析包,好比yargs,tj的commander.js等等
若是你想使用比较复杂的参数或者命令,建议仍是用第三方包比较好,手写解析太耗精力了。
如今,你能够自由自在的写你本身的cli脚本了。
若是你但愿写一个项目打完包自动推上git的cli,或者自动从git仓库里面拉取项目启动模板,那么,你须要经过node的child_process
模块开启子进程,在子进程内调用git命令:
//test.js const child_process = require('child_process'); let subProcess=child_process.exec("git version",function(err,stdout){ if(err)console.log(err); console.log(stdout); subProcess.kill() }); 复制代码
不只是git命令,包括系统命令、其余cli命令均可以在这里执行。特别是系统命令,使用系统命令对文件目录进行操做,效率比fs高到不知道哪里去了。
社区上也有一些不错的包,好比阮一峰老师推荐的shelljs
若是你不那么但愿你的cli用起来那么“硬核”,但愿更人性化一点,好比提供一些友好的输入、提示啊,给你的输出加点颜色区分重点啊,写个简单的进度条啊等等,那么你就须要美化一下你的输出了。
除了颜色这部分,不使用第三方包实现起来很是繁琐复杂,其余的功能,均可以试试本身写。
颜色部分使用了第三方包colors,这里就不演示了。
其余都是由nodejs自带的readline模块实现的。
//index.js const readline = require('readline'); const unloadChar='-'; const loadedChar='='; const rl=readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question('你想对谁说声hello? ',answer=>{ let i = 0; let time = setInterval(()=>{ if(i>10){ clearInterval(time); readline.cursorTo(process.stdout, 0, 0); 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
方法输出一行看起来像是进度条的字符串到命令行上。interface
这个类的.close
方法关掉readline进程,也能够直接调用process.exit
退出。绘制的思路跟canvas绘制动画同样,只不过canvas是清除画布,而命令行这里是经过readline.clearScreenDown
清除输出。
这样,一个简易的,人性化的,带点点进度条动画的命令行cli工具就写好了,你也能够发挥你的想象力,去写一些更有趣的效果出来。
毕竟咱们前端,有浏览器咱们能够写动画,没了浏览器咱们同样能够写动画。