likes
comments
collection
share

Nest.js中的设计模式——模块化

作者站长头像
站长
· 阅读数 38

Nest是以模块作为应用组件进行开发的,开发时需要注重模块化的编程思想。

We want to emphasize that modules are strongly recommended as an effective way to organize your components.

我们要强调的是,强烈建议将模块作为组织组件的有效方式。

一个简单的Nest模块

Nest.js的模块化用到了面向对象(OOP)依赖注入(DI)的设计方法。

OOP编程

面向对象编程OOP三大要素:封装多态继承

封装就是把抽象的数据和对数据进行的操作封装在一起,意味着OOP也具有模块化的特点,只是封装的服务边界是对象内。

例如:

import { Cat } from './interfaces/cat.interface';

export class CatsService {
  private readonly cats: Cat[] = [];

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

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

依赖注入

依赖注入(dependency injection,DI)的意思为,给予调用方它所需要的事物。

“依赖”是指可被方法调用的事物。依赖注入形式下,调用方不再直接使用“依赖”,取而代之是“注入” 。“注入”是指将“依赖”传递给调用方的过程。在“注入”之后,调用方才会调用该“依赖”。传递依赖给调用方,而不是让让调用方直接获得依赖,这个是该设计的根本需求。

上面讲到的依赖注入是一种控制反转 (IoC)技术,将依赖项的实例化委托给IoC 容器(NestJS中有自己的全局IoC容器)。

注册过程实际上就是取一个IoC容器的token(一般是类的名称),关联到依赖项(实例化对象),这是包含“依赖查找”和“依赖注入”的过程。如果没有找到“依赖项”,Nest将创建一个实例,缓存它,然后返回它,或者如果已经缓存了一个实例,则返回现有实例。

Nest中是如何实现的呢?先看看下面代码

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

@Injectable() // 定义可注入到IoC的依赖项
export class CatsService {
  private readonly cats: Cat[] = [];

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

  findAll(): Cat[] {
    return this.cats;
  }
}
// cat.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(
      private catsService: CatsService // 以CatsService为token查找并注入依赖
  ) {}

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

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}
// app.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';

@Module({
  controllers: [CatsController], // 注册【'CatsController' token -> CatsController实例化对象】
  providers: [CatsService], // 注册【'CatsService' token -> CatsService实例化对象】
})
// 模块入口
export class AppModule {
}

结合看上面的例子,实际上做了三件事情:

  1. cats.service.ts中,@Injectable()装饰器将CatsService类声明为可以由 Nest IoC 容器管理的类
  2. cats.controller.ts,中使用构造函数注入CatsController声明对令牌的依赖:CatsService
  3. app.module.ts中,我们将token与文件CatsService中的类相关联, 也称为注册

下面拓展说一下其他模块化相关的东西,为方便说明,后面依赖项均以Provider名称代替。

一个更复杂的Nest模块

模块的生命周期

我们先来看看Nest的应用生命周期,如图:

Nest.js中的设计模式——模块化

可以看到,Nest提供了onModuleInitonModuleDesctory的hook去定义模块内依赖项在初始化和销毁时的时候可以做的事情。

更灵活的注册

上面例子中是一个最基本的注册方式,还有更灵活的方式可以去进行注册,实际上@Module()中的provider类型定义是这样的

// @nestjs/common/interfaces/modules/module-metadata.interface.ts
export interface ModuleMetadata {
   providers?: Provider[];
   // ...省略了部分代码
}


// @nestjs/common/interfaces/modules/provider.interface.ts
export declare type Provider<T = any> = Type<any> | ClassProvider<T> | ValueProvider<T> | FactoryProvider<T> | ExistingProvider<T>;

可以看到,有四种方式去注册Provider。总结一下:

  • useClass,类的实例化对象作为注册项
  • useValue,注册一个值作为注册项,像一般写测试模块就是用这种方式进行mock
  • useFactory,异步去创建Provider的值
  • useExisting,为注册的provider提供一个别名

动态模块

