Mở rộng từ tài liệu standalone application, còn có package nest-commander để viết các ứng dụng command line theo cấu trúc tương tự một ứng dụng Nest thông thường.
Info nest-commander là một package bên thứ ba và không được quản lý bởi toàn bộ đội ngũ core của NestJS. Vui lòng báo cáo bất kỳ vấn đề nào gặp phải với thư viện tại repository tương ứng
Cài đặt
Cũng giống như mọi package khác, bạn cần cài nó trước khi có thể sử dụng.
$ npm i nest-commander
Một file Command
nest-commander giúp việc viết các ứng dụng command-line mới trở nên dễ dàng bằng decorator thông qua decorator @Command() cho class và decorator @Option() cho các method của class đó. Mỗi file command nên implement abstract class CommandRunner và nên được đánh dấu bằng decorator @Command().
Mỗi command được Nest coi như một @Injectable(), nên Dependency Injection thông thường vẫn hoạt động như bạn mong đợi. Điều duy nhất cần lưu ý là abstract class CommandRunner, class này nên được implement bởi mỗi command. Abstract class CommandRunner đảm bảo mọi command đều có một method run trả về Promise<void> và nhận vào các tham số string[], Record<string, any>. Method run là nơi bạn có thể bắt đầu toàn bộ logic của mình, nó sẽ nhận vào bất kỳ tham số nào không khớp với các option flag và truyền chúng vào dưới dạng mảng, phòng khi bạn thực sự muốn làm việc với nhiều tham số. Còn về các option, tức Record<string, any>, tên của các property này khớp với property name được truyền cho decorator @Option(), còn giá trị của chúng khớp với giá trị trả về của option handler. Nếu bạn muốn có type safety tốt hơn, bạn hoàn toàn có thể tạo một interface cho các option của mình.
Chạy Command
Tương tự như cách trong một ứng dụng NestJS chúng ta dùng NestFactory để tạo server và chạy nó bằng listen, package nest-commander cung cấp một API đơn giản để chạy server của bạn. Import CommandFactory và dùng static method run, truyền vào module gốc của ứng dụng. Đoạn code sẽ trông như sau
import { CommandFactory } from 'nest-commander';
import { AppModule } from './app.module.js';
async function bootstrap() {
await CommandFactory.run(AppModule);
}
await bootstrap();
Mặc định, logger của Nest sẽ bị tắt khi dùng CommandFactory. Tuy nhiên bạn vẫn có thể cung cấp nó, dưới dạng tham số thứ hai của hàm run. Bạn có thể truyền vào một logger NestJS tùy chỉnh, hoặc một mảng các log level bạn muốn giữ lại - có thể hữu ích nếu ít nhất truyền vào ['error'] ở đây, nếu bạn chỉ muốn in ra log lỗi của Nest.
import { CommandFactory } from 'nest-commander';
import { AppModule } from './app.module.js';
import { LogService } from './log.service.js';
async function bootstrap() {
await CommandFactory.run(AppModule, new LogService());
// hoặc, nếu bạn chỉ muốn in ra warning và error của Nest
await CommandFactory.run(AppModule, ['warn', 'error']);
}
await bootstrap();
Và thế là xong. Bên dưới, CommandFactory sẽ lo việc gọi NestFactory giúp bạn và gọi app.close() khi cần thiết, nên bạn không cần phải lo về memory leak ở đó. Nếu bạn cần thêm xử lý lỗi, bạn luôn có thể dùng try/catch bọc quanh lệnh run, hoặc chain thêm method .catch() vào lời gọi bootstrap().
Testing
Vậy thì viết một command line script “siêu ngầu” mà không thể test dễ dàng thì có ích gì đúng không? May mắn là nest-commander có một số tiện ích bạn có thể tận dụng, phù hợp hoàn hảo với hệ sinh thái NestJS, những ai đã quen với Nest sẽ thấy rất thân thuộc. Thay vì dùng CommandFactory để build command ở chế độ test, bạn có thể dùng CommandTestFactory và truyền vào metadata của mình, cách hoạt động khá giống với Test.createTestingModule từ @nestjs/testing. Thực tế, nó sử dụng chính package đó bên dưới. Bạn cũng vẫn có thể chain thêm các method overrideProvider trước khi gọi compile(), nhờ đó có thể thay thế các thành phần DI ngay trong bài test.
Ghép tất cả lại
Class dưới đây tương đương với việc có một CLI command có thể nhận subcommand basic hoặc được gọi trực tiếp, với -n, -s, và -b (cùng với các flag dài tương ứng) đều được hỗ trợ và có custom parser cho từng option. Flag --help cũng được hỗ trợ, như thông lệ của commander.
import { Command, CommandRunner, Option } from 'nest-commander';
import { LogService } from './log.service.js';
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 });
}
}
Đảm bảo command class được thêm vào một module
@Module({
providers: [LogService, BasicCommand],
})
export class AppModule {}
Và giờ để chạy CLI trong main.ts của bạn, bạn có thể làm như sau
async function bootstrap() {
await CommandFactory.run(AppModule);
}
await bootstrap();
Và như vậy, bạn đã có một ứng dụng command line.
Thông tin thêm
Truy cập trang docs của nest-commander để biết thêm thông tin, ví dụ, và tài liệu API.
Ghi chú người dịch Cách tiếp cận này khá giống Artisan Console trong Laravel (class command với $signature, handle()) hoặc cobra.Command trong Go: bạn định nghĩa command như một class/struct riêng, còn framework lo phần parse argument và gọi đúng handler. Điểm hay ở đây là mỗi command vẫn là một @Injectable() bình thường, nên bạn tái sử dụng được toàn bộ provider, service đã viết cho phần web app - không cần viết lại logic riêng cho CLI.
Ghi chú người dịch Vì nest-commander là package của bên thứ ba (không thuộc @nestjs/* chính thức), nên khi nâng cấp NestJS lên major version mới, hãy kiểm tra kỹ tính tương thích của package này trước, tránh trường hợp nó chưa kịp hỗ trợ API mới.