zoukankan      html  css  js  c++  java
  • 使用npm创建一个命令行工具

    使用node创建一个命令行工具

    提纲
    1、概述
    2、通过例子创建命令行工具包
    3、命令行工具包的使用场景

    1、概述

    npm install 可以安装一个包到全局目录,也就是nodejs安装目录下的node_modules下,也可以安装一个包到当前项目的node_modules下。一般而言,用npm安装一个命令行工具的时候,建议将它安装到全局目录下,但是,如果想要安装一个项目中需要用到的库,则要安装在当前项目根目录下,这样方便当前项目js代码中require导入包,进而使用这个包中的类和函数。

    从上面可以看出,npm管理的包,粗略的可以分为两类:第一类是命令工具包,第二类是程序库包。这两类包可以用npm去安装,也可以用npm去创建,就是说,可以用npm从头创建一个命令行工具包,也可以用npm从头创建一个程序库包。

    本文主要是通过一个例子,来展示如何使用npm去创建一个命令行工具包。

    2、通过例子创建命令行工具包

    npm的命令行工具包实际上也是一个npm包,所以创建它的过程遵循创建一个普通npm包的基本流程。

    1、生成npmclitest包

    PS E:prjsweb
    pminittest> mkdir npmclitest
    PS E:prjsweb
    pminittest> cd npmclitest
    PS E:prjsweb
    pminittest
    pmclitest> npm init
    

    2、编写index.js文件

    然后,新建一个index.js文件

    // index.js
    console.log("hello, npmclitest!");
    

    一般而言,通过node去运行index.js,效果如下:

    PS E:prjsweb
    pminittest
    pmclitest> node index.js
    hello, npmclitest!
    PS E:prjsweb
    pminittest
    pmclitest>
    
    

    可以做得更逼真一点,我们在package.json里面的scripts字段上添加一下脚本名:

    {
     "scripts":{
      "hello":"node index.js"
     }
    }
    

    然后命令行调用:

    PS E:prjsweb
    pminittest
    pmclitest> npm run hello
    
    > npmclitest@1.0.0 hello E:prjsweb
    pminittest
    pmclitest
    > node index.js
    
    hello, npmclitest!
    PS E:prjsweb
    pminittest
    pmclitest> 
    
    

    但是,看到这里你肯定会说,人家webpack还有vue-cli都是“有名字”的!什么 vue-cli init app 、 webpack -p 的,多漂亮,看看这个命令行, node index.js ,还 npm run hello ,谁不会啊,怕又不是来水文章的哦?差评!!

    3、给命令工具包命名

    别急啊,各位大人,接下来就说说,如何给这个node脚本起个名字。

    在package.json里面,添加一个bin字段:

    {
      "name": "npmclitest",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo "Error: no test specified" && exit 1",
        "hello": "node index.js"
      },
      "bin": {
        "npmclitest": "index.js"
      },
      "author": "",
      "license": "ISC"
    }
    
    

    bin字段里面写上这个命令行的名字,也就是 npmclitest ,它告诉npm,里面的js脚本可以通过命令行的方式执行,以 npmclitest 的命令调用。当然命令行的名字,可以随便写。

    在当前package.json目录下,打开命令行工具,执行 npm link ,将当前的代码在npm全局目录下留个快捷方式。

    4、在全局目录下安装npmclitest命令

    PS E:prjsweb
    pminittest
    pmclitest> npm link
    npm notice created a lockfile as package-lock.json. You should commit this file.
    npm WARN npmclitest@1.0.0 No description
    npm WARN npmclitest@1.0.0 No repository field.
    
    up to date in 0.49s
    C:UsersCICVAppDataRoaming
    pm
    pmclitest -> C:UsersCICVAppDataRoaming
    pm
    ode_modules
    pmclitestindex.js
    C:UsersCICVAppDataRoaming
    pm
    ode_modules
    pmclitest -> E:prjsweb
    pminittest
    pmclitest
    PS E:prjsweb
    pminittest
    pmclitest> npmclitest
    hello, npmclitest!
    PS E:prjsweb
    pminittest
    pmclitest> 
    

    这样就可以在命令行下,像使用普通命令一样使用npmclitest命令了。

    这里有一个值得注意的点,在index.js头部必须有#!/usr/bin/env node,否则在npm link后,在windows下,npmclitest会无法执行。具体原理,以后再说。

    5、另外一种安装脚本的方法

    另外,如果你仅希望你的cli脚本仅在项目里执行,则需要在你项目里面新建一个目录,重复上述的操作,只是在第三步的时候,不要link到全局里面去,而是使用 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 的方式执行。

    这里也给我们提供了一个cli开发流程思路:

    初期开发可以通过node index.js来看效果。
    测试的时候可以通过npm link的方式进行安装测试。
    发布

    哦?这样子使用的话不就回到最最最开始的时候那种原始的 npm run hello 一样么。

    是的,但是有质的区别。
    使用 node index.js 这种方式调用的话固然简单灵活,但是严重依赖脚本路径,一旦目录结构发生变动,写在scripts的命令就要更改一次;但是使用npm安装之后,本地的cli脚本就被拉到node_modules里面,目录结构变动对其影响不大。
    其次,是不利于分享与发布,如果你想把你的cli脚本发布出去,那么有一个好听响亮的名字,比起在说明文档里面告诉使用者如何找到你的脚本路径再用node执行它,简直好上那么一万倍不是么?

    6、参数读取:process.argv

    名字有了,输出也有了,看看我们跟那些大名鼎鼎的cli工具,在形式上还差点啥?对了,人家可以支持不同参数选项的,还可以根据输入的不同,产生不同的结果。

    这样吧,我们给这个cli加一个功能,既然叫 hello-cli ,那不能只会 hello world 吧,必须要见谁就说 hello 才行:

    > npmclitest older
    ## 输出
    > hello older
    

    虽然这个功能很简单,但是至少也是实现了“根据输入的不同,产生不同结果”的效果。

    命令行上的参数,可以通过 process 这个变量获取, process 是一个全局对象而不是一个包,不需要通过 require 引入。通过 process 这个对象我们可以拿到当前脚本执行环境等一系列信息,其中就包括命令行的输入情况,这个信息,保存在 process.argv 这个属性里。我们可以打印一下:

    //index.js
    console.log(process.argv);
    

    打印结果:
    C:Program Files odejs ode.exe,C:UsersCICVAppDataRoaming pm ode_modules pmclitestindex.js,elder

    可以看出,argv是个数组,前两位是固定的,分别是node程序的路径和脚本存放的位置,从第三位开始才是额外输入的内容。那么实现上面的功能就很简单了,只要读取argv数组的第三位,然后输出出来就可以了。

    //index.js
    console.log(`hello ${process.argv[2]||'world'}`);
    

    npm社区中也有一些优秀的命令行参数解析包,比如yargs ,tj的commander.js 等等。

    如果你想使用比较复杂的参数或者命令,建议还是用第三方包比较好,手写解析太耗精力了。

    7、子进程

    现在,你可以自由自在的写你自己的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。

    8、美化输出

    如果你不那么希望你的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 方法输出一行看起来像是进度条的字符串到命令行上。

    所以在计时器里面,当计数小于10的时候,我们让光标移到第一行上,然后清除所有输出,输出进度条字符串;当计数大于10的时候,我们关掉计时器,清除输出,打印结果。

    最后不要忘记关掉进程,可以使用 interface 这个类的 .close 方法关掉readline进程,也可以直接调用 process.exit 退出。

    绘制的思路跟canvas绘制动画一样,只不过canvas是清除画布,而命令行这里是通过 readline.clearScreenDown 清除输出。

    这样,一个简易的,人性化的,带点进度条动画的命令行工具就写好了,你也可以发挥你的想象力,去写一些更有趣的效果出来。

    3、命令行工具包的使用场景

    前端日常开发中,会遇见各种各样的cli,比如一行命令帮你打包的webpack,一行命令帮你生成vue项目模板的vue-cli,还有创建react项目的create-react-app等等等等。这些工具极大地方便了我们的日常工作,让计算机自己去干繁琐的工作,而我们,就可以节省出大量的时间用于学习、交流、开发。

    但是有时候一些十分特别的需求,我们是找不到适合的cli工具去做的。比如说,你的项目十分庞大,你给项目添加一个新的路由,要经过 创建目录 -> 创建.vue文件 -> 更新vue-router的路由列表 这一趟流程,就算快捷键创建目录文件用得再熟悉,也比不过你一行命令来得快,特别是路由目录嵌套深,.vue文件初始化模板复杂的时候。

    所以呢,这时候,就可以自己项目写一个cli,用来专门做这些繁琐的活。

    参考资料:

    1、这是最关键的一篇,讲了如何在全局下安装命令行,如何在本地安装命令行。https://www.jb51.net/article/150196.htm
    2、这里有命令行工具发布到npm仓库的教程,https://www.jianshu.com/p/e213034ff85c

  • 相关阅读:
    一点一点学ASP.NET之基础概念——HttpModule
    Repeater为空时显示处理
    数据绑定控件的ItemDataBound事件与System.Data.Common.DbDataRecord
    Fckeditor编辑器添加【行距】
    gridview用法大全(二)
    Web.config自定义节点configSections
    C#中的特性(Attributes)(翻译)
    给程序员的忠告
    项目管理(转载)
    在ASP.NET 2.0中使用样式、主题和皮肤(转载)
  • 原文地址:https://www.cnblogs.com/zhangzl419/p/15223055.html
Copyright © 2011-2022 走看看