上面的简单例子中,Module Class里面并没有任何的方法,模块只是定义了模块内的组件,例如Provider和Controller,它们作为整个应用程序的模块化部分组合在一起。称为静态模块。

那么动态模块(DynamicModule)是指什么呢? 使用静态模块被引用,引用的模块没有机会影响被引用模块的配置方式。动态模块就是在使用模块可以使用 API 来控制配置模块在导入时的行为。也就是说在Module Class定义方法去控制能被模块初始化时通过参数影响的行为方法。一般按照规范,方法命名为forRoot()register(),如果是异步方法的就命名为forRootAsync()forRegisterAsync()

综上,可以看到下面的@Module()参数类型定义中,DynamicModule是出现在importexport的类型定义中,并且也是一个ModuleMetadata类型。

export interface ModuleMetadata {
    /**
     * Optional list of imported modules that export the providers which are
     * required in this module.
     */
    imports?: Array<Type<any> | DynamicModule | Promise<DynamicModule> | ForwardReference>;
    /**
     * Optional list of controllers defined in this module which have to be
     * instantiated.
     */
    controllers?: Type<any>[];
    /**
     * Optional list of providers that will be instantiated by the Nest injector
     * and that may be shared at least across this module.
     */
    providers?: Provider[];
    /**
     * Optional list of the subset of providers that are provided by this module
     * and should be available in other modules which import this module.
     */
    exports?: Array<DynamicModule | Promise<DynamicModule> | string | symbol | Provider | ForwardReference | Abstract<any> | Function>;
}

export interface DynamicModule extends ModuleMetadata {
    /**
     * A module reference
     */
    module: Type<any>;
    /**
     * When "true", makes a module global-scoped.
     *
     * Once imported into any module, a global-scoped module will be visible
     * in all modules. Thereafter, modules that wish to inject a service exported
     * from a global module do not need to import the provider module.
     *
     * @default false
     */
    global?: boolean;
}

ModuleRef

ModuleRef模块引用,可以用于操作(get/create)Nest的IoC容器的依赖项,一般是在封装自己的模块时会用到。

// @nestjs/core/injector/module-ref.d.ts
export declare abstract class ModuleRef {
    protected readonly container: NestContainer;
    constructor(container: NestContainer);
    abstract get<TInput = any, TResult = TInput>(typeOrToken: Type<TInput> | string | symbol, options?: {
        strict: boolean;
    }): TResult;
    abstract create<T = any>(type: Type<T>): Promise<T>;
    protected find<TInput = any, TResult = TInput>(typeOrToken: Type<TInput> | string | symbol, contextModule?: Module): TResult;
    // ...省略了部分代码
}

下面是get()的例子

@Injectable()
export class CatsService implements OnModuleInit {
  private service: Service;
  constructor(private moduleRef: ModuleRef) {}

  onModuleInit() {
    this.service = this.moduleRef.get(Service);
  }
}

下面例子是用create()注入在IoC容器中没有的依赖项

@Injectable()
export class CatsService implements OnModuleInit {
  private catsFactory: CatsFactory;
  constructor(private moduleRef: ModuleRef) {}

  async onModuleInit() {
    this.catsFactory = await this.moduleRef.create(CatsFactory);
  }
}

例子

这个是文章内用到的内容较为完整的例子:一个封装了amqp连接rabbitmq的模块

总结

下面是我个人对于Nest模块化设计的一些总结(规范):

  • 不一定是所有代码都要封装成provider,模块内仍然可以使用Node.js原生的模块(Commonjs modules/ECMAScript modules)进行代码组织,衡量标准是看是否需要其他依赖项被依赖
  • @Module()的参数就是注册表
  • 尽量不要将一个Provider在多个Module中注册,这是个代码服务边界的问题,What happen in Module,stay in module,将注册的Provider以export的形式进行服务边界的定义。
  • 模块化组织代码的方式能更好地对代码进行重构和编写核心代码的测试
转载自:https://juejin.cn/post/7090360061045768205
评论
请登录