依赖注入与模块

依赖注入与模块

Nest 和传统的 MVC 框架的区别在于它更注重于后端部分(控制器、服务与数据)的架构,视图层相对比较独立,完全可以由用户自定义配置。Nest 的分层借鉴自 Spring,更细化。随着代码库的增长 MVC 模式中 Modal 和 Controller 会变得含糊不清,导致难于维护。

Nest vs MVC

Provider 主要的设计理念来自于控制反转(Inversion of Control,简称 IOC1)模式中的依赖注入(Dependency Injection)特性。使用 @Injectable() 装饰的类就是一个 Provider,装饰器方法会优先于类被解析执行。

Service

我们可以自己实现一个名叫 CatsService 的 Service,得益于 TypeScript 类型,Nest 可以通过 CatsService 类型查找到 catsService,依赖被查找并传入到控制器的构造函数中。

export interface Cat {
  name: string;
  age: number;
  breed: string;
}

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

  create(cat: Cat) {
    this.cats.push(cat);
  }

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

有了 Service 我们就可以在控制器中注入并引用到它了:

@Controller("cats")
export class CatsController {
  constructor(private readonly catsService: CatsService) {}
  // 等同于
  private readonly catsService: CatsService;
  constructor(catsService: CatsService) {
    this.catsService = catsService;
  }

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

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

你能发现 Controller 和 Service 处于完全解耦的状态:Controller 做的事情仅仅是接收请求,并在合适的时候调用到 Service,至于 Service 内部怎么实现的 Controller 完全不在乎。这样以来有两个好处:

  • 其一,Controller 和 Service 的职责边界很清晰,不存在灰色地带;
  • 其二,各自只关注自身职责涉及的功能,比方说 Service 通常来写业务逻辑,但它也仅仅只与业务相关。

当然你可能会觉得这很理想,时间长了增加了诸如缓存、验证等逻辑后,代码最终会变得无比庞大而难于维护。事实上这也是一个框架应该考虑和抽象出来的,后续 Nest 会有一系列的解决方法,但目前为至我们只需要了解到 Controller 和 Service 的设计原理即可。

Providers

注入作用域

Providers 有一个和应用程序一样的生命周期。当应用启动,每个依赖都必须被获取到。

自定义的 Providers

Nest 有一个内置的 IOC 容器,用来解析 Providers 之间的关系。这个功能相对于 DI 来讲更底层,但是功能却异常强大,@Injectable() 只是冰山一角。事实上,你可以使用值,类和同步或者异步的工厂。

可选的 Providers

有时候,你可以会需要一个依赖,但是这个依赖并不需要一定被容器解析出来。比如我们通常会传入一个配置对象,但是如果不传会使用一个默认值代替。可以使用 @Optional() 来装饰一个非必选的参数。

@Injectable()
export class HttpService<T> {
  constructor(
    @Optional()
    @Inject("HTTP_OPTIONS")
    private readonly httpClient: T
  ) {}
}

基于属性的注入

前面我们提过了 Nest 实现注入是基于类的构造函数的,但是在一些特殊情况下,基于属性的注入会特别有用。比如一个顶层的类依赖一个或多个 Providers 时,通过在子类的构造函数中调用 super() 方法并不是很优雅,为了避免这种情况我们可以在属性上使用 @Inject() 装饰器。

@Injectable()
export class HttpService<T> {
  @Inject("HTTP_OPTIONS")
  private readonly httpClient: T;
}

注册 Provider

一般来讲控制器就是 Service 的消费(使用)者,我们需要将这些 Service 注册到 Nest 上,这样就可以让 Nest 帮你完成注入操作。通常我们会使用 @Module 装饰器来完成注册的过程。

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

模块

模块(Module)是一个使用了 @Module() 装饰的类。@Module() 装饰器提供了一些 Nest 需要使用的元数据,用来组织应用程序的结构。

Module 示意图

每个应用都至少有一个根模块,根模块就是 Nest 应用的入口。Nest 会从这里查找出整个应用的依赖/调用图。@Module() 装饰器接收一个参数对象,有以下取值:

名称 功能
providers 可以被 Nest 的注入器初始化的 providers,至少会在此模块中共享
controllers 这个模块需要用到的控制器集合
imports 引入的其它模块集合
exports 此模块提供的 providers 的子集,其它模块引入此模块时可用

模块默认会封装 providers,如果要在不同模块之间共享 provider 可以在 exports 参数中指定。

功能模块

使用下面的代码可以将相关的控制器和 Service 包装成一个模块:

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

共享的模块

在 Nest 中模块默认是单例的,因此你可在不同的模块之间共享任意 Provider 实例。

共享的模块

模块都是共享的,我们可以通过导出当前模块的指定 Service 来实现其它模块对 Service 的复用。

@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService] // 导出
})
export class CatsModule {}

全局模块

当一些模块在你的应用频繁使用时,可以使用全局模块来避免每次都要调用的问题。Angular 会把 provider 注册到全局作用域上,然而 Nest 会默认将 provider 注册到模块作用域上。如果你没有显式地导出模块的 provider,那么其它地方就无法使用它。如果你想让一个模块随处可见,那就使用 @Global() 装饰器来装饰这个模块。

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

@Global() 装饰器可以让模块获得全局作用域,譬如我们有全局的配置模块,其关联的服务于模块声明如下:

@Injectable()
export class ConfigService {}

@Global()
@Module({
  providers: [
    {
      provide: ConfigService,
      useValue: new ConfigService()
    }
  ],
  exports: [ConfigService]
})
export class ConfigModule {}

App Module 定义如下:

@Module({
  imports: [ConfigModule, FeatureModule],
  controllers: [AppController],
  providers: [AppService]
})
export class AppModule {}

在其他的模块服务中,我们可以直接依赖于该服务:

@Injectable()
export class FeatureService {
  constructor(private readonly configService: ConfigService) {}
}

动态模块

Nest 模块系统支持动态模块的功能,这将让自定义模块的开发变得容易。

import { Module, DynamicModule } from "@nestjs/common";
import { createDatabaseProviders } from "./database.providers";
import { Connection } from "./connection.provider";

@Module({
  providers: [Connection]
})
export class DatabaseModule {
  static forRoot(entities = [], options?): DynamicModule {
    const providers = createDatabaseProviders(options, entities);
    return {
      module: DatabaseModule,
      providers: providers,
      exports: providers
    };
  }
}

模块的静态方法 forRoot 返回一个动态模块,可以是同步或者异步模块。

上一页