iT邦幫忙

2024 iThome 鐵人賽

DAY 3
0

nestjs 核心元件 Module

目標

  1. 介紹 nestjs 核心元件 Module
  2. 說明 nestjs 建構與分享元件的設計概念

nestjs 建構原理

說明

nestjs 與一般 nodejs 程式概念一樣都是把邏輯透過 module 來把邏輯做模組化。在程式啟動點,把這些模組透過工廠模式建構載入設定。當所有元件建構都被初始化之後,才正式啟動服務。

範例

使用 nestjs cli 建構一個 mountain_climb 專案

nest new mountain_climb

長出來的專案結構如下:

然後找尋 package.json 察看 entry point 可能在 script 的點,發現

從 start:prod 看起來,有可能是 main.ts 這個檔案。但是實際上是怎麼鏈結,需要看一下 nest-cli.json 這個檔案。打開一看

發現 , holy 媽祖!根本沒特別指定。這時候就可以查看官網 關於 nestjs cli 設定章節
會發現有寫一行很隱諱的寫著預設 application 需要一個 main.ts。所以預設的 entry point 就是main.ts。雖然說打開 main.ts 也可以發現內容有些關於建置跟啟動的部份。但是如果我不想要 entry point 叫作這個名字呢?
那就需要自己在 nest-cli.json 加入一個 entryFile 的設定。

舉例如下:


然後執行

pnpm start:dev

就可以正常運行了。但這邊為了符合常規設定,所以我們還是回復預設值 main.ts 或直接移除 entryFile 這欄。

Note: 改回來的同時 entry point 的檔案也需要改回來

main 建構解析

接著可以來看 main.ts 內容

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 容器去生成。所有元件的建構順序會依照注入設定依序注入。

最後根據 app 特性去啟動。比如這邊是使用 web server 所以會使用 app.listen 。如果是其他種類服務 ,也可以使用 app.start 的方式來啟動。

nestjs 元件介紹

最基礎單位是 Module

透過 Provider 來使用共用服務或是元件

app.module.ts:

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

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

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

controllers 用來放置與 HTTP 互動的邏輯

app.module.ts:

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:

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 容器概念。然而不同的是, Angular 一旦註冊 Module 就是全局元件。但 nestjs 並非如此,需要特別設定可以註冊是否需要全局元件,預設不是全局。而需要透過 import 的方式來引入

全局引入

代表該 Module 只需要在最外層的 root module 內 import。即可在其他 Module 內使該元件 export 出來的功能。而該 Module 在宣告時,需要再上面加入 @Global 這個修飾子

舉例:
user.module.ts

import { Module } from '@nestjs/common';
import { UserService } from './user.service';

@Global()
@Module({
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}

book.module.ts

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

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

import { Module } from '@nestjs/common';
import { UserService } from './user.service';

@Module({
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}

book.module.ts

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

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 上,通常會把同模組相關的東西放在同一個資料夾。
如下

在 auth app 下,除了 root module 之外還有一個 users module 。 users module 的相關邏輯會放在一個 users 資料夾。這樣在抽換 users module 或是 debug 時也會比較好限縮範圍找 bug。

公用模組

公用的 module 會統一放個叫作 lib 的資料夾下

這是一般比較偏近官方的作法。如果要自己去額外變 layout 最好有自己一套的設計原則。否則真的就是降低可維護性,提高閱讀成本。

結論

基本邏輯是,nestjs 會把元件功能以模組為單位封裝在 module 內。通常這些模組本身會建立一個同名資料夾,把相同模組內的功能放在一起。沒有特殊設計,儘可能不要去隨一修改這種模式,一來不好維護,而來需要再花多餘的時間理解。

舉個反例:比如,有些人為了學什麼乾淨XX架構,硬是把資料隨意擺放。由於沒有設計到位。最後那個說要做乾淨XX架構的人,也不知道那些資料夾要這樣放有何目的。這就是為改而改,沒有設計。

這就是為何說,即使 nestjs 框架已經提供一套設計,仍是有可能寫成一團大泥球。需要遵循某些規範,才能讓可維護性提高。

借用軟體界前輩的一句話:『不作死,就不會死』。在發明輪子前,先想想要解決的問題是什麼。


上一篇
nestjs 的依賴注入概念
下一篇
nestjs 核心元件 Provider
系列文
透過 nestjs 框架,讓 nodejs 系統維護度增加31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言