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.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();
分為兩個部份:
雖然順序是先建構再執行,這邊因為執行比較短,所以先講。真正再呼叫程式的只有第 8 行, bootstrap()。其他部份都是在做建構
第1部份就是使用 NestFactory 這個建構 Factory 來做所有元件的初始化。包含在每個被標注成 @Module 的元件都會被 Factory 依據 constructor 的設定來建構。
特別注意的是 nestjs 使用 DI 容器去生成。所有元件的建構順序會依照注入設定依序注入。
最後根據 app 特性去啟動。比如這邊是使用 web server 所以會使用 app.listen 。如果是其他種類服務 ,也可以使用 app.start 的方式來啟動。
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!';
}
}
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();
}
}
在 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 框架已經提供一套設計,仍是有可能寫成一團大泥球。需要遵循某些規範,才能讓可維護性提高。
借用軟體界前輩的一句話:『不作死,就不會死』。在發明輪子前,先想想要解決的問題是什麼。