Nguyên tắc cơ bản – NestJS

Custom providers

Trong các chương trước, chúng ta đã đề cập đến các khía cạnh khác nhau của Dependency Injection (DI) và cách nó được sử dụng trong Nest. Một ví dụ về điều này là dependency injection dựa trên phương thức khởi tạo  constructor based  được sử dụng để inject các instances (thường là các service providers) vào các lớp. Bạn sẽ không ngạc nhiên khi biết rằng Dependency Injection được tích hợp vào lõi Nest một cách cơ bản. Cho đến nay, chúng tôi mới chỉ khám phá một mẫu chính. Khi ứng dụng của bạn ngày càng phức tạp, bạn có thể cần phải tận dụng các tính năng đầy đủ của hệ thống DI, vì vậy hãy cùng khám phá chúng chi tiết hơn.

DI fundamentals

Dependency injection là một kỹ thuật inversion of control (IoC), trong đó bạn ủy quyền việc khởi tạo các phần phụ thuộc vào vùng chứa IoC (trong trường hợp của chúng tôi là hệ thống thời gian chạy NestJS), thay vì thực hiện nó trong mã của riêng bạn một cách cấp bậc. Hãy xem điều gì đang xảy ra trong ví dụ này từ chương  Providers chapter.

Đầu tiên, chúng tôi xác định một provider. @Injectable() decorator đánh dấu lớp CatsService là một provider.


import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  findAll(): Cat[] {
    return this.cats;
  }
}

Sau đó, chúng tôi yêu cầu Nest inject provider vào lớp controller của chúng tôi:

cats.controller.ts

import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

Cuối cùng, chúng tôi đăng ký provider với Nest IoC container:

app.module.ts

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class AppModule {}

Chính xác thì điều gì đang xảy ra dưới vỏ bọc để làm cho điều này thành công? Có ba bước chính trong quy trình:

  1. Trong cats.service.ts@Injectable() decorator khai báo lớp CatsService là lớp có thể được quản lý bởi Nest IoC .
  2. ITrong cats.controller.tsCatsController khai báo dependency vào token CatsService với inject hàm tạo:
  constructor(private catsService: CatsService)

3. In app.module.ts, chúng tôi liên kết token CatsService với lớp CatsService từ tệp cat.service.ts. Dưới đây chúng tôi sẽ xem chính xác cách thức liên kết này (còn gọi là đăng ký) xảy ra.

Khi vùng chứa Nest IoC khởi tạo CatsController, trước tiên nó sẽ tìm kiếm bất kỳ  dependencies*. Khi tìm thấy phần phụ thuộc CatsService, nó sẽ thực hiện tra cứu token CatsService, mã này trả về lớp CatsService, theo bước đăng ký (# 3 ở trên). Giả sử phạm vi SINGLETON (hành vi mặc định), Nest sau đó sẽ tạo một instance của CatsService, lưu vào bộ nhớ cache và trả về nó hoặc nếu một đối tượng đã được lưu trong bộ nhớ cache, hãy trả về instance hiện có.

Giải thích này được đơn giản hóa một chút để minh họa quan điểm. Một lĩnh vực quan trọng mà chúng tôi đề cập đến là quá trình phân tích mã cho các dependencies rất phức tạp và xảy ra trong quá trình khởi động ứng dụng. Một tính năng chính là phân tích dependencies (hoặc “tạo biểu đồ phụ thuộc”), có tính bắc cầu. Trong ví dụ trên, nếu bản thân CatsService có các dependencies, chúng cũng sẽ được giải quyết. Biểu đồ phụ thuộc đảm bảo rằng các phụ thuộc được giải quyết theo đúng thứ tự – về cơ bản là “từ dưới lên”. Cơ chế này giúp nhà phát triển không phải quản lý các biểu đồ phụ thuộc phức tạp như vậy.

Standard providers

Hãy xem xét kỹ hơn @Module() decorator. Trong app.module, chúng tôi khai báo:

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})

Thuộc tính providers có 1 mảng providers. Cho đến nay, chúng tôi đã cung cấp providers đó thông qua danh sách các tên lớp. Trên thực tế, cú pháp providers: [CatsService] ngắn gọn cho cú pháp hoàn chỉnh hơn:

providers: [
  {
    provide: CatsService,
    useClass: CatsService,
  },
];

Bây giờ chúng tôi thấy cấu trúc rõ ràng này, chúng tôi có thể hiểu quy trình đăng ký. Ở đây, rõ ràng chúng tôi đang liên kết token CatsService với lớp CatsService. Ký hiệu viết tắt chỉ là một sự tiện lợi để đơn giản hóa trường hợp sử dụng phổ biến nhất, trong đó token được sử dụng để request một instance của một lớp có cùng tên.

