Nest Commander
除了独立应用文档中介绍的内容之外,还有一个 nest-commander 包,可以让你用类似典型 Nest 应用的结构来编写命令行程序。
提示
nest-commander 是一个第三方包,并不由整个 NestJS 核心团队维护。如果你在该库中发现问题,请在其对应仓库中反馈。
安装
和其他任何包一样,在使用之前必须先安装它。
$ 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,并传入应用的根模块。大致如下:
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'] 会比较有用。
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 标志。
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 });
}
}请确保将该命令类添加到某个模块中:
@Module({
providers: [LogService, BasicCommand],
})
export class AppModule {}现在,为了能在 main.ts 中运行这个 CLI,可以这样写:
async function bootstrap() {
await CommandFactory.run(AppModule);
}
bootstrap();就这样,你就拥有了一个命令行应用。
更多信息
更多信息、示例和 API 文档,请访问 nest-commander 文档站点。