[NestJS 带你飞!] DAY05 - Module

Module 在 Nest 的世界里是非常重要的成员,它主要是把相同性质的功能包装在一起,并依照各模组的需求来串接,而前面有提过整个 Nest App 必定有一个根模组,Nest 会从根模组架构整个应用。

「把相同性质的功能包装在一起」是什麽意思呢?

以餐厅的例子来说,我们将餐厅分成了台湾美食、日式料理与美式风味三个区块,每个区块都有他们负责的范围,不会有在台湾美食区点日式豚骨拉面的情况,因为台湾美食区只提供台湾道地的美食;换成 Nest 的角度来举例的话,我们有三个模组,分别是:TodoModuleUserModuleAuthModule,正常来说我们不会希望在 UserModule 里面设计可以拿到 Todo 资讯的功能吧?UserModule 就应该只提供与 User 最相关的资源,达到各司其职的功效。

「依照各模组的需求来串接」又是什麽意思呢?

事实上,Module 的功能 不一定 要包含 Controller,它可以只是一个很单纯的功能所包装而成的模组,比如说:MongooseModule。以餐厅的例子来说,我们希望在台湾美食区可以使用「筷子」这个餐具,而日式料理区同样也会使用「筷子」这个餐具,然而在美式风味区就不太适合了,所以把「筷子」视为一个共享的模组,在台湾美食区与日式料理区共用。

建置 Module

所有的 Module 都必须使用 @Module 装饰器来定义。可以用 NestCLI 快速生成 Module:

$ nest generate module <MODULE_NAME>

注意<MODULE_NAME> 可以含有路径,如:features/todo,这样就会在 src 资料夹下建立该路径并含有 Module。

这边我建立了一个名为 todo 的 Module:

$ nest generate module features/todo

提醒:如果先前有按照教学建立 TodoController 的话,可以先移除,这边将会建立新的 Controller。

src/features 底下会看见一个名为 todo 的资料夹,里面有 todo.module.ts
https://ithelp.ithome.com.tw/upload/images/20210313/20119338VciJrHpjiD.png

todo.module.ts 的内容如下:

import { Module } from '@nestjs/common';

@Module({})
export class TodoModule {}

参数介绍

在建立完 Module 後会发现 @Module 装饰器里面只有一个空物件,这是因为 NestCLI 不确定使用者建立该模组的用途为何,所以留空给使用者自行填入。那具体有哪些参数可以使用呢?共有以下四大项目:

  • controllers:将要归纳在该 Module 下的 Controller 放在这里,会在载入该 Module 时实例化它们。
  • providers:将会使用到的 Provider 放在这里,比如说:Service。会在载入该 Module 时实例化它们。
  • exports:在这个 Module 下的部分 Provider 可能会在其他 Module 中使用,此时就可以把这些 Provider 放在这里进行汇出。
  • imports:将其他模组的 Provider 汇入。

提醒:Provider 会在後面篇章做更详细的说明。

功能模组 (Feature Module)

大多数的 Module 都属於功能模组,其概念就是前面一直强调的:把相同性质的功能包装在一起。这边我们就先把 Controller 加到 Module 中,透过指令建立 Controller:

$ nest generate controller <CONTROLLER_NAME>

这边我指定的 <CONTROLLER_NAME>features/todo,会看到 TodoModule 自动汇入了该 Controller 到 controllers 里:

import { Module } from '@nestjs/common';
import { TodoController } from './todo.controller';

@Module({
  controllers: [TodoController]
})
export class TodoModule {}

前面有提过一个含有路由功能的模组通常都有 Controller 与 Service,这边我们先透过指令产生一个 Service,後续会再针对 Service 做说明:

$ nest generate service <SERVICE_NAME>

这边我指定的 <SERVICE_NAME>features/todo,会看到 TodoModule 自动汇入了该 Service 到 providers 里:

import { Module } from '@nestjs/common';
import { TodoController } from './todo.controller';
import { TodoService } from './todo.service';

@Module({
  controllers: [TodoController],
  providers: [TodoService]
})
export class TodoModule {}

稍微修改一下 todo.service.ts 的内容,大致上就是在 TodoService 建立一个 getTodos 方法回传 todos 的资讯:

import { Injectable } from '@nestjs/common';

@Injectable()
export class TodoService {

  private todos: { id: number, title: string, description: string }[] = [
    {
      id: 1,
      title: 'Title 1',
      description: ''
    }
  ];

  getTodos(): { id: number, title: string, description: string }[] {
    return this.todos;
  }

}

然後再修改 todo.controller.ts 的内容,在 TodoControllerconstructor 注入 TodoService

import { Controller, Get } from '@nestjs/common';
import { TodoService } from './todo.service';

@Controller('todos')
export class TodoController {

  constructor(
    private readonly todoService: TodoService
  ) {}

  @Get()
  getAll() {
    return this.todoService.getTodos();
  }

}

这样就完成一个可以作动的功能模组了,那要如何使用它呢?很简单,只要在根模组汇入它就可以了,不过在产生 Module 的时候就自动汇入了,不需要手动去新增,是不是很方便呢!赶快打开浏览器查看 http://localhost:3000/todos
https://ithelp.ithome.com.tw/upload/images/20210314/2011933859rG0JqOSl.png

共享模组 (Shared Module)