Custom providers

Điều gì xảy ra khi các yêu cầu của bạn vượt quá những yêu cầu do các Standard providers đưa ra? Đây là vài ví dụ:

  • Bạn muốn tạo instance tùy chỉnh của nội dung thay vì có Nest tức thời (hoặc trả về instance được lưu trong bộ nhớ cache của) một lớp
  • Bạn muốn sử dụng lại một lớp hiện có trong dependency thứ hai
  • Bạn muốn ghi đè một lớp bằng phiên bản giả để testing

Nest cho phép bạn xác định các providers tùy chỉnh để xử lý các trường hợp này. Nó cung cấp một số cách để xác định các providers tùy chỉnh. Hãy thử qua chúng.

Value providers: useValue

Cú pháp useValue hữu ích để inject vào một giá trị không đổi, đưa một thư viện bên ngoài vào vùng chứa Nest hoặc thay thế một triển khai thực bằng một đối tượng giả. Giả sử bạn muốn buộc Nest sử dụng CatsService giả cho mục đích thử nghiệm.

import { CatsService } from './cats.service';

const mockCatsService = {
  /* mock implementation
  ...
  */
};

@Module({
  imports: [CatsModule],
  providers: [
    {
      provide: CatsService,
      useValue: mockCatsService,
    },
  ],
})
export class AppModule {}

Trong ví dụ này, token CatsService sẽ phân giải thành đối tượng giả mockCatsService. useValue yêu cầu một giá trị – trong trường hợp này là một đối tượng theo nghĩa đen có giao diện giống với lớp CatsService mà nó đang thay thế. Do kiểu gõ cấu trúc của TypeScript, bạn có thể sử dụng bất kỳ đối tượng nào có interface tương thích, bao gồm một đối tượng theo nghĩa đen hoặc một instance lớp được khởi tạo bằng new.

Non-class-based provider tokens

Cho đến nay, chúng tôi đã sử dụng tên lớp làm tokens provider của mình (giá trị của thuộc tính provide trong provider được liệt kê trong mảng provides). Điều này phù hợp với mẫu chuẩn được sử dụng với phương thức tiêm dựa trên phương thức khởi tạo, trong đó token cũng là một tên lớp. (Tham khảo lại  DI Fundamentals để biết thêm về tokens nếu khái niệm này không hoàn toàn rõ ràng). Đôi khi, chúng tôi có thể muốn sự linh hoạt khi sử dụng chuỗi hoặc ký hiệu làm token DI. Ví dụ:

import { connection } from './connection';

@Module({
  providers: [
    {
      provide: 'CONNECTION',
      useValue: connection,
    },
  ],
})
export class AppModule {}

Trong ví dụ này, chúng tôi đang liên kết một token có giá trị chuỗi (‘CONNECTION’) với một đối tượng connection đã có từ trước mà chúng tôi đã nhập từ một tệp bên ngoài.

NOTICE Ngoài việc sử dụng chuỗi làm giá trị token, bạn cũng có thể sử dụng JavaScript symbols or TypeScript enums.

Trước đây chúng ta đã biết cách inject a provider bằng cách sử dụng mẫu constructor based injection. Mẫu này yêu cầu phần dependency phải được khai báo với một tên lớp. Provider tùy chỉnh ‘CONNECTION’ sử dụng token có giá trị chuỗi. Hãy xem làm thế nào để inject một provider như vậy. Để làm như vậy, chúng tôi sử dụng @Inject() decorator. Decorator này nhận một đối số duy nhất – token.

HINT @Inject() decorator được dùng từ gói @nestjs/common.

Mặc dù chúng tôi trực tiếp sử dụng chuỗi ‘CONNECTION’ trong các ví dụ trên cho mục đích minh họa, để tổ chức mã sạch, cách tốt nhất là xác định tokens trong một tệp riêng biệt, chẳng hạn như constants.ts. Đối xử với chúng nhiều như bạn làm với các ký hiệu hoặc enums được xác định trong tệp riêng của chúng và được nhập khi cần thiết.

Class providers: useClass

Cú pháp useClass cho phép bạn xác định động một lớp mà token sẽ phân giải. Ví dụ, giả sử chúng ta có một lớp ConfigService trừu tượng (hoặc mặc định). Tùy thuộc vào môi trường hiện tại, chúng tôi muốn Nest cung cấp cách triển khai dịch vụ cấu hình khác nhau. Đoạn mã sau thực hiện một chiến lược như vậy.

