前言
最近在做一个面向开发者的项目,需要开发一个CLI,用于连接开发者与项目站点,作为开发者与站点之间的桥梁。所以需要这个CLI至少提供登录登出、上传下载操作对应的指令。因为设置到一些本地操作,在每个不同的操作之前或之后,还有一些环境检查、清理及初始化的事情需要处理。 一开始,和之前做CLI一样,我首选了commander来开发这次的CLI。但写着写着我发现,管道式的commander越来越无法满足我这个复杂CLI的需求,主要体现在以下几个方面:
-
对于multiple command, 无法根据上下文的多个参数决定某个指令具体需要执行哪些内容
-
没有hooks的配置,做一些操作的前置后置的处理时,总是需要各种hard code
-
无法优雅地将一个复杂的CLI的不同动作对应的指令分模块来管理,当CLI非常复杂且冗长时,代码也会变得十分难以维护
于是我设想有这样一个CLI开发的基础工具,可以解决上述的问题,比如让我可以优雅地将不同指令的代码做合理的区分,比如可以较为方便地做各种pre-post这样的前后置的处理。寻寻觅觅见,我发现了本文想要推荐的主角 —— oclif。不仅满足我上面提到的各种需要处理的问题,还提供了很多非常实用的功能。但是当我在npmtrend对比commander、yargs和oclif时,发现oclif的传播实在是有限,下载量实在是少的可怜。
所以接下来就具体展开介绍介绍oclif,安利这个开发CLI的好帮手🔧
oclif
在oclif的官方站点的自我定位中可以看到,它是一个帮助构建node CLI的框架。那它是如何来帮助开发者构建CLI的呢?
oclif single&oclif multi, 根据需求定制初始化CLI目录
oclif首先将CLI分为两大类,分别是single-command和multi-command。
single-command指的是类似ls或curl这样的single-command,配合各种flag来达到各种不同的执行的效果,如ls -l;
另一种则是multi-command, 比如npm或者git,它用非常多的二级指令和各种flag来区分CLI执行的指令。
在这样清晰的区分前提下,oclif提供了针对开发single-command和multi-command时,创建CLI项目的不同指令,分别是npx oclif single ${cliname}和npx oclif multi ${cliname},执行之后,会根据用户的选择创建一个配置好编译、运行指令CLI的初始项目,用户再根据自己开发的CLI的具体需要,修改已有的配置或者新增指令。
以执行创建mulit-command的CLI为例,执行npx oclif multi newcli, 一路回车使用默认配置,会得到这样一个项目目录,默认使用ts和tsc编译,bin对于打包构建后的执行路径,src/commands目录下每个文件代表一个不同的指令 ——
.
├── README.md
├── bin
│ ├── run ## bin对于的执行路径
│ └── run.cmd ## 兼容windows的执行
├── package.json
├── src
│ ├── commands ##
│ │ └── hello.ts ##对应hello二级指令,初始化会提供一个hello指令供参考
│ └── index.ts ## 源码入口文件
├── test ## 测试相关
├── tsconfig.json
└── yarn.lock
执行yarn install & yarn prepack完成打包工作,并执行./bin/run,可以看到整个CLI的情况,一个CLI的项目已经被非常快速地创建
在初始化的项目的package.json中,会有一个oclif的字段,用于注册下文的commands、plugins、hooks的执行文件的地址。根目录下,会用一个
oclif.manifest.json的文件,来记录这个CLI目前的各类信息,如果我们对CLI的各个指令做了增删改的调整并重新编译,这个文件也会进行更新。
一个优质的command class
上文提到,通过oclif创建的项目的multi-command的CLI的command目录下每个文件对应一个二级指令,比如例子的hello文件。每个指令文件都是一个class, 继承自oclif的包@oclif/command提供了的类Command,Command很类似commander提供的能力,但是以类的形式出现,又比commander更丰富且可扩展,当然,我个人也觉得类的方式比commander的配置方式更像一个CLI的编码方式。以src/commands/hello为例,
- 提供了很多个不同的静态成员,如
description(指令描述)、flags(配置非必填的参数)、和args(配置必填的参数)和examples(一个array,可以配置多种不同flags、args的组合的demo)。 - run方法,即指令执行时对应的函数,在run方法中,可以拿到flags和args的信息,并根据配置在run中执行对应的branch。
如果我们需要新增一个二级指令,则需要在commands下新增一个目录(可以通过
oclif command来新增),同样的新指令对应的文件也是一个继承自Command的类,并编写它对应的flags、args和run指令即可。
import {Command, flags} from '@oclif/command'
export default class Hello extends Command {
static description = 'describe the command here'
static examples = [
`$ newcli2 hello
hello world from ./src/hello.ts!
`,
]
static flags = {
help: flags.help({char: 'h'}),
// flag with a value (-n, --name=VALUE)
name: flags.string({char: 'n', description: 'name to print'}),
// flag with no value (-f, --force)
force: flags.boolean({char: 'f'}),
}
static args = [{name: 'file'}]
async run() {
const {args, flags} = this.parse(Hello)
const name = flags.name ?? 'world'
this.log(`hello ${name} from ./src/commands/hello.ts`)
if (args.file && flags.force) {
this.log(`you input --force and --file: ${args.file}`)
}
}
}
hooks配置,优雅处理各种pre&post的操作
如前言提到的,有时会需要做一些指令执行前或指令执行后的操作。oclif同样注意到了这种需求,也提供了比较优雅的处理方式。oclif定义了hooks可执行的四个阶段(event),分别为init、prerun、postrun和command_not_found。通过oclif hook ${hookName} --event=${eventName}创建的hook,会以src/hooks/${eventName}/${hookName}目录下创建一个hook执行对应的文件,如下所示,执行npx oclif hook beforeAll --event=prerun,代表一个在所有指令执行前都先执行的处理操作,则会在src/hooks/prerun/beforeAll的路径出现这样的template,同时在package.json的oclif字段中将这个hook注册。
import {Hook} from '@oclif/config'
const hook: Hook<'prerun'> = async function (opts) {
process.stdout.write(`example hook running ${opts.id}\n`)
}
export default hook
当然了,如果我们并不想在所有指令执行前、执行后做前置后置处理,而是只针对某些指令做处理,比如我们就是这样的场景。则需要用custom events,同样是在hooks目录下创建对应的执行文件,然后在需要执行前置后置处理的指令的run方法中,通过this.config.runHook(${eventname})的方法调用执行,具体的可以看官方的demo的custom events这个章节。
plugins,插件配置
插件配置也是oclif提供的一个蛮符合我需求的东西,基本官方的插件就覆盖了很大部分CLI常见的插件需求,都是即插即用的,直接安装依赖后,在oclif字段的plugins上增加这个插件即可。
@oclif/plugin-help—— 用于添加help指令@oclif/plugin-warn-if-update-available—— 用于在CLI更新的时候提示使用者有新版本可以安装 当然还有其他plugins,这两个是我的CLI需要用的,具体官方提供的Plugins可以看看oclif的plugin介绍
贴心地生成使用文档
上文提到了,每次重新编译执行prepack和postpack时,oclif.manifest.json都会被更新与最新的功能对应。与此同时,README.md文档中使用文档、版本号等信息也会一并被oclif dev的脚本同步更新,信息当然是从各个指令和manifest中拿到的,只要我们写的是对的,README就是准确的。其实当CLI非常大的时候,自己去写文档真是一件非常头疼的事情,我相信不少开发者自己可能也有自己一套生产文档的脚步吧,如果一个一个码字那真是非常糟糕的开发体验。

其实在写这篇文章之前,我一直在想为什么在我看来更优秀的oclif的使用量和传播量远不如commander和yargs。写着写着我意识到,commander和yargs确实已经满足了很多轻量CLI开发场景的需求。 但是像oclif这样更完善的工具,值得被更多人看到。