从零开始编写 Node 命令行程序

兴百放,YMFE 编码小学生,喜欢折腾,追求简单而美和极简主义。

前言

大家平时用一些诸如 ykit, vue-cli,create-react-app 等命令行脚手架时,有没有人考虑过以下两个问题

  • 它是如何实现的;
  • 如果有需求开发这样的命令行,如何入手;

现在带着这些问题,让我们拨开这层迷雾,直达他的核心,从零开始,搭建一个最为简单的脚手架。

Let’ s go!

需求描述

这是一个真实的需求。

使用者只需一条命令就可以把当前目录或者特定目录中的图片上传到图片服务器,另外,还需要支持递归扫描。

例如:

1
2
3
4
5
# 上传当前目录
sample-cli push .

# 上传特定目录
sample-cli push /path/to

用到的 npm 包

涉及到命令,就避不开要能正确获得命令的名字,参数和选项这些内容,如果这些都是自己处理,是相当麻烦。幸好,有些大神已经处理好这些内容,我们只需用就可以了。

  • commander (必选,统一处理命令,参数和选项)
  • colors(可选,输出带颜色的 console)

初始化项目

1
2
3
4
5
# 进入目录
cd sample-cli

# 一路回车即可,暴力吧
npm init

添加 bin 目录

在项目根目录中添加 bin 文件夹(存放启动脚本),在 bin 文件夹中,添加 sample-cli.js 文件(随便命名),内容如下:

1
2
3
#!/usr/bin/env node

console.info('hello npm cli');

第一行必须是这样写。如果你熟悉 shell 命令的话,容易理解,如果不熟悉,也没关系,只要记住有这一行就行。

其实就是告诉系统,这个文件以 node 方式执行。

在 package.json 中添加 bin 字段

1
2
3
"bin": {
"sample-cli": "./bin/sample-cli.js"
}

这个字段可以将开发者希望执行的脚本注册到环境变量(PATH)中,不同的 key 对应不同的脚本。也就是说,当我们执行

1
~ sample-cli

等价于

1
~ /path/to/bin/sample-cli.js

关于 bin 字段更多信息,请参考npm 文档 package.json 一节

本地开发及测试

在本地开发时,不需要每次执行 npm install -g 命令,如果修改一个文件,就执行一次,相当的麻烦。 npm 命令内部提供了一个 link 命令帮我们做这件事,执行 npm link 会自动在全局变量中注册我们的命令。

1
2
# 在项目录执行
npm link

然后在 terminal 中执行 sample-cli ,就会输出

1
hello npm cli

到这里,命令行的雏形已经出来了,是不是 so easy~~~

编写第一个命令

现在我们希望有一个叫 push 的命令(sample-cli push),哪怎么办呢?这就需要 commander 上场了。
此处有掌声。。。
最好提前先看看 commander 的使用文档

安装依赖

1
npm install commander

修改 ./bin/sample-cli.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env node

var program = require('commander');
program
// 获得版本
.version(require('../package').version)
.usage('<command> [options]');

program
.command('push <pathname>')
.description('上传图片')
.option('-r, --recursive', '扫描当前文件下所有图片')
.action((pathname, cmd) => {
require('../lib/push')(pathname, cmd)
});

program.parse(process.argv);

// 如果什么都没输,直接显示 帮助
if (!process.argv.slice(2).length) {
program.outputHelp();
}

虽然行数比较多,但是看语法很容易理解。

添加命令文件

新建 lib 文件夹(约定俗成),存放一些命令。新建 push.js ,内容如下:

1
2
3
4
5
6
7
8
9
async function push(pathname, options) {
console.info(`pathname: ${pathname}, recursive: ${options.recursive}`);
}
module.exports = (...args) => {
push(...args).catch(err => {
error(err);
process.exit;
});
}

运行

我们要求 push 命令需要确切知道上传的路径。因此,如果直接运行

1
sample-cli push

会提示 error: missing required argument `pathname’

现在给 push 加上 pathname

1
sample-cli push /path/to

会提示:pathname: /path/to, recursive: undefined

说明已经拿到所需的参数。拿到的之后做什么事,就看各自的实际场景了。

另外,加选项的输出,大家亲自体验体验,这里就不多做赘述。

发布到 npm 仓库

首先,需要在 npm 仓库站点有自己账户

其次,使用 npm login 在本地登陆

最后,在当前脚手架的目录,运行 npm publish ,同步到 npm 仓库中。

总结

本篇围绕一个简单的场景,让大家明白编写命令行工具其实是一件很简单容易的事。在今后的工作中,如果有些需求的步骤很有流程,那么我们可以自己编写这样的命令,让程序代替我们去做,能够大大节省我们的开发效率和开发时间,并且还能造福其他同学,何乐而不为呢?你说对不对。

参考

你所不知道的模块调试技巧 - npm link