# nestjs 介紹與實作 day 3 - 介紹 nestjs Module 元件 ## 目標 1. 介紹 nestjs 核心元件 Module 2. 說明 nestjs 建構與分享元件的設計概念 ## nestjs 建構原理 ### 說明 nestjs 與一般 nodejs 程式概念一樣都是把邏輯透過 module 來把邏輯做模組化 在程式啟動點,把這些模組透過工廠模式建構載入設定 當所有元建構都被初始化之後,才正式啟動服務 ### 範例 使用 nestjs cli 建構一個 mountain_climb 專案 ```shell= nest new mountain_climb ``` 長出來的專案結構如下: ![](https://hackmd.io/_uploads/rJRvyesC3.png) 然後找尋 package.json 察看 entry point 可能在 script 的點 發現 ![](https://hackmd.io/_uploads/HyhozejRn.png) 從 start:prod 看起來 有可能是 main.ts 這個檔案 但是實際上是怎麼鏈結 需要看一下 nest-cli.json 這個檔案 打開一看 ![](https://hackmd.io/_uploads/rkqMXgjR2.png) 發現 , holy 媽祖!根本沒特別指定 這時候就可以查看官網 關於 [nestjs cli 設定章節](https://docs.nestjs.com/cli/monorepo#workspace-projects) 會發現有寫一行很隱諱的寫著預設 application 需要一個 main.ts 所以預設的 entry point 就是 main.ts 雖然說打開 main.ts 也可以發現內容有些關於建置跟啟動的部份 但是如果我不想要 entry point 叫作這個名字呢 那就需要自己在 nest-cli.json 加入一個 entryFile 的設定 舉例如下: ![](https://hackmd.io/_uploads/ryuNBgoA3.png) ![](https://hackmd.io/_uploads/SJrLHgo0n.png) 然後執行 ```shell= pnpm start:dev ``` ![](https://hackmd.io/_uploads/SJAqHeoRn.png) 就會發現可以正常運行了 但這邊為了符合常規設定,所以我們還是回復預設值 main.ts 或直接移除 entryFile 這欄 **Note**: 改回來的同時 entry point 的檔案也需要改回來 ### main 建構解析 接著可以來看 main.ts 內容 ```typescript= import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); } bootstrap(); ``` 分為兩個部份 1. 建構 2. 執行 #### 執行 雖然順序是先建構再執行 這邊因為執行比較短,所以先講 真正再呼叫程式的只有第 8 行, bootstrap() 其他部份都是在做建構 #### 建構 第1部份就是使用 NestFactory 這個建構 Factory 來做所有元件的初始化 包含在每個被標注成 @Module 的元件都會被 Factory 依據 constructor 的設定來建構 特別注意的是 nestjs 使用 di container 所有元件的建構順序會依照注入設定依序注入 最後根據 app 特性去啟動 比如這邊是使用 web server 所以會使用 app.listen 如果是其他服務 也可以使用 app.start 的方式來啟動 ## nestjs 元件介紹 ### 最基礎單位是 Module ![](https://hackmd.io/_uploads/rkAjL0Gu3.png) ### 透過 Provider 來使用共用服務或是元件 app.module.ts: ```typescript= import { Module } from '@nestjs/common'; import { AppService } from './app.service'; @Module({ imports: [], controllers: [], providers: [AppService], exports: [] }) export class AppModule {} ``` 透過 Injectable 關鍵字讓 Service 可以被 provide app.service.ts ```typescript= import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { getHello(): string { return 'Hello World!'; } } ``` ### controllers 用來放置與 HTTP 互動的邏輯 app.module.ts: ```typescript= import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @Module({ imports: [], controllers: [AppController], providers: [AppService], }) export class AppModule {} ``` app.controller.ts: ```typescript= import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() getHello(): string { return this.appService.getHello(); } } ``` ### imports 用來引入外部非全域的 Module ## nestjs 如何分享元件 在 nestjs 雖然與 Angular 框架類似有 Module 元件以及 DI Container 概念 然而不同的是, Angular 一旦註冊 Module 就是全局元件 但 nestjs 並非如此,需要特別設定可以註冊是否需要全局元件,預設不是全局 而需要透過 import 的方式來引入 ### 全局引入 代表該 Module 只需要在最外層的 root module 內 import 即可在其他 Module 內使該元件 export 出來的功能 而該 Module 在宣告時,需要再上面加入 @Global 這個修飾子 舉例: user.module.ts ```typescript= import { Module } from '@nestjs/common'; import { UserService } from './user.service'; @Global() @Module({ providers: [UserService], exports: [UserService], }) export class UserModule {} ``` book.module.ts ```typescript= import { Module } from '@nestjs/common'; import { BookController } from './book.controller'; import { BookStoreService } from './book-store.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { BookStoreEntity } from './book-store.entity'; @Module({ imports: [TypeOrmModule.forFeature([BookStoreEntity])], controllers: [BookController], providers: [BookStoreService], }) export class BookStoreModule {} ``` book.controller.ts ```typescript= import { Controller, Get Query, } from '@nestjs/common'; import { BookStoreDto } from './dtos/book-store.dto'; import { BookStoreService } from './book-store.service'; import { UserService } from './user/user.service'; @Controller('books') export class BookController { private logger = new Logger(BookController.name); constructor( private readonly bookStoreService: BookStoreService, private readonly userService: UserService, ) {} @Get('/user') async sayHi(@Query('user') user: string) { this.logger.log({ user }); return user + ' says ' + this.userService.greeting(); } } ``` 比如 logger module 或是一些 global 連線比如 db connection 或是 redis 等等 ### 逐個在需要的地方引入 代表該 Module 屬於非全局 module ,只有在引入的 Module 才能使用內部 export 出來的服務 舉例: user.module.ts ```typescript= import { Module } from '@nestjs/common'; import { UserService } from './user.service'; @Module({ providers: [UserService], exports: [UserService], }) export class UserModule {} ``` book.module.ts ```typescript= import { Module } from '@nestjs/common'; import { BookController } from './book.controller'; import { BookStoreService } from './book-store.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { BookStoreEntity } from './book-store.entity'; import { UserModule } from './user/user.module'; @Module({ imports: [TypeOrmModule.forFeature([BookStoreEntity]), UserModule], controllers: [BookController], providers: [BookStoreService], }) export class BookStoreModule {} ``` book.controller.ts ```typescript= import { Controller, Get Query, } from '@nestjs/common'; import { BookStoreDto } from './dtos/book-store.dto'; import { BookStoreService } from './book-store.service'; import { UserService } from './user/user.service'; @Controller('books') export class BookController { private logger = new Logger(BookController.name); constructor( private readonly bookStoreService: BookStoreService, private readonly userService: UserService, ) {} @Get('/user') async sayHi(@Query('user') user: string) { this.logger.log({ user }); return user + ' says ' + this.userService.greeting(); } } ``` ## 理想上的分享方式 一般來說,全局引用可以很方便的去複用共同使用的功能 然而代價就是會有全局汙染,因為所有 module 都認得該 module 的 Instance 最理想的方式是可以把,引用限制該功能模組之內 這樣再拔除或是修改該模組時,影響的範圍就會比較小 ### 非公用模組 所以在 layout 上 同常會把同模組相關的東西放在同一個 folder 如下 ![](https://hackmd.io/_uploads/Hy2PYWj0h.png) 在 auth app 下,除了 root module 之外還有一個 users module users module 的相關邏輯會放在一個 users 資料夾 這樣在抽換 users module 或是 debug 時也會比較好限縮範圍找 bug ### 公用模組 公用的 module 會統一放個叫作 lib 的 folder 下 ![](https://hackmd.io/_uploads/H1qFcbsR3.png) 這是一般比較偏近官方的作法 如果要自己去額外變 layout 最好有自己一套的設計原則 否則真的就是降低可維護性,提高閱讀成本 ### 暗黑兵法 自己創一套模改版的 layout 只有自己看的懂 改名叫作 clean architecture 這樣就可以達到只有自己看的懂,別人改不動的境地了 別人問就說為了符合 clean architecture 或是什麼 N 角架構 建構了一個護城河 silo