一、CLI 简介
CLI(Command Line Interface)命令行界面是在图形用户界面得到普及之前使用最为广泛的用户界面,它通常不支持鼠标,用户通过键盘输入指令,计算机接收到指令后,予以执行。也有人称之为字符用户界面(character user interface,CUI)。
为了便于大家的理解,我们来举一个实际的例子,比如 Angular 开发者都熟悉的 Angular CLI:
(图片来源 —— https://cli.angular.io/)
除了 Angular CLI 之外,一些主流的框架也有提供相应的 CLI,比如 Vue CLI 和 Ionic CLI 等。在日常工作中,为了提高开发效率或统一开发方式,我们通常会开发团队内专属的 CLI 工具。那么如何开发 CLI 工具呢,对于前端开发者来说,我们可以基于 Node.js 来开发,因为目前 NPM 上已经有很多成熟的第三方库,如 chalk、Inquirer.js、commander.js 和 configstore 等。基于这些成熟的第三方库,我们就可以方便、快捷地开发 Node.js CLI 工具。
二、Oclif 简介
This is a framework for building CLIs in Node.js. This framework was built out of the Heroku CLI but generalized to build any custom CLI. It’s designed both for single-file CLIs with a few flag options, or for very complex CLIs that have subcommands (like git or heroku).
Oclif 是由 Heroku(一个支持多种编程语言的云应用平台,在 2010 年被 Salesforce.com 收购)开发的 Node.js Open CLI 开发框架,它可以用来开发 single-command
CLI 或 multi-command
CLI,同时还提供了可扩展的插件机制和钩子机制。
2.1 CLI 类型
使用 Oclif 你可以创建两种不同类型的 CLI,即 Single CLIs 和 Multi CLIs。Single CLIs 类似于 Linux 或 MacOS 平台中常见的 ls
或 cat
命令。而 Multi CLIs 类似于前面提到的 Angular CLI 或 Vue CLI,它们包含子命令,这些子命令本身也是 Single CLI。在 package.json
文件中有一个 oclif.commands
字段,该字段指向一个目录,该目录包含了当前 CLI 的所有子命令。举个例子,假设你拥有一个名为 mycli
的 CLI,该 CLI 含有 mycli create
和 mycli destroy
两个子命令,那么你将拥有一个与下面类似的项目结构:
package.json
src/
└── commands/
├── create.ts
└── destroy.ts
2.2 用法
创建一个 single-command CLI:
代码语言:javascript复制$ npx oclif single mynewcli
? npm package name (mynewcli): mynewcli
$ cd mynewcli
$ ./bin/run
hello world from ./src/index.js!
创建一个 multi-command CLI:
代码语言:javascript复制$ npx oclif multi mynewcli
? npm package name (mynewcli): mynewcli
$ cd mynewcli
$ ./bin/run --version
mynewcli/0.0.0 darwin-x64 node-v9.5.0
$ ./bin/run --help
USAGE
$ mynewcli [COMMAND]
COMMANDS
hello
help display help for mynewcli
$ ./bin/run hello
hello world from ./src/hello.js!
三、Todocli 实战
下面我们将创建一个 Todo CLI,它可以执行以下 4 个操作:
- 添加一个新任务
- 查看所有任务
- 更新任务
- 移除任务
3.1 初始化项目
使用 Oclif 你可以创建两种不同类型的 CLI,即 Single CLIs 和 Multi CLIs。这里我们来创建一个 Multi CLIs 项目:
代码语言:javascript复制$ npx oclif multi todocli
以上命令执行后,我们需要设置 todocli 项目的一些配置信息,具体如下图所示:
当上述的命令成功执行后,会在当前的命令的执行目录下创建一个 todocli
项目。接着我们进入该项目,然后运行 help
命令:
$ cd todocli && ./bin/run --help
以上命令运行后,控制台将输出以下结果:
代码语言:javascript复制a todo list cli
VERSION
todocli/1.0.0 darwin-x64 node-v10.12.0
USAGE
$ todocli [COMMAND]
COMMANDS
hello describe the command here
help display help for todocli
3.2 项目结构
在 src
目录中,我们可以发现一个名为 commands
子目录,该目录包含所有与文件名相关的所有命令。比如,我们有一个名为 hello
的命令,那么在 commands
目录中将会包含一个 hello.js
或 hello.ts
文件。这里我们无需进行任何设置,即可运行该命令。
$ ./bin/run hello
hello world from ./src/commands/hello.ts
现在让我们删除 hello.ts
,因为我们不需要它了。
3.3 设置数据库
为了存储我们的任务,我们需要一个存储系统。为简单起见,我们将使用 lowdb,这是一个非常简单的 JSON 文件存储系统。
让我们来安装它:
代码语言:javascript复制$ npm install -S lowdb
$ npm install -D @types/lowdb
待成功安装 lowdb 依赖后,在我们项目的根目录下创建一个 db.json
文件,这个文件用来保存数据。然后我们继续在 src
目录中创建一个 db.ts
文件并输入以下内容:
import * as lowdb from "lowdb";
import * as FileSync from "lowdb/adapters/FileSync";
type Todo = {
id: number;
task: string;
done: boolean;
};
type TodoSchema = {
todos: Todo[];
};
const adapter = new FileSync<TodoSchema>("db.json");
const db = lowdb(adapter);
db.defaults({ todos: [] }).write();
const Todos = db.get("todos");
export { db, Todos };
3.4 添加任务
设置完数据库,让我们先来实现添加 Todo 任务的功能。这里我们将使用 Oclif CLI 提供的命令,来快速创建 command
。下面我们先来创建 add
命令:
$ npx oclif command add
以上命令运行后,在 src/command
目录下会生成一个 add.ts
文件,打开该文件并输入以下代码:
import { Command, flags } from "@oclif/command";
import { Todos } from "../db";
export default class Add extends Command {
static description = `Adds a new todo
...
Adds a new todo to the existing list
`;
static flags = {
task: flags.string({ char: "n", description: "task" })
};
async run() {
const {
flags: { task }
} = this.parse(Add);
if (!task) return;
const todo = await Todos.push({
task,
id: Todos.value().length,
done: false
}).write();
this.log(JSON.stringify(todo));
}
}
上述代码中包含一些关键组件:
description
属性,用于描述命令的用途;flags
属性,用于描述传递给命令的标识;- 一个
run
方法用于执行当前命令的主要功能;
创建完 add
命令后,我们可以在命令行中运行它:
$ ./bin/run add --task="Learn Oclif"
以上命令成功执行后,会输出以下信息:
代码语言:javascript复制[{"task":"Learn Oclif","id":0,"done":false}]
同时在项目根目录的 db.json
文件中也会保存相应的信息:
{
"todos": [
{
"task": "Learn Oclif",
"id": 0,
"done": false
}
]
}
3.5 查看任务
下面我们继续使用 Oclif CLI 提供的命令,来创建一个新的 show
命令:
$ npx oclif command show
以上命令运行后,在 src/command
目录下会生成一个 show.ts
文件,打开该文件并输入以下代码:
import { Command, flags } from "@oclif/command";
import chalk from "chalk";
import { Todos } from "../db";
export default class Show extends Command {
static description = `Shows existing tasks
...
Show all the tasks sorted by their ids
`;
async run() {
const todos = await Todos.sortBy("id").value();
todos.forEach(({ id, task, done }) => {
this.log(
`${chalk.magenta(id.toString())} ${
done ? chalk.green("DONE") : chalk.grey("NOT DONE")
} : ${task}`
);
});
}
}
创建完 show
命令,我们马上来测试一下,即在命令行输入以下命令:
$ ./bin/run show
以上命令成功执行后,会输出以下信息:
代码语言:javascript复制0 NOT DONE : Learn Oclif
此外,在运行 show
命令时,我们还可以添加 --help
标识,输出该命令的帮助信息:
$ ./bin/run show --help
Shows existing tasks
USAGE
$ todocli show
DESCRIPTION
...
Show all the tasks sorted by their ids
3.6 更新任务
目前我们已经创建了 add
和 show
两个命令,接下来我们再来创建一个 update
命令,该命令用于更新已创建的 Todo 任务,简单起见,我们只实现更新任务是否完成的状态。与前两个命令一样,我们首先创建 update
命令:
$ npx oclif command update
以上命令运行后,在 src/command
目录下会生成一个 update.ts
文件,打开该文件并输入以下代码:
import { Command, flags } from "@oclif/command";
import { Todos } from "../db";
export default class Update extends Command {
static description = `Marks a task as done
...
Marks a task as done
`;
static flags = {
id: flags.string({ char: "n", description: "task id" })
};
async run() {
const {
flags: { id }
} = this.parse(Update);
if (!id) return;
const todo = await Todos.find({ id: parseInt(id, 10) })
.assign({ done: true })
.write();
this.log(JSON.stringify(todo));
}
}
创建完 update
命令,我们来尝试更新一下前面通过 add
命令创建的 Learn Oclif
待办任务的状态:
$ ./bin/run update --id=0
以上命令成功执行后,会输出以下信息:
代码语言:javascript复制{"task":"Learn Oclif","id":0,"done":true}
3.7 移除任务
现在我们来创建最后一个命令,该命令用于移除 Todo 任务。与前面一样,我们首先创建 remove
命令:
$ npx oclif command remove
以上命令运行后,在 src/command
目录下会生成一个 remove.ts
文件,打开该文件并输入以下代码:
import { Command, flags } from "@oclif/command";
const { Todos } = require("../db");
export default class Remove extends Command {
static description = `Removes a task by id
...
Removes a task permanently from database by id
`;
static flags = {
id: flags.string({ char: "n", description: "task id", required: true })
};
async run() {
const {
flags: { id }
} = this.parse(Remove);
const todo = await Todos.remove({ id: parseInt(id, 10) }).write();
this.log(JSON.stringify(todo));
}
}
创建完 remove
命令,我们来实际测试一下:
$ ./bin/run remove --id=0
如果不出意外的话,当以上命令成功运行后,项目根目录下 db.json
文件的内容将发生变化,具体如下:
{
"todos": []
}
很明显前面我们通过 add
命令创建的 Todo 任务,已经被移除了。此时,Todo CLI 包含的 4 个命令都已经创建完成了,最后我们来介绍一下如何把 Todo CLI 项目发布到 NPM。
3.8 构建与发布
发布到 NPM 前,你需要确保拥有一个 NPM 账户,然后使用以下命令进行登录:
代码语言:javascript复制$ npm login
接着在项目的根目录中运行以下 NPM 脚本:
代码语言:javascript复制$ npm run prepack
执行该命令后会对项目进行自动构建并更新项目中的 README.md
说明文档。项目构建成功后,就可以发布到 NPM 了,具体操作如下:
$ npm version (major|minor|patch) # bumps version, updates README, adds git tag
$ npm publish
四、参考资源
- 维基百科 - 命令行界面
- Github - oclif
- how-to-build-a-cli-tool-in-nodejs
欢迎小伙伴们订阅前端全栈修仙之路,及时阅读 Angular、TypeScript、Node.js/Java和Spring技术栈最新文章。