iT邦幫忙

2021 iThome 鐵人賽

DAY 5
0
Modern Web

NestJS 帶你飛!系列 第 5

[NestJS 帶你飛!] DAY05 - Module

本系列文已出版成書「NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式」,感謝 iT 邦幫忙與博碩文化的協助。如果對 NestJS 有興趣、覺得這個系列文對你有幫助的話,歡迎前往購書,你的支持是我最大的寫作動力!

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. 善用常用模組的方式來統一管理多個常用模組。

上一篇
[NestJS 帶你飛!] DAY04 - Controller (下)
下一篇
[NestJS 帶你飛!] DAY06 - Provider (上)
系列文
NestJS 帶你飛!32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
gogo.afuzzz
iT邦新手 5 級 ‧ 2021-11-06 23:39:43

我們再透過瀏覽器打開 http://localhost:3000/todos 來查看 Todo 是否有增加

共享模組好像不會共用變數,這邊在 copy-todos 建立 item 後,實際在呼叫 todos 返回的值不太一樣。

HAO iT邦研究生 3 級 ‧ 2021-11-07 11:32:21 檢舉

你好,共享模組的原理就是把 TodoModule 實例化的 TodoService 實例(instance) 共享出去,當 CopyTodoModule 匯入 TodoModule 時,就可以存取到與 TodoModule 相同的 TodoService 實例,所以是會存取同一個陣列的,你可以再確認一下,有問題都歡迎提問/images/emoticon/emoticon12.gif

undefined iT邦新手 5 級 ‧ 2022-04-09 12:17:29 檢舉

根模組需要匯入CopyTodoController

0
AndrewYEE
iT邦新手 3 級 ‧ 2022-11-07 17:12:07

雖然現在留言好像有點晚了,但希望能增加一個是共用Module共用在service的例子,上面例子是直接在copy-todo的controller中直接使用todo的service,但如同您前幾篇介紹,controller非必要,可能比較會用到的還是在service之間的共用。

不過你寫的很詳細清楚,謝謝您。

HAO iT邦研究生 3 級 ‧ 2022-11-07 21:48:12 檢舉

您好,在 CopyTodoModule 裡 import TodoModule 就可以在 CopyTodoModule 的作用域內使用 TodoService,所以假如 CopyTodoModule 有一個 CopyTodoService,它其實也可以注入 TodoService 呦!

AndrewYEE iT邦新手 3 級 ‧ 2022-11-08 11:43:31 檢舉

謝謝您

我要留言

立即登入留言