oclif,满足我对开发CLI的一切想象

2,484 阅读6分钟

前言

最近在做一个面向开发者的项目,需要开发一个CLI,用于连接开发者与项目站点,作为开发者与站点之间的桥梁。所以需要这个CLI至少提供登录登出、上传下载操作对应的指令。因为设置到一些本地操作,在每个不同的操作之前或之后,还有一些环境检查、清理及初始化的事情需要处理。 一开始,和之前做CLI一样,我首选了commander来开发这次的CLI。但写着写着我发现,管道式的commander越来越无法满足我这个复杂CLI的需求,主要体现在以下几个方面:

  1. 对于multiple command, 无法根据上下文的多个参数决定某个指令具体需要执行哪些内容

  2. 没有hooks的配置,做一些操作的前置后置的处理时,总是需要各种hard code

  3. 无法优雅地将一个复杂的CLI的不同动作对应的指令分模块来管理,当CLI非常复杂且冗长时,代码也会变得十分难以维护

    于是我设想有这样一个CLI开发的基础工具,可以解决上述的问题,比如让我可以优雅地将不同指令的代码做合理的区分,比如可以较为方便地做各种pre-post这样的前后置的处理。寻寻觅觅见,我发现了本文想要推荐的主角 —— oclif。不仅满足我上面提到的各种需要处理的问题,还提供了很多非常实用的功能。但是当我在npmtrend对比commander、yargs和oclif时,发现oclif的传播实在是有限,下载量实在是少的可怜。 image 所以接下来就具体展开介绍介绍oclif,安利这个开发CLI的好帮手🔧

oclif

在oclif的官方站点的自我定位中可以看到,它是一个帮助构建node CLI的框架。那它是如何来帮助开发者构建CLI的呢?

oclif single&oclif multi, 根据需求定制初始化CLI目录

oclif首先将CLI分为两大类,分别是single-commandmulti-commandsingle-command指的是类似lscurl这样的single-command,配合各种flag来达到各种不同的执行的效果,如ls -l; 另一种则是multi-command, 比如npm或者git,它用非常多的二级指令和各种flag来区分CLI执行的指令。 在这样清晰的区分前提下,oclif提供了针对开发single-commandmulti-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的项目已经被非常快速地创建 image 在初始化的项目的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),分别为initprerunpostruncommand_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非常大的时候,自己去写文档真是一件非常头疼的事情,我相信不少开发者自己可能也有自己一套生产文档的脚步吧,如果一个一个码字那真是非常糟糕的开发体验。 image

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