创建Nodejs命令行程序详解

704 阅读4分钟

本文主要是探究Nodejs程序全局安装的原理以及如何用Nodejs开发命令行程序

创建一个命令

我们正常编写js文件,然后在js文件的顶部加上这行注释#!/usr/bin/env node,例如我们创建一个文件hello.js文件,内容如下:

#!/usr/bin/env node

console.log('你好');

切换到hello.js文件所在的目录,执行./hello.js,会看到屏幕上打印了"你好",表明我们成功的创建了一个命令脚本

但是该方法还有一个欠缺,就是每次运行的时候还需要输入完整的路径,不够自动化,我们希望达到的效果是输入hello命令,直接在屏幕上看到打印结果

$ hello 
>> 你好

想要做到这个效果,我们需要在package.json文件中增加bin字段,注册一个命令(名称)和执行命令的脚本

{
    "bin": {
        "hello": "./bin/hello.js"
    }
} 

我们将这个包通过npm run publish发布,然后安装这个包,如果是全局安装,则执行hello之后就会看到打印结果,如果是安装在某个项目里,则需要用npx hello命令查看效果

接下来我们探究下原理,以非全局安装为例,执行完npm install之后,会自动生成一个文件夹 node_modules/.bin ,并在里面自动生成三个文件hellohello.cmdhello.ps1,这三个文件的功能是一样的,都是执行对应的命令脚本,如hello文件的内容是:

#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")

case `uname` in
    *CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;;
esac

if [ -x "$basedir/node" ]; then
  "$basedir/node"  "$basedir/../hello/bin/hello.js" "$@"
  ret=$?
else 
  node  "$basedir/../hello/bin/hello.js" "$@"
  ret=$?
fi
exit $ret

综上可知,如果我们想要创建一个命令,只需要两步:

  1. 在命令的执行脚本顶部加上#!/usr/bin/env node标记
  2. package.json文件里加上 bin 字段,注册命令名称以及对应的命令脚本路径

关于参数的获取

命令行程序除了命令的注册外,命令参数的获取也是一个重要的环节,基本上关于命令行参数有这么几个基本的需求:

  • 定义参数的数据类型
  • 定义参数的默认值
  • 获取用户配置的参数
  • 参数的使用方法(实现--help)
  • 参数别名(-n --name)
  • 实现显示当前版本号功能

手动实现上述功能,主要是依赖对process.argv的处理,上述的工作 Commander.js 已经帮我们做了实现,我们接下来介绍一下其用法

安装

npm install commander

配置version

const { program } = require('commander');
program.version('0.0.1');
const { Command } = require('commander');
const program = new Command();
program.version('0.0.1');

定义参数、获取参数

涉及到数据类型,必填项,默认值,别名,变长参数

  • 数据类型:有两种最常用的选项,一类是 boolean 型选项,选项无需配置参数,另一类选项则可以设置参数(使用尖括号声明在该选项后,如--expect )。如果在命令行中不指定具体的选项及参数,则会被定义为undefined

  • 必填:通过.requiredOption方法可以设置选项为必填。必填选项要么设有默认值,要么必须在命令行中输入,对应的属性字段在解析时必定会有赋值。该方法其余参数与.option一致

  • 别名:每个选项可以定义一个简写名称(-后面接单个字符)和一个长选项名称(--后面接一个或多个单词),使用逗号、空格或|分隔,如 option('-d, --debug', 'output extra debugging')

  • 变长参数:定义选项时,可以通过使用...来设置参数为可变长参数。在命令行中,用户可以输入多个参数,解析后会以数组形式存储在对应属性字段中,如option('-l, --letter [letters...]', 'specify letters')

完整示例:

#!/usr/bin/env node

const commander = require('commander');
const program = new commander.Command();

program.version('0.0.1');

program
    .option('-d, --debug', 'output extra debugging') // --debug 別名 -d
    .requiredOption('-h, --hot', 'hot must chose') // 必填项
    .option('-ad, --adress [adress...]', 'adress can mutl') // 必填项
    .option('-s, --small', 'small pizza size', true)
    .option('-p, --pizza-type <type>', 'flavour of pizza', 'okok');

program.parse(process.argv);

const options = program.opts();

console.log(options);


//
// $ ./cli-run.js -h -ad 123 32131 1321
// {
//     small: true,
//     pizzaType: 'okok',
//     hot: true,
//     adress: ['123', '32131', '1321']
// }

业界常见实现方式

通过上文可知,命令行的实际内容是个js文件,只不过在package.json里用bin字段做了指定,以便可以在命令行中调取这个命令。在很多知名库,如webpackESLint,它们除了会提供API调用方式外,还会提供命令行方式,基本上是在项目根目录添加一个bin文件夹,实现连接命令与API的脚本,向API传递从命令行获取到的参数。所以基本上就是命令行在API的基础上做了一层获取参数的封装而已。

附录

参考文章

必备开发包