Nest cung cấp class ModuleRef để duyệt qua danh sách nội bộ các provider và lấy về reference đến bất kỳ provider nào bằng injection token của nó làm khóa tra cứu. Class ModuleRef cũng cung cấp cách khởi tạo động cả provider tĩnh lẫn provider có scope (scoped provider). ModuleRef có thể được inject vào một class theo cách thông thường:
@Injectable()
export class CatsService {
constructor(private moduleRef: ModuleRef) {}
}
@Injectable()
@Dependencies(ModuleRef)
export class CatsService {
constructor(moduleRef) {
this.moduleRef = moduleRef;
}
}
Gợi ý Class ModuleRef được import từ package @nestjs/core.
Lấy instance
Instance ModuleRef (từ đây ta sẽ gọi nó là module reference) có method get(). Mặc định, method này trả về một provider, controller, hoặc injectable (ví dụ guard, interceptor, v.v.) đã được đăng ký và khởi tạo trong module hiện tại, dựa trên injection token/tên class của nó. Nếu không tìm thấy instance, một exception sẽ được ném ra (throw).
@Injectable()
export class CatsService implements OnModuleInit {
private service: Service;
constructor(private moduleRef: ModuleRef) {}
onModuleInit() {
this.service = this.moduleRef.get(Service);
}
}
@Injectable()
@Dependencies(ModuleRef)
export class CatsService {
constructor(moduleRef) {
this.moduleRef = moduleRef;
}
onModuleInit() {
this.service = this.moduleRef.get(Service);
}
}
Cảnh báo Bạn không thể lấy các provider có scope (transient hoặc request-scoped) bằng method get(). Thay vào đó, hãy dùng kỹ thuật được mô tả bên dưới. Tìm hiểu cách kiểm soát scope tại đây.
Ghi chú người dịch Lý do get() không lấy được scoped provider là vì các provider dạng DEFAULT (singleton) chỉ có một instance duy nhất tồn tại sẵn trong container nên get() chỉ việc tra bảng và trả về; còn transient/request-scoped provider chưa tồn tại sẵn — mỗi lần cần dùng phải tạo mới (hoặc theo sub-tree), nên bắt buộc phải qua resolve() (bất đồng bộ) thay vì get() (đồng bộ). Đây là điểm khác biệt so với Angular’s Injector.get() nếu bạn từng quen mô hình DI tương tự.
Để lấy một provider từ global context (ví dụ, nếu provider đó đã được inject ở một module khác), hãy truyền option { strict: false } làm argument thứ hai cho get().
this.moduleRef.get(Service, { strict: false });
Resolving scoped providers (resolve provider có scope)
Để resolve động một provider có scope (transient hoặc request-scoped), hãy dùng method resolve(), truyền injection token của provider làm argument.
@Injectable()
export class CatsService implements OnModuleInit {
private transientService: TransientService;
constructor(private moduleRef: ModuleRef) {}
async onModuleInit() {
this.transientService = await this.moduleRef.resolve(TransientService);
}
}
@Injectable()
@Dependencies(ModuleRef)
export class CatsService {
constructor(moduleRef) {
this.moduleRef = moduleRef;
}
async onModuleInit() {
this.transientService = await this.moduleRef.resolve(TransientService);
}
}
Method resolve() trả về một instance riêng biệt (unique) của provider, lấy từ DI container sub-tree (cây con) của riêng nó. Mỗi sub-tree có một context identifier (định danh ngữ cảnh) riêng biệt. Do đó, nếu bạn gọi method này nhiều lần và so sánh các reference instance, bạn sẽ thấy chúng không bằng nhau.
@Injectable()
export class CatsService implements OnModuleInit {
constructor(private moduleRef: ModuleRef) {}
async onModuleInit() {
const transientServices = await Promise.all([
this.moduleRef.resolve(TransientService),
this.moduleRef.resolve(TransientService),
]);
console.log(transientServices[0] === transientServices[1]); // false
}
}
@Injectable()
@Dependencies(ModuleRef)
export class CatsService {
constructor(moduleRef) {
this.moduleRef = moduleRef;
}
async onModuleInit() {
const transientServices = await Promise.all([
this.moduleRef.resolve(TransientService),
this.moduleRef.resolve(TransientService),
]);
console.log(transientServices[0] === transientServices[1]); // false
}
}
Để tạo một instance duy nhất dùng chung qua nhiều lần gọi resolve(), và đảm bảo chúng dùng chung một DI container sub-tree được tạo ra, bạn có thể truyền một context identifier vào method resolve(). Dùng class ContextIdFactory để tạo context identifier. Class này cung cấp method create() trả về một định danh riêng biệt phù hợp.
@Injectable()
export class CatsService implements OnModuleInit {
constructor(private moduleRef: ModuleRef) {}
async onModuleInit() {
const contextId = ContextIdFactory.create();
const transientServices = await Promise.all([
this.moduleRef.resolve(TransientService, contextId),
this.moduleRef.resolve(TransientService, contextId),
]);
console.log(transientServices[0] === transientServices[1]); // true
}
}
@Injectable()
@Dependencies(ModuleRef)
export class CatsService {
constructor(moduleRef) {
this.moduleRef = moduleRef;
}
async onModuleInit() {
const contextId = ContextIdFactory.create();
const transientServices = await Promise.all([
this.moduleRef.resolve(TransientService, contextId),
this.moduleRef.resolve(TransientService, contextId),
]);
console.log(transientServices[0] === transientServices[1]); // true
}
}
Gợi ý Class ContextIdFactory được import từ package @nestjs/core.
Ghi chú người dịch Cơ chế “DI container sub-tree” này khá xa lạ nếu bạn quen kiểu inject thủ công qua constructor của Go — ở đây Nest tự quản lý một cây instance riêng cho mỗi contextId, đặc biệt hữu ích với request-scoped provider: mỗi request tạo ra một sub-tree mới, đảm bảo các provider request-scoped trong cùng một request chia sẻ đúng một instance, còn giữa các request thì hoàn toàn tách biệt (không đụng state của nhau).
Đăng ký provider REQUEST
Các context identifier được tạo thủ công (bằng ContextIdFactory.create()) đại diện cho các DI sub-tree mà trong đó provider REQUEST sẽ là undefined, vì chúng không được khởi tạo và quản lý bởi hệ thống dependency injection của Nest.
Để đăng ký một object REQUEST tùy chỉnh cho một DI sub-tree được tạo thủ công, hãy dùng method ModuleRef#registerRequestByContextId(), như sau:
const contextId = ContextIdFactory.create();
this.moduleRef.registerRequestByContextId(/* YOUR_REQUEST_OBJECT */, contextId);
Lấy sub-tree hiện tại
Đôi khi, bạn có thể muốn resolve một instance của provider request-scoped bên trong một request context. Giả sử CatsService là request-scoped và bạn muốn resolve instance CatsRepository, vốn cũng được đánh dấu là provider request-scoped. Để dùng chung cùng một DI container sub-tree, bạn phải lấy context identifier hiện tại thay vì tạo một cái mới (ví dụ, bằng hàm ContextIdFactory.create() như đã thấy ở trên). Để lấy context identifier hiện tại, trước tiên hãy inject request object bằng decorator @Inject().
@Injectable()
export class CatsService {
constructor(
@Inject(REQUEST) private request: Record<string, unknown>,
) {}
}
@Injectable()
@Dependencies(REQUEST)
export class CatsService {
constructor(request) {
this.request = request;
}
}
Gợi ý Tìm hiểu thêm về request provider tại đây.
Giờ, dùng method getByRequest() của class ContextIdFactory để tạo một context id dựa trên request object, rồi truyền nó vào lệnh gọi resolve():
const contextId = ContextIdFactory.getByRequest(this.request);
const catsRepository = await this.moduleRef.resolve(CatsRepository, contextId);
Khởi tạo động các class tùy chỉnh
Để khởi tạo động một class chưa từng được đăng ký làm provider, hãy dùng method create() của module reference.
@Injectable()
export class CatsService implements OnModuleInit {
private catsFactory: CatsFactory;
constructor(private moduleRef: ModuleRef) {}
async onModuleInit() {
this.catsFactory = await this.moduleRef.create(CatsFactory);
}
}
@Injectable()
@Dependencies(ModuleRef)
export class CatsService {
constructor(moduleRef) {
this.moduleRef = moduleRef;
}
async onModuleInit() {
this.catsFactory = await this.moduleRef.create(CatsFactory);
}
}
Kỹ thuật này cho phép bạn khởi tạo có điều kiện (conditionally instantiate) các class khác nhau ở bên ngoài framework container.