const configServiceProvider = {
  provide: ConfigService,
  useClass:
    process.env.NODE_ENV === 'development'
      ? DevelopmentConfigService
      : ProductionConfigService,
};

@Module({
  providers: [configServiceProvider],
})
export class AppModule {}

Hãy xem xét một vài chi tiết trong mẫu mã này. Bạn sẽ nhận thấy rằng chúng tôi xác định configServiceProvider với một đối tượng chữ trước, sau đó truyền nó vào thuộc tính providers của decorator’s mô-đun. Đây chỉ là một chút tổ chức mã, nhưng về mặt chức năng tương đương với các ví dụ mà chúng ta đã sử dụng cho đến nay trong chương này.

Ngoài ra, chúng tôi đã sử dụng tên lớp ConfigService làm token của chúng tôi. Đối với bất kỳ lớp nào phụ thuộc vào ConfigService, Nest sẽ inject một instance của lớp được provided (DevelopmentConfigService hoặc ProductionConfigService) ghi đè bất kỳ triển khai mặc định nào có thể đã được khai báo ở nơi khác (ví dụ: một ConfigService được khai báo với @Injectable() decorator).

Factory providers: useFactory

Cú pháp useFactory cho phép tạo động các providers. Provider thực tế sẽ được cung cấp bởi giá trị trả về từ một function của factory. Factory function có thể đơn giản hoặc phức tạp nếu cần. Một factory đơn giản có thể không phụ thuộc vào bất kỳ provider nào khác. Một factory phức tạp hơn có thể tự inject các provider khác mà nó cần để tính toán kết quả của nó. Đối với trường hợp thứ hai, cú pháp của provider gốc có một cặp cơ chế liên quan:

  1. Factory function có thể chấp nhận các đối số (tùy chọn).
  2. Thuộc tính inject (tùy chọn) chấp nhận một mảng các providers mà Nest sẽ phân giải và truyền làm đối số cho factory function trong quá trình khởi tạo. Hai danh sách phải tương quan với nhau: Nest sẽ chuyển các instances từ danh sách inject làm đối số cho factory function theo cùng một thứ tự.

Ví dụ dưới đây chứng minh điều này.

const connectionFactory = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider: OptionsProvider) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [OptionsProvider],
};

@Module({
  providers: [connectionFactory],
})
export class AppModule {}

Alias providers: useExisting

Cú pháp useExisting cho phép bạn tạo aliases cho các providers hiện có. Điều này tạo ra hai cách để truy cập cùng một provider. Trong ví dụ bên dưới, token (dựa trên chuỗi) ‘AliasedLoggerService‘ là bí danh cho token (dựa trên lớp) LoggerService. Giả sử chúng ta có hai dependencies khác nhau, một cho ‘AliasedLoggerService‘ và một cho LoggerService. Nếu cả hai phần dependencies đều được chỉ định với phạm vi SINGLETON, cả hai đều sẽ giải quyết cho cùng một instance.

@Injectable()
class LoggerService {
  /* implementation details */
}

const loggerAliasProvider = {
  provide: 'AliasedLoggerService',
  useExisting: LoggerService,
};

@Module({
  providers: [LoggerService, loggerAliasProvider],
})
export class AppModule {}

Non-service based providers

Trong khi các providers thường cung cấp services, họ không giới hạn việc sử dụng đó. Một provider có thể cung cấp bất kỳ giá trị nào. Ví dụ: một provider có thể cung cấp một loạt các đối tượng cấu hình dựa trên môi trường hiện tại, như được hiển thị bên dưới:

const configFactory = {
  provide: 'CONFIG',
  useFactory: () => {
    return process.env.NODE_ENV === 'development' ? devConfig : prodConfig;
  },
};

@Module({
  providers: [configFactory],
})
export class AppModule {}

Export custom provider

Giống như bất kỳ provider nào, provider tùy chỉnh được xác định phạm vi đến mô-đun khai báo của nó. Để hiển thị nó với các mô-đun khác, nó phải được exported. Để xuất một provider tùy chỉnh, chúng tôi có thể sử dụng token của nó hoặc đối tượng provider đầy đủ.

Ví dụ sau cho thấy xuất bằng token:

const connectionFactory = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider: OptionsProvider) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [OptionsProvider],
};

@Module({
  providers: [connectionFactory],
  exports: ['CONNECTION'],
})
export class AppModule {}

Ngoài ra, export với đối tượng provider đầy đủ:

const connectionFactory = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider: OptionsProvider) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [OptionsProvider],
};

@Module({
  providers: [connectionFactory],
  exports: [connectionFactory],
})
export class AppModule {}

Leave a Reply

%d bloggers like this: