Chương Modules đã nói về các khái niệm cơ bản của module trong Nest, và có giới thiệu ngắn gọn về dynamic modules. Chương này sẽ đi sâu hơn vào chủ đề dynamic module. Sau khi đọc xong, bạn sẽ nắm được bản chất của dynamic module cũng như khi nào và làm thế nào để dùng nó.
Giới thiệu
Hầu hết ví dụ code trong phần Overview của tài liệu đều dùng các module thông thường, hay còn gọi là module tĩnh (static module). Module định nghĩa các nhóm component như provider và controller ghép lại thành một phần module hóa của toàn bộ ứng dụng. Chúng cung cấp một execution context (ngữ cảnh thực thi), hay phạm vi (scope), cho các component này. Ví dụ, provider được khai báo trong một module thì các thành viên khác trong cùng module đều nhìn thấy được mà không cần export. Khi một provider cần được nhìn thấy từ bên ngoài module, trước tiên nó phải được export từ module host (host module), rồi mới được import vào module tiêu thụ (consuming module).
Hãy cùng xem lại một ví dụ quen thuộc.
Đầu tiên, ta định nghĩa UsersModule để cung cấp (provide) và export UsersService. UsersModule là module host của UsersService.
import { Module } from '@nestjs/common';
import { UsersService } from './users.service.js';
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
Tiếp theo, ta định nghĩa AuthModule, module này import UsersModule, nhờ đó các provider được export từ UsersModule sẽ khả dụng bên trong AuthModule:
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service.js';
import { UsersModule } from '../users/users.module.js';
@Module({
imports: [UsersModule],
providers: [AuthService],
exports: [AuthService],
})
export class AuthModule {}
Nhờ các khai báo trên, ta có thể inject UsersService vào, chẳng hạn, AuthService được host trong AuthModule:
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service.js';
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
/*
Implementation that makes use of this.usersService
*/
}
Ta sẽ gọi cách làm này là gắn kết module tĩnh (static module binding). Toàn bộ thông tin mà Nest cần để ghép nối các module với nhau đã được khai báo sẵn trong module host và module tiêu thụ. Hãy phân tích kỹ hơn những gì diễn ra trong quá trình này. Nest làm cho UsersService khả dụng bên trong AuthModule bằng cách:
- Khởi tạo
UsersModule, bao gồm cả việc import bắc cầu (transitively) các module khác màUsersModuletự tiêu thụ, và giải quyết bắc cầu mọi dependency (xem Custom providers). - Khởi tạo
AuthModule, và làm cho các provider được export từUsersModulekhả dụng cho các component trongAuthModule(như thể chúng đã được khai báo ngay trongAuthModule). - Inject một instance của
UsersServicevàoAuthService.
Ghi chú người dịch Nếu quen React, bạn có thể hình dung host module xuất UsersService giống như một Context Provider “expose” ra giá trị, còn consuming module import nó giống như component con dùng useContext — chỉ khác là ở Nest việc “đăng ký ai cung cấp gì” xảy ra qua khai báo providers/exports tĩnh, được IoC container xử lý lúc khởi động app, chứ không phải lồng cây component lúc render.
Trường hợp cần dùng dynamic module
Với static module binding, module tiêu thụ không có cách nào tác động đến cách các provider từ module host được cấu hình. Tại sao điều này lại quan trọng? Hãy xét trường hợp ta có một module đa dụng (general purpose) cần hoạt động khác nhau tùy vào từng use case. Điều này tương tự khái niệm “plugin” trong nhiều hệ thống, nơi một cơ chế (facility) chung cần được cấu hình trước khi có thể được consumer sử dụng.
Một ví dụ điển hình trong Nest là module cấu hình (configuration module). Nhiều ứng dụng thấy hữu ích khi tách các chi tiết cấu hình ra ngoài (externalize) bằng một module cấu hình. Điều này giúp dễ dàng thay đổi cài đặt ứng dụng linh hoạt ở các môi trường triển khai khác nhau: ví dụ, một database phát triển cho developer, một database staging cho môi trường staging/testing, v.v. Bằng cách giao việc quản lý các tham số cấu hình cho một module cấu hình, source code của ứng dụng vẫn độc lập với các tham số cấu hình.
Thách thức nằm ở chỗ bản thân module cấu hình, vì nó là generic (giống một “plugin”), cần được tùy biến bởi module tiêu thụ nó. Đây chính là lúc dynamic module phát huy tác dụng. Nhờ tính năng dynamic module, ta có thể làm cho module cấu hình trở nên động (dynamic), để module tiêu thụ có thể dùng một API nhằm kiểm soát cách module cấu hình được tùy biến ngay tại thời điểm nó được import.
Nói cách khác, dynamic module cung cấp một API để import module này vào module khác, đồng thời tùy biến thuộc tính và hành vi của module đó ngay khi import, thay vì dùng static binding như đã thấy từ trước đến giờ.
Ví dụ Config module
Ở phần này ta sẽ dùng phiên bản cơ bản của ví dụ code từ chương configuration. Phiên bản hoàn chỉnh tính đến cuối chương này có sẵn dưới dạng ví dụ làm việc tại đây.
Yêu cầu của ta là làm cho ConfigModule nhận một object options để tùy biến nó. Đây là tính năng ta muốn hỗ trợ. Ví dụ cơ bản hard-code vị trí file .env nằm ở thư mục gốc của project. Giả sử ta muốn cấu hình được điều đó, để có thể quản lý các file .env ở bất kỳ thư mục nào tùy ý. Ví dụ, giả sử bạn muốn lưu các file .env khác nhau trong một thư mục con nằm dưới project root tên là config (tức thư mục anh em với src). Bạn muốn có thể chọn thư mục khác nhau khi dùng ConfigModule ở các project khác nhau.
Dynamic module cho ta khả năng truyền tham số vào module được import để thay đổi hành vi của nó. Hãy xem cơ chế này hoạt động ra sao. Sẽ dễ hiểu hơn nếu ta bắt đầu từ đích đến — hình dung xem điều này trông như thế nào từ góc nhìn của module tiêu thụ — rồi đi ngược lại. Trước tiên, hãy xem nhanh lại ví dụ import tĩnh ConfigModule (tức cách tiếp cận không có khả năng tác động đến hành vi của module được import). Chú ý kỹ mảng imports trong decorator @Module():
import { Module } from '@nestjs/common';
import { AppController } from './app.controller.js';
import { AppService } from './app.service.js';
import { ConfigModule } from './config/config.module.js';
@Module({
imports: [ConfigModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Hãy xem một import dynamic module, trong đó ta truyền vào một object cấu hình, sẽ trông như thế nào. So sánh sự khác biệt trong mảng imports giữa hai ví dụ sau:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller.js';
import { AppService } from './app.service.js';
import { ConfigModule } from './config/config.module.js';
@Module({
imports: [ConfigModule.register({ folder: './config' })],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Hãy xem điều gì đang diễn ra trong ví dụ dynamic ở trên. Những phần nào đang “chuyển động”?
ConfigModulelà một class bình thường, nên ta có thể suy ra nó phải có một static method tên làregister(). Ta biết nó là static vì ta gọi nó trên chính classConfigModule, chứ không phải trên một instance của class. Lưu ý: method này, mà chúng ta sẽ tạo ngay sau đây, có thể đặt tên tùy ý, nhưng theo quy ước ta nên gọi nó làforRoot()hoặcregister().- Method
register()do ta tự định nghĩa, nên ta có thể nhận bất kỳ input argument nào mình muốn. Trong trường hợp này, ta sẽ nhận một objectoptionsđơn giản với các thuộc tính phù hợp — đây là trường hợp phổ biến. - Ta có thể suy ra method
register()phải trả về thứ gì đó giống như mộtmodule, vì giá trị trả về của nó xuất hiện trong danh sáchimportsquen thuộc, mà từ trước đến giờ ta biết là chứa danh sách các module.
Thực tế, thứ mà method register() của ta sẽ trả về là một DynamicModule. Dynamic module thực chất chỉ là một module được tạo ra tại run-time, có đầy đủ thuộc tính giống hệt static module, cộng thêm một thuộc tính nữa gọi là module. Hãy xem nhanh lại một khai báo static module mẫu, chú ý kỹ các option module được truyền vào decorator:
@Module({
imports: [DogsModule],
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService]
})
Dynamic module phải trả về một object có cùng interface y hệt, cộng thêm một thuộc tính nữa gọi là module. Thuộc tính module đóng vai trò là tên của module, và phải trùng với tên class của module, như minh họa trong ví dụ dưới đây.
Gợi ý Với một dynamic module, mọi thuộc tính trong object option của module đều là tùy chọn trừ module.
Còn static method register() thì sao? Giờ ta đã thấy rõ nhiệm vụ của nó là trả về một object có interface DynamicModule. Khi gọi nó, thực chất ta đang cung cấp một module cho danh sách imports, tương tự cách ta làm ở trường hợp static bằng cách liệt kê tên class của module. Nói cách khác, API dynamic module đơn giản chỉ trả về một module, nhưng thay vì cố định các thuộc tính trong decorator @Module, ta chỉ định chúng theo kiểu lập trình (programmatically).
Vẫn còn vài chi tiết cần làm rõ để bức tranh trọn vẹn hơn:
- Giờ ta có thể khẳng định rằng thuộc tính
importscủa decorator@Module()không chỉ nhận tên class của module (ví dụimports: [UsersModule]), mà còn nhận một hàm trả về một dynamic module (ví dụimports: [ConfigModule.register(...)]). - Một dynamic module có thể tự import các module khác. Ta sẽ không làm điều đó trong ví dụ này, nhưng nếu dynamic module phụ thuộc vào các provider từ module khác, bạn có thể import chúng bằng thuộc tính
importstùy chọn. Đây cũng hoàn toàn tương tự cách bạn khai báo metadata cho một static module bằng decorator@Module().
Với những hiểu biết này, giờ ta có thể xem khai báo dynamic ConfigModule của mình phải trông như thế nào. Hãy thử làm nhé.
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service.js';
@Module({})
export class ConfigModule {
static register(): DynamicModule {
return {
module: ConfigModule,
providers: [ConfigService],
exports: [ConfigService],
};
}
}
Đến đây chắc bạn đã hình dung rõ cách các phần ghép lại với nhau. Gọi ConfigModule.register(...) sẽ trả về một object DynamicModule với các thuộc tính về cơ bản giống hệt những gì trước đây ta vẫn cung cấp dưới dạng metadata qua decorator @Module().
Gợi ý Import DynamicModule từ @nestjs/common.
Tuy nhiên, dynamic module của ta hiện chưa có gì thú vị lắm, vì ta chưa đưa vào khả năng cấu hình nó như đã nói muốn làm. Hãy giải quyết điều đó tiếp theo.
Cấu hình module
Giải pháp hiển nhiên để tùy biến hành vi của ConfigModule là truyền cho nó một object options trong static method register(), đúng như ta đã đoán ở trên. Hãy xem lại thuộc tính imports của module tiêu thụ:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller.js';
import { AppService } from './app.service.js';
import { ConfigModule } from './config/config.module.js';
@Module({
imports: [ConfigModule.register({ folder: './config' })],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Cách này xử lý gọn gàng việc truyền một object options vào dynamic module của ta. Vậy ta dùng object options đó trong ConfigModule như thế nào? Hãy suy nghĩ một chút về điều này. Ta biết ConfigModule về cơ bản là nơi host để cung cấp và export một service có thể inject — ConfigService — cho các provider khác dùng. Chính ConfigService mới là nơi cần đọc object options để tùy biến hành vi của nó. Giả sử tạm thời ta đã biết cách nào đó để đưa options từ method register() vào ConfigService. Với giả định đó, ta có thể chỉnh sửa service một chút để tùy biến hành vi dựa trên các thuộc tính từ object options. (Lưu ý: vì hiện tại ta chưa thực sự xác định cách truyền nó vào, nên ta sẽ tạm hard-code options. Ta sẽ khắc phục điều này ngay sau đây).
import { Injectable } from '@nestjs/common';
import * as fs from 'node:fs';
import * as path from 'node:path';
import dotenv from 'dotenv';
import type { EnvConfig } from './interfaces.js';
@Injectable()
export class ConfigService {
private readonly envConfig: EnvConfig;
constructor() {
const options = { folder: './config' };
const filePath = `${process.env.NODE_ENV || 'development'}.env`;
const envFile = path.resolve(import.meta.dirname, '../../', options.folder, filePath);
this.envConfig = dotenv.parse(fs.readFileSync(envFile));
}
get(key: string): string {
return this.envConfig[key];
}
}
Giờ ConfigService đã biết cách tìm file .env trong thư mục ta chỉ định trong options.
Nhiệm vụ còn lại là làm sao inject object options từ bước register() vào ConfigService. Và tất nhiên, ta sẽ dùng dependency injection (tiêm phụ thuộc) để làm điều đó. Đây là điểm mấu chốt, hãy chắc chắn bạn hiểu rõ. ConfigModule của ta đang cung cấp ConfigService. Đến lượt mình, ConfigService lại phụ thuộc vào object options chỉ được cung cấp tại run-time. Vậy nên, tại run-time, trước tiên ta cần gắn (bind) object options vào IoC container của Nest, sau đó để Nest inject nó vào ConfigService. Nhớ lại từ chương Custom providers rằng provider có thể chứa bất kỳ giá trị nào, không chỉ service, nên ta hoàn toàn có thể dùng dependency injection để xử lý một object options đơn giản.
Trước tiên hãy giải quyết việc gắn object options vào IoC container. Ta làm việc này trong static method register(). Nhớ rằng ta đang xây dựng động (dynamically constructing) một module, và một trong các thuộc tính của module chính là danh sách provider của nó. Vậy điều ta cần làm là định nghĩa object options như một provider. Điều này sẽ giúp nó có thể inject được vào ConfigService, mà ta sẽ tận dụng ở bước tiếp theo. Trong đoạn code dưới đây, hãy chú ý kỹ mảng providers:
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service.js';
@Module({})
export class ConfigModule {
static register(options: Record<string, any>): DynamicModule {
return {
module: ConfigModule,
providers: [
{
provide: 'CONFIG_OPTIONS',
useValue: options,
},
ConfigService,
],
exports: [ConfigService],
};
}
}
Giờ ta có thể hoàn tất quá trình bằng cách inject provider 'CONFIG_OPTIONS' vào ConfigService. Nhớ rằng khi định nghĩa một provider dùng token không phải class, ta cần dùng decorator @Inject() như đã mô tả ở đây.
import * as fs from 'node:fs';
import * as path from 'node:path';
import dotenv from 'dotenv';
import { Injectable, Inject } from '@nestjs/common';
import type { EnvConfig } from './interfaces.js';
@Injectable()
export class ConfigService {
private readonly envConfig: EnvConfig;
constructor(@Inject('CONFIG_OPTIONS') private options: Record<string, any>) {
const filePath = `${process.env.NODE_ENV || 'development'}.env`;
const envFile = path.resolve(import.meta.dirname, '../../', options.folder, filePath);
this.envConfig = dotenv.parse(fs.readFileSync(envFile));
}
get(key: string): string {
return this.envConfig[key];
}
}
Một lưu ý cuối: để đơn giản ta đã dùng injection token dạng string ('CONFIG_OPTIONS') ở trên, nhưng best practice là định nghĩa nó thành một hằng số (hoặc Symbol) trong một file riêng, rồi import file đó. Ví dụ:
export const CONFIG_OPTIONS = 'CONFIG_OPTIONS';
Ghi chú người dịch Nếu từng làm PHP/Laravel, mẫu provide: 'CONFIG_OPTIONS', useValue: options này khá giống việc bind một giá trị vào service container bằng string key rồi resolve lại bằng key đó — khác biệt là Nest khuyến khích bạn dùng hằng số/Symbol thay vì magic string để tránh gõ nhầm token lúc inject, một lỗi runtime rất dễ gặp và khó debug vì TypeScript không bắt được ở compile-time.
Ví dụ
Bạn có thể xem ví dụ đầy đủ của code trong chương này tại đây.
Quy ước cộng đồng
Có thể bạn đã thấy các method như forRoot, register, và forFeature xuất hiện ở một số package @nestjs/ và tự hỏi sự khác biệt giữa chúng là gì. Không có quy tắc cứng nào cho việc này, nhưng các package @nestjs/ thường cố tuân theo các hướng dẫn sau:
Khi tạo một module với:
-
register, bạn đang mong muốn cấu hình một dynamic module với một cấu hình cụ thể chỉ dùng cho module gọi nó. Ví dụ, với@nestjs/axioscủa Nest:HttpModule.register({ baseUrl: 'someUrl' }). Nếu ở một module khác bạn dùngHttpModule.register({ baseUrl: 'somewhere else' }), nó sẽ có cấu hình khác. Bạn có thể làm vậy cho bao nhiêu module tùy thích. -
forRoot, bạn đang mong muốn cấu hình một dynamic module một lần rồi tái sử dụng cấu hình đó ở nhiều nơi (dù có thể không nhận ra vì nó được ẩn đi/abstracted away). Đây là lý do vì sao bạn chỉ có mộtGraphQLModule.forRoot(), mộtTypeOrmModule.forRoot(), v.v. -
forFeature, bạn đang mong muốn dùng cấu hình từforRootcủa một dynamic module nhưng cần sửa đổi một số cấu hình riêng cho nhu cầu của module gọi nó (ví dụ module này nên truy cập repository nào, hoặc context mà một logger nên dùng.)
Tất cả các method này thường cũng có phiên bản async tương ứng — registerAsync, forRootAsync, và forFeatureAsync — mang cùng ý nghĩa, nhưng dùng Dependency Injection của Nest cho cả phần cấu hình.
Configurable module builder
Vì việc tự tay tạo các dynamic module có khả năng cấu hình cao, expose các method async (registerAsync, forRootAsync, v.v.) khá phức tạp, đặc biệt với người mới, Nest cung cấp class ConfigurableModuleBuilder giúp đơn giản hóa quá trình này, cho phép bạn xây dựng “bản thiết kế” (blueprint) module chỉ trong vài dòng code.
Ví dụ, hãy lấy lại ví dụ ta đã dùng ở trên (ConfigModule) và chuyển nó sang dùng ConfigurableModuleBuilder. Trước khi bắt đầu, hãy đảm bảo ta tạo một interface riêng thể hiện các option mà ConfigModule nhận vào.
export interface ConfigModuleOptions {
folder: string;
}
Với interface này, hãy tạo một file riêng mới (nằm cạnh file config.module.ts hiện có) và đặt tên là config.module-definition.ts. Trong file này, ta sẽ dùng ConfigurableModuleBuilder để xây dựng định nghĩa cho ConfigModule.
import { ConfigurableModuleBuilder } from '@nestjs/common';
import type { ConfigModuleOptions } from './interfaces/config-module-options.interface.js';
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder<ConfigModuleOptions>().build();
import { ConfigurableModuleBuilder } from '@nestjs/common';
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder().build();
Giờ hãy mở file config.module.ts và chỉnh sửa implementation để tận dụng ConfigurableModuleClass được tự động sinh ra:
import { Module } from '@nestjs/common';
import { ConfigService } from './config.service.js';
import { ConfigurableModuleClass } from './config.module-definition.js';
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule extends ConfigurableModuleClass {}
Việc extend ConfigurableModuleClass nghĩa là ConfigModule giờ không chỉ cung cấp method register (như với implementation tự viết trước đây), mà còn cả method registerAsync, cho phép consumer cấu hình module đó bất đồng bộ (asynchronously), chẳng hạn bằng cách cung cấp async factory:
@Module({
imports: [
ConfigModule.register({ folder: './config' }),
// hoặc:
// ConfigModule.registerAsync({
// useFactory: () => {
// return {
// folder: './config',
// }
// },
// inject: [...any extra dependencies...]
// }),
],
})
export class AppModule {}
Method registerAsync nhận một object với các thuộc tính sau làm argument:
{
/**
* Injection token resolving to a class that will be instantiated as a provider.
* The class must implement the corresponding interface.
*/
useClass?: Type<
ConfigurableModuleOptionsFactory<ModuleOptions, FactoryClassMethodKey>
>;
/**
* Function returning options (or a Promise resolving to options) to configure the
* module.
*/
useFactory?: (...args: any[]) => Promise<ModuleOptions> | ModuleOptions;
/**
* Dependencies that a Factory may inject.
*/
inject?: FactoryProvider['inject'];
/**
* Injection token resolving to an existing provider. The provider must implement
* the corresponding interface.
*/
useExisting?: Type<
ConfigurableModuleOptionsFactory<ModuleOptions, FactoryClassMethodKey>
>;
}
Hãy đi qua từng thuộc tính ở trên:
useFactory- một hàm trả về object cấu hình. Hàm này có thể đồng bộ hoặc bất đồng bộ. Để inject dependency vào factory function, dùng thuộc tínhinject. Ta đã dùng biến thể này trong ví dụ ở trên.inject- một mảng dependency sẽ được inject vào factory function. Thứ tự các dependency phải khớp với thứ tự tham số trong factory function.useClass- một class sẽ được khởi tạo (instantiate) làm provider. Class này phải implement interface tương ứng. Thông thường, đây là một class cung cấp methodcreate()trả về object cấu hình. Đọc thêm về điều này ở phần Custom method key bên dưới.useExisting- một biến thể củauseClass, cho phép bạn dùng lại một provider đã có sẵn thay vì yêu cầu Nest tạo một instance mới của class. Điều này hữu ích khi bạn muốn dùng một provider đã được đăng ký trong module. Lưu ý class đó phải implement cùng interface như class dùng vớiuseClass(do đó vẫn phải cung cấp methodcreate(), trừ khi bạn ghi đè tên method mặc định, xem phần Custom method key bên dưới).
Luôn chọn một trong các option trên (useFactory, useClass, hoặc useExisting), vì chúng loại trừ lẫn nhau (mutually exclusive).
Cuối cùng, hãy cập nhật class ConfigService để inject provider option module được tự sinh ra, thay vì 'CONFIG_OPTIONS' mà ta dùng từ trước đến giờ.
@Injectable()
export class ConfigService {
constructor(@Inject(MODULE_OPTIONS_TOKEN) private options: ConfigModuleOptions) { ... }
}
Custom method key (tên method tùy chỉnh)
Mặc định, ConfigurableModuleClass cung cấp method register và method tương ứng registerAsync. Để dùng một tên method khác, hãy dùng method ConfigurableModuleBuilder#setClassMethodName, như sau:
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder<ConfigModuleOptions>().setClassMethodName('forRoot').build();
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder().setClassMethodName('forRoot').build();
Cách khai báo này sẽ chỉ thị cho ConfigurableModuleBuilder sinh ra một class expose forRoot và forRootAsync thay vì register/registerAsync. Ví dụ:
@Module({
imports: [
ConfigModule.forRoot({ folder: './config' }), // <-- chú ý dùng "forRoot" thay vì "register"
// hoặc:
// ConfigModule.forRootAsync({
// useFactory: () => {
// return {
// folder: './config',
// }
// },
// inject: [...any extra dependencies...]
// }),
],
})
export class AppModule {}
Custom options factory class (class factory option tùy chỉnh)
Vì method registerAsync (hoặc forRootAsync hay bất kỳ tên nào khác, tùy vào cấu hình) cho phép consumer truyền vào một provider definition được resolve ra thành cấu hình module, một consumer của thư viện có thể cung cấp một class để dùng cho việc xây dựng object cấu hình.
@Module({
imports: [
ConfigModule.registerAsync({
useClass: ConfigModuleOptionsFactory,
}),
],
})
export class AppModule {}
Mặc định, class này phải cung cấp method create() trả về một object cấu hình module. Tuy nhiên, nếu thư viện của bạn theo một quy ước đặt tên khác, bạn có thể thay đổi hành vi này và chỉ thị cho ConfigurableModuleBuilder mong đợi một method khác, chẳng hạn createConfigOptions, bằng cách dùng method ConfigurableModuleBuilder#setFactoryMethodName:
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder<ConfigModuleOptions>().setFactoryMethodName('createConfigOptions').build();
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder().setFactoryMethodName('createConfigOptions').build();
Giờ đây, class ConfigModuleOptionsFactory phải expose method createConfigOptions (thay vì create):
@Module({
imports: [
ConfigModule.registerAsync({
useClass: ConfigModuleOptionsFactory, // <-- class này phải cung cấp method "createConfigOptions"
}),
],
})
export class AppModule {}
Extra options (option bổ sung)
Có những trường hợp đặc biệt (edge-case) khi module của bạn cần nhận thêm các option quyết định cách nó hoạt động (một ví dụ hay gặp là cờ isGlobal - hay đơn giản là global) mà đồng thời, không nên được đưa vào provider MODULE_OPTIONS_TOKEN (vì chúng không liên quan đến các service/provider được đăng ký trong module đó — ví dụ, ConfigService không cần biết module host của nó có được đăng ký là global module hay không).
Trong những trường hợp như vậy, có thể dùng method ConfigurableModuleBuilder#setExtras. Xem ví dụ sau:
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder<ConfigModuleOptions>()
.setExtras(
{
isGlobal: true,
},
(definition, extras) => ({
...definition,
global: extras.isGlobal,
}),
)
.build();
Trong ví dụ trên, argument đầu tiên truyền vào method setExtras là một object chứa giá trị mặc định cho các thuộc tính “extra”. Argument thứ hai là một hàm nhận vào module definition được tự sinh ra (với provider, exports, v.v.) và object extras đại diện cho các thuộc tính bổ sung (do consumer chỉ định hoặc giá trị mặc định). Giá trị trả về của hàm này là một module definition đã được chỉnh sửa. Trong ví dụ cụ thể này, ta lấy thuộc tính extras.isGlobal và gán nó vào thuộc tính global của module definition (thuộc tính này quyết định module có phải global module hay không, đọc thêm tại đây).
Giờ khi tiêu thụ module này, cờ isGlobal bổ sung có thể được truyền vào như sau:
@Module({
imports: [
ConfigModule.register({
isGlobal: true,
folder: './config',
}),
],
})
export class AppModule {}
Tuy nhiên, vì isGlobal được khai báo là một thuộc tính “extra”, nó sẽ không có mặt trong provider MODULE_OPTIONS_TOKEN:
@Injectable()
export class ConfigService {
constructor(
@Inject(MODULE_OPTIONS_TOKEN) private options: ConfigModuleOptions,
) {
// object "options" sẽ không có thuộc tính "isGlobal"
// ...
}
}
Ghi chú người dịch setExtras giải quyết một vấn đề khá thực tế: có những flag chỉ ảnh hưởng đến cách module được đăng ký (global hay không) chứ không phải cấu hình nghiệp vụ mà service bên trong cần biết. Nếu không tách ra, ConfigService sẽ vô tình nhận cả những field không liên quan đến logic của nó — vi phạm nguyên tắc đóng gói (encapsulation). Đây là điểm dễ bỏ sót nếu bạn tự viết register() thủ công mà không dùng ConfigurableModuleBuilder.
Extending auto-generated methods (mở rộng method tự sinh)
Các static method tự sinh (register, registerAsync, v.v.) có thể được mở rộng nếu cần, như sau:
import { Module } from '@nestjs/common';
import { ConfigService } from './config.service.js';
import {
ConfigurableModuleClass,
ASYNC_OPTIONS_TYPE,
OPTIONS_TYPE,
} from './config.module-definition.js';
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule extends ConfigurableModuleClass {
static register(options: typeof OPTIONS_TYPE): DynamicModule {
return {
// logic tùy chỉnh của bạn ở đây
...super.register(options),
};
}
static registerAsync(options: typeof ASYNC_OPTIONS_TYPE): DynamicModule {
return {
// logic tùy chỉnh của bạn ở đây
...super.registerAsync(options),
};
}
}
Lưu ý việc dùng các kiểu OPTIONS_TYPE và ASYNC_OPTIONS_TYPE, hai kiểu này phải được export từ file module definition:
export const {
ConfigurableModuleClass,
MODULE_OPTIONS_TOKEN,
OPTIONS_TYPE,
ASYNC_OPTIONS_TYPE,
} = new ConfigurableModuleBuilder<ConfigModuleOptions>().build();