如今基于 node.js 的 cli 工具层出不穷,尤其以前端脚手架工具更迭最为频繁,纯构建工具有 grunt、gulp、webpack、parcel 和 rullup 等,各前端框架也有各自的 cli 工具 create-react-app、vue-cli 和 angular-cli,各大厂的集成化解决方案 fis、jdf、umi 等。但是每个团队的应用场景不同,以上工具不一定完全满足需求,是时候自己动手开发一个定制化的 cli 工具了。
主要内容
cli是如何执行命令的- 创建
npm包 - 创建可执行文件
- 关于命令行参数
- 运行环境检查
- 更新检查
- 测试和发布
要开发 cli 工具首先要知道 cli 是什么,以及如何在 cli 里执行自己开发的命令行工具。
cli 是如何执行命令的
命令行界面(英文名称:Command Line Interface),是一种通过命令行来交互的工具或者应用。不同的操作系统内置了不同的 cli:
- unix-like
- bash
- sh
- csh
- zsh
- …
- windows
- cmd.exe
- PowerShell
通常情况下,一个命令对应了系统中的一个可执行文件,比如:cd、ls。当用户在 cli 中输入要执行的命令时,cli 会去预先配置好的查找路径中去查找同名的可执行文件并运行它。查找路径通常存在一个叫 PATH 的环境变量里。
比如 osx 系统可以通过命令 $PATH 知道其内容:
1 | |
接下来看看如何创建这个可执行文件。
创建 npm 包
首先通过 npm init 命令来创建一个 npm 包:
1 | |
创建可执行文件
- 在
cli-test根目录新建一个文件bin/index.js
1 | |
注意第一行的 #!/usr/bin/env node,#! 表示要指定脚本文件的解析程序,/usr/bin/env 表示要去哪里找解析程序,node 是解析程序的名字(表示这个要文件由 node.js 来运行)。
- 并且在
package.json中添加配置项:
1 | |
指定在 cli 中的名称为 cli-test,可执行文件为 bin/index.js。当用户通过 npm install 安装这个模块的时候,npm 会根据不同的操作系统自动创建一个可执行文件,比如在 osx 中就会创建 /usr/local/bin/cli-test 文件并关联到该可执行文件。
- 为
bin/index.js添加可执行权限(unix-like 系统):
1 | |
通过上面的步骤,一个 cli 命令行工具就创建完成了,接下来就可以编写工具的具体执行逻辑了,bin/index.js 也是一个 node.js 模块,可以引用其它 node.js 模块而且不需要添加执行权限:
1 | |
关于命令行参数
通常命令行工具都会有很多功能,每个功能通过不同的命令来调用,并且每个功能还会接收不同的参数,比如常用的 vue-cli,在终端输入 vue 并回车就可以看到 vue-cli 所支持的所有命令及其可选参数:
1 | |
process.argv
在 node.js 中可以使用 process.argv 数组来获取命令行输入的参数:
1 | |
1 | |
但是 process.argv 得到的内容不好区分命令、参数以及参数的值,所以这里需要引入一个第三方模块 yargs-parser。
1 | |
帮助信息
每个命令行工具都应该有一个帮助信息展示功能,例如上文中输入 vue 不加任何命令和参数(也可以添加参数 -h 或者 --help),输出的内容就是 vue-cli 的帮助信息,里面包含了支持的所有 命令 及 可选参数。
1 | |
关于路径
在开发命令行工具时,需要关注两个路径信息:
- 可执行脚本文件的路径
- node.js 进程的当前工作路径
可执行脚本文件的路径 即当前脚本文件路径,比如 bin/index.js 的路径,通过 __dirname 可以获得该路径的值。
1 | |
1 | |
通过这个路径可以读取或写入在本命令行工具安装路径里的文件,比如配置信息,缓存等。
node.js 当前工作路径 即用户在终端执行命令行工具的命令时所处的路径,通过 process.cwd() 可以获得该路径的值。
1 | |
1 | |
通过这个路径可以知道用户想要在什么位置使用该命令行工具,并且可以读取用户自定义的配置文件,比如 vue 的配置文件 vue.config.js。
commander.js
如果觉得以上处理 命令 和 参数 的方式很繁琐,又想快速上手制作出一个命令行工具,可以使用第三方模块 commander.js。
commander.js 是一个完整的 node.js 命令行解决方案,灵感来自 Ruby 的 commander。它有很详细的说明文档,并且有官方中文版,这里就不做过多介绍了。
用户行为交互
为了给用户提供更加友好的使用体验,通常会增加一些交互行为反馈功能,比如任务执行等待提示、输出结果高亮提示和用户输入提示等。
任务执行等待提示
ora 是一个不错的等待提示工具,也很容易上手:
1 | |
输出结果高亮
chalk 可以在命令行终端里定义字符串样式的模块,通过这个模块可以在终端输出各种样式的文本信息:
1 | |
用户输入交互
inquirer 模块集成了常用的命令行工具的用户交互功能,比如:input, number, confirm, list, rawlist, expand, checkbox, password, editor 等。
- 文本输入
1 | |
- 单项选择
1 | |
更多功能请参考官方文档: https://github.com/SBoudrias/Inquirer.js#documentation
运行环境检查
有些命令行工具需要安装第三方工具或者在特殊的环境(比如:node.js 的版本在 v10 及以上)才能运行,所以在运行任何程序之前,需要检查用户当前的运行环境是否能够正常使用本工具。
node.js 版本检查
npm 提供了在 package.json 文件中配置 engines 来声明 node.js 的版本,也可以声明 npm 的版本:
1 | |
当用户在安装本命令行工具时,npm 检查当前的 node.js 版本,如果不满足要求则会警告提示,如果带上 engine-strict 参数则会直接报错。
当然,开发者也可以在运行命令行工具时用程序去检查,这里借助第三方模块 node-semver:
1 | |
其它环境检查
如果本工具还需要依赖其它环境,也可以使用类似上面的检查方法,只是各个工具的版本号获取方法可能稍有不同。也可以参考一些复杂的命令行工具,添加一个 doctor 命令(比如 cli-test doctor)专门做环境检查用。
更新检查
通常一个应用程序都不会只发布一个版本,而是会不断的迭代新的版本,当 npm 包发布新版本时用户是无法感知到的,所以需要在用户每次使用这个工具的时候,主动去检查线上最新的版本,如果用户使用的版本太旧就主动提示用户升级。
获取当前 npm 包的版本很容易,可以读取 package.json 文件中的 version 拿到。获取线上的最新版本号稍麻烦些,需要用到 node.js 的 http 模块发起一个 http 请求到http://registry.npmjs.org/[模块名],返回结果是一个 json 字符串,里面包含了 dist-tags.latest 字段可以获取当前最新版本。
为了方便举例,这里使用到一个第三方 http 请求库 axios
1 | |
测试和发布
测试
命令行工具开发完成后需要测试它的功能是否正确,npm 提供了 npm link 命令模拟安装本地模块,也可以使用 yarn link。
在发布之前通常要对源代码进行编译并且完整地跑一遍单元测试,编译和单元测试的内容因项目而异,不在这里做过多介绍。需要注意的一点时,为了防止发布时忘记了编译和测试代码,可以通过 package.json 中配置 scripts.prepublish 来解决。
1 | |
发布
npm 提供了 dist-tag 来标记当前发布的版本,默认是 latest,可以自定义。比如要发布一个测试版本,可以使用 npm publish --tag next,通常情况下 npm install cli-test 只会安装 latest 标记的最新版本,要安装到 --tag next 的版本,需要指明安装的版本号:npm install cli-test@x.x.x或执行 npm install cli-test@next 指定要安装 --tag next 的最新版本。
