本系列文已出版成書「NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式」,感謝 iT 邦幫忙與博碩文化的協助。如果對 NestJS 有興趣、覺得這個系列文對你有幫助的話,歡迎前往購書,你的支持是我最大的寫作動力!
Module 在 Nest 的世界裡是非常重要的成員,它主要是把相同性質的功能包裝在一起,並依照各模組的需求來串接,而前面有提過整個 Nest App 必定有一個根模組,Nest 會從根模組架構整個應用。
「把相同性質的功能包裝在一起」是什麼意思呢?
以餐廳的例子來說,我們將餐廳分成了臺灣美食、日式料理與美式風味三個區塊,每個區塊都有他們負責的範圍,不會有在臺灣美食區點日式豚骨拉麵的情況,因為臺灣美食區只提供臺灣道地的美食;換成 Nest 的角度來舉例的話,我們有三個模組,分別是:TodoModule
、UserModule
與 AuthModule
,正常來說我們不會希望在 UserModule
裡面設計可以拿到 Todo 資訊的功能吧?UserModule
就應該只提供與 User 最相關的資源,達到各司其職的功效。
「依照各模組的需求來串接」又是什麼意思呢?
事實上,Module 的功能 不一定 要包含 Controller,它可以只是一個很單純的功能所包裝而成的模組,比如說:MongooseModule
。以餐廳的例子來說,我們希望在臺灣美食區可以使用「筷子」這個餐具,而日式料理區同樣也會使用「筷子」這個餐具,然而在美式風味區就不太適合了,所以把「筷子」視為一個共享的模組,在臺灣美食區與日式料理區共用。
所有的 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
:
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 會在後面篇章做更詳細的說明。
大多數的 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
的內容,在 TodoController
的 constructor
注入 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 :
在 Nest 的世界裡,預設情況下 Module 都是單例的,也就是說可以在各模組間共享同一個實例。事實上,每一個 Module 都算是共享模組,只要遵照設計原則來使用,每個 Module 都具有高度的重用性,這也是前面強調的「依照各模組的需求來串接」。這裡我們可以做個簡單的驗證,把 TodoService
從 TodoModule
做匯出:
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
的內容,在 CopyTodoController
的 constructor
注入 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 來測試:
接著,我們再透過瀏覽器打開 http://localhost:3000/todos 來查看 Todo 是否有增加:
這裡我們可以得出一個結論,像 Service 這種 Provider 會在 Module 中建立一個實例,當其他模組需要使用該實例時,就可以透過匯出的方式與其他 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 {}
注意:雖然可以透過提升為全域來減少匯入的次數,但非必要情況應少用,這樣才是好的設計準則。
這是一種設計技巧,Module 可以不含任何 Controller 與 Provider,只單純把匯入的 Module 再匯出,這樣的好處是可以把多個常用的 Module 集中在一起,其他 Module 要使用的話只需要匯入此 Module 就可以了。下方為範例程式碼:
@Module({
imports: [
AModule,
BModule
],
exports: [
AModule,
BModule
],
})
export class CommonModule {}
Module 在 Nest 是非常重要的角色,特別是有很核心的機制與 Provider 息息相關,下一篇會介紹這個機制,這裡就先懶人包一下今天的內容:
controllers
、providers
、imports
與 exports
四個參數。我們再透過瀏覽器打開 http://localhost:3000/todos 來查看 Todo 是否有增加
共享模組好像不會共用變數,這邊在 copy-todos
建立 item 後,實際在呼叫 todos
返回的值不太一樣。
你好,共享模組的原理就是把 TodoModule
實例化的 TodoService
實例(instance) 共享出去,當 CopyTodoModule
匯入 TodoModule
時,就可以存取到與 TodoModule
相同的 TodoService
實例,所以是會存取同一個陣列的,你可以再確認一下,有問題都歡迎提問
根模組需要匯入CopyTodoController
雖然現在留言好像有點晚了,但希望能增加一個是共用Module共用在service的例子,上面例子是直接在copy-todo的controller中直接使用todo的service,但如同您前幾篇介紹,controller非必要,可能比較會用到的還是在service之間的共用。
不過你寫的很詳細清楚,謝謝您。
您好,在 CopyTodoModule
裡 import TodoModule
就可以在 CopyTodoModule
的作用域內使用 TodoService
,所以假如 CopyTodoModule
有一個 CopyTodoService
,它其實也可以注入 TodoService
呦!
謝謝您