在 Nest 的世界里,预设情况下 Module 都是单例的,也就是说可以在各模组间共享同一个实例。事实上,每一个 Module 都算是共享模组,只要遵照设计原则来使用,每个 Module 都具有高度的重用性,这也是前面强调的「依照各模组的需求来串接」。这里我们可以做个简单的验证,把 TodoServiceTodoModule 做汇出:

import { Module } from '@nestjs/common';
import { TodoController } from './todo.controller';
import { TodoService } from './todo.service';

@Module({
  controllers: [TodoController],
  providers: [TodoService],
  exports: [TodoService]
})
export class TodoModule {}

接着,建立一个新的 Module 与 Controller,这里我使用的指令如下:

$ nest generate module features/copy-todo
$ nest generate controller features/copy-todo

这边调整一下 todo.service.ts 的内容,在 TodoService 新增一个 createTodo 的方法:

import { Injectable } from '@nestjs/common';

@Injectable()
export class TodoService {

  private todos: { id: number, title: string, description: string }[] = [
    {
      id: 1,
      title: 'Title 1',
      description: ''
    }
  ];

  getTodos(): { id: number, title: string, description: string }[] {
    return this.todos;
  }

  createTodo(item: { id: number, title: string, description: string }) {
    this.todos.push(item);
  }

}

CopyTodoModule 里汇入 TodoModule

import { Module } from '@nestjs/common';
import { TodoModule } from '../todo/todo.module';
import { CopyTodoController } from './copy-todo.controller';

@Module({
  controllers: [CopyTodoController],
  imports: [TodoModule]
})
export class CopyTodoModule {}

修改 copy-todo.controller.ts 的内容,在 CopyTodoControllerconstructor 注入 TodoService,并建立一个方法来调用 createTodo

import { Body, Controller, Post } from '@nestjs/common';
import { TodoService } from '../todo/todo.service';

@Controller('copy-todos')
export class CopyTodoController {

  constructor(
    private readonly todoService: TodoService
  ) {}

  @Post()
  create(@Body() body: { id: number, title: string, description: string }) {
    this.todoService.createTodo(body);
    return body;
  }

}

透过 Postman 来测试:
https://ithelp.ithome.com.tw/upload/images/20210314/20119338jU1O5MQTd1.png

接着,我们再透过浏览器打开 http://localhost:3000/todos 来查看 Todo 是否有增加:
https://ithelp.ithome.com.tw/upload/images/20210314/20119338lM66SB9pY6.png

这里我们可以得出一个结论,像 Service 这种 Provider 会在 Module 中建立一个实例,当其他模组需要使用该实例时,就可以透过汇出的方式与其他 Module 共享。下方为简单的概念图:
https://ithelp.ithome.com.tw/upload/images/20210320/201193380LmMwXhmGx.png

全域模组 (Global Module)

当有 Module 要与多数 Module 共用时,会一直在各 Module 进行汇入的动作,这时候可以透过提升 Module 为 全域模组,让其他模组不需要汇入也能够使用,只需要在 Module 上再添加一个 @Global 的装饰器即可。以 TodoModule 为例:

import { Module, Global } from '@nestjs/common';
import { TodoController } from './todo.controller';
import { TodoService } from './todo.service';

@Global()
@Module({
  controllers: [TodoController],
  providers: [TodoService],
  exports: [TodoService]
})
export class TodoModule {}

注意:虽然可以透过提升为全域来减少汇入的次数,但非必要情况应少用,这样才是好的设计准则。

常用模组 (Common Module)

这是一种设计技巧,Module 可以不含任何 Controller 与 Provider,只单纯把汇入的 Module 再汇出,这样的好处是可以把多个常用的 Module 集中在一起,其他 Module 要使用的话只需要汇入此 Module 就可以了。下方为范例程序码:

@Module({
  imports: [
    AModule,
    BModule
  ],
  exports: [
    AModule,
    BModule
  ],
})
export class CommonModule {}

小结

Module 在 Nest 是非常重要的角色,特别是有很核心的机制与 Provider 息息相关,下一篇会介绍这个机制,这里就先懒人包一下今天的内容:

  1. Module 把相同性质的功能包装在一起,并依照各模组的需求来串接。
  2. Module 拥有 controllersprovidersimportsexports 四个参数。
  3. 大部分的 Module 都是功能模组,其概念即为「把相同性质的功能包装在一起」。
  4. 每个 Module 都是共享模组,其遵循着「依照各模组的需求来串接」的概念来设计。
  5. 透过共享模组的方式来与其他模组共用同一个实例。
  6. 可以透过全域模组来减少汇入次数,但不该把多数模组做提升,在设计上不是很理想。
  7. 善用常用模组的方式来统一管理多个常用模组。

<<:  Day6-React Hook 篇-useReducer

>>:  [Day 6] Reactive Programming - Java 9(SubmissionPublisher、Processor)

[职场]掌握薪水谈判的秘诀,取得自己应有的报酬 & 完赛感言

如果你是一个有野心且不安於现状的人,薪水谈判是你一定要去做的事情。 在阅读这篇文章前,你可以先思考...

Day 18 - 未知与空值 undefined、null、NaN

前言 今天来讨论另一个容易被忽略的主题,如果要表达「有值」的情况,大家都很熟悉: const sco...

IT铁人DAY 25-Iterator 迭代器模式

  今天要认识的迭代器模式我个人觉得需要多花一点心思,才能够了解它并善用它,程序当中也算是经常使用的...

Laravel 实战经验分享 - Day27 Eloquent 的关联

今天来补充之前没有提到的 Eloquent 关联绑定,我们在 Day7 的时候曾规划过资料库关联,D...