Skip to content

Nest Commander

除了独立应用文档中介绍的内容之外,还有一个 nest-commander 包,可以让你用类似典型 Nest 应用的结构来编写命令行程序。

提示

nest-commander 是一个第三方包,并不由整个 NestJS 核心团队维护。如果你在该库中发现问题,请在其对应仓库中反馈。

安装

和其他任何包一样,在使用之前必须先安装它。

bash
$ npm i nest-commander

命令文件

nest-commander 通过装饰器让编写新的命令行应用变得很简单:类上使用 @Command() 装饰器,类中的方法上使用 @Option() 装饰器。每个命令文件都应实现 CommandRunner 抽象类,并使用 @Command() 装饰器进行修饰。

每个命令在 Nest 中都会被视作一个 @Injectable(),因此你熟悉的依赖注入机制仍然可以像预期那样工作。唯一需要注意的是抽象类 CommandRunner,每个命令都应实现它。CommandRunner 抽象类确保所有命令都具有一个 run 方法,该方法返回 Promise<void>,并接收 string[], Record<string, any> 作为参数。run 方法就是你启动全部逻辑的地方,它会接收所有未匹配到 option 标志的参数,并以数组形式传入,以便你在需要时处理多个参数。至于选项参数,也就是 Record<string, any>,其中属性名会与 @Option() 装饰器中给定的 name 属性一致,而属性值则对应选项处理函数的返回值。如果你希望获得更好的类型安全,也完全可以为这些选项自行创建一个接口。

运行命令

类似于在 NestJS 应用中我们可以使用 NestFactory 创建服务并通过 listen 启动,nest-commander 包也暴露了一个易于使用的 API 来运行你的程序。导入 CommandFactory,使用其静态方法 run,并传入应用的根模块。大致如下:

ts
import { CommandFactory } from 'nest-commander';
import { AppModule } from './app.module';

async function bootstrap() {
  await CommandFactory.run(AppModule);
}

bootstrap();

默认情况下,使用 CommandFactory 时 Nest 的 logger 是关闭的。不过你也可以将它作为 run 函数的第二个参数传入。你可以提供自定义的 NestJS logger,或者传入你希望保留的日志级别数组。如果你只想输出 Nest 的错误日志,那么至少传入 ['error'] 会比较有用。

ts
import { CommandFactory } from 'nest-commander';
import { AppModule } from './app.module';
import { LogService } './log.service';

async function bootstrap() {
  await CommandFactory.run(AppModule, new LogService());

  // or, if you only want to print Nest's warnings and errors
  await CommandFactory.run(AppModule, ['warn', 'error']);
}

bootstrap();

就是这样。在底层,CommandFactory 会帮你处理 NestFactory 的调用,并在需要时调用 app.close(),因此你通常不需要担心这里的内存泄漏问题。如果你需要添加错误处理,可以直接用 try/catch 包裹 run 调用,或者在 bootstrap() 调用后链式添加 .catch()

测试

如果命令行脚本写得很棒,但却不好测试,那就没太大意义。幸运的是,nest-commander 提供了一些可以很好融入 NestJS 生态的工具,用起来会很顺手。与其在测试模式下使用 CommandFactory 构建命令,不如使用 CommandTestFactory 并传入你的元数据,这一点与 @nestjs/testing 中的 Test.createTestingModule 非常相似。实际上,它底层正是使用了这个包。你仍然可以在调用 compile() 之前继续链式调用 overrideProvider 之类的方法,以便在测试中直接替换 DI 组件。

整体示例

下面这个类等价于创建了一个 CLI 命令:它可以接受 basic 这个子命令,也可以直接被调用,同时支持 -n-s-b(以及它们的长选项),并且每个选项都有自定义解析器。和 commander 的惯例一样,它也支持 --help 标志。

ts
import { Command, CommandRunner, Option } from 'nest-commander';
import { LogService } from './log.service';

interface BasicCommandOptions {
  string?: string;
  boolean?: boolean;
  number?: number;
}

@Command({ name: 'basic', description: 'A parameter parse' })
export class BasicCommand extends CommandRunner {
  constructor(private readonly logService: LogService) {
    super()
  }

  async run(
    passedParam: string[],
    options?: BasicCommandOptions,
  ): Promise<void> {
    if (options?.boolean !== undefined && options?.boolean !== null) {
      this.runWithBoolean(passedParam, options.boolean);
    } else if (options?.number) {
      this.runWithNumber(passedParam, options.number);
    } else if (options?.string) {
      this.runWithString(passedParam, options.string);
    } else {
      this.runWithNone(passedParam);
    }
  }

  @Option({
    flags: '-n, --number [number]',
    description: 'A basic number parser',
  })
  parseNumber(val: string): number {
    return Number(val);
  }

  @Option({
    flags: '-s, --string [string]',
    description: 'A string return',
  })
  parseString(val: string): string {
    return val;
  }

  @Option({
    flags: '-b, --boolean [boolean]',
    description: 'A boolean parser',
  })
  parseBoolean(val: string): boolean {
    return JSON.parse(val);
  }

  runWithString(param: string[], option: string): void {
    this.logService.log({ param, string: option });
  }

  runWithNumber(param: string[], option: number): void {
    this.logService.log({ param, number: option });
  }

  runWithBoolean(param: string[], option: boolean): void {
    this.logService.log({ param, boolean: option });
  }

  runWithNone(param: string[]): void {
    this.logService.log({ param });
  }
}

请确保将该命令类添加到某个模块中:

ts
@Module({
  providers: [LogService, BasicCommand],
})
export class AppModule {}

现在,为了能在 main.ts 中运行这个 CLI,可以这样写:

ts
async function bootstrap() {
  await CommandFactory.run(AppModule);
}

bootstrap();

就这样,你就拥有了一个命令行应用。

更多信息

更多信息、示例和 API 文档,请访问 nest-commander 文档站点

基于 NestJS 官方文档翻译