圖解 Nestjs - 適合中國寶寶的入門指導

筆者入門 Nest 的時候屬實是迷糊了一陣,本文將從初學者的視角出發,試圖爲大家解釋 Nestjs 到底是如何運作的。如有錯誤歡迎指出,謝謝~


假設我們來做這樣一個服務:寶可夢大全

提供四個接口:

  1. 獲取完整的寶可夢列表

  2. 根據寶可夢編號獲取某一隻寶可夢的信息

  3. 獲取完整的技能列表

  4. 根據某個技能獲取可以學會該技能的寶可夢列表

Module = 模塊

Module,中文譯作模塊,我們將從它來入手,搞清楚 Nestjs 大概是如何工作的。

官網這張圖表達的很清楚,Nest 的大致理念就是一顆 “模塊樹”,從根模塊出發,連接到許多的子功能模塊。

我們的寶可夢查詢的結構可能是這樣的:

(本文中我們使用 Prisma 來操作數據庫)

現在讓我們聚焦到其中一個功能模塊:Pokemon Module

細說 Pokemon Module

這個模塊的領域是寶可夢,顯然這是一個非常核心的模塊。那麼 “模塊”,又是怎麼運作的呢?

從外部看,“模塊” 是一個黑盒,有自己的輸入和輸出:

而黑盒的內部,則主要是兩部分:

  1. Services

  2. Controllers

Controllers 是接收請求的入口,Services 則是方法實現,這個應該不難理解

那麼我們仔細看看所謂的輸入輸出是什麼。

Module 的輸入是什麼?

由於寶可夢的信息是存在數據庫中的,因此寶可夢模塊需要 Prisma 模塊來與數據庫交互。所以 Prisma Module 是 Pokemon Module 的輸入。

Module 的輸出是什麼?

既然我們說 Prisma Module 是 Pokemon Module 的輸入,那,Prisma Module 一定是輸出了什麼,對吧?

沒錯,Prisma Module 的輸出就是 Prisma Service。Pokemon Module 可以使用 Prisma Service 中的各種方法來交互數據庫。

Provider?

在剛剛的敘述中,我們還沒有提及在 Nest 中非常有存在感的Provider

我們說過,“Controllers 是接收請求的入口,Services 則是方法實現”,那麼,Controllers 是如何調用 Services 的呢?答案就是,當 Services 成爲 Provider 時

這就像是同一樣東西的兩種名字。它既是 Pokemon Service,也是 Pokemon Module 的 Provider,取決於從什麼視角去看他。因此,在代碼中它往往表現爲這樣:

// nestjs project directory
|-pokemon
| |_pokemon.module.ts
| |_pokemon.controller.ts
| |_pokemon.service.ts
|-......
// pokemon.module.ts
@Module({
  controllers: [PokemonController],
  providers: [PokemonService],
  imports: [PrismaModule],
  exports: [PokemonService],
})
// pokemon.controller.ts
export class PokemonController {
  // 正是因爲Module的providers中傳入了PokemonService
  // Controller的constructor才能接收到pokemonService參數
  constructor(private readonly pokemonService: PokemonService) {}
  
  @Get('/find/all')
  async findAll() {
    const allPokemons = await this.pokemonService.findAll();
  // ......
  }
  
  @Get('/find/:id')
  async findOne(
    @Param('id', ParseIntPipe) id: number,
  ) {
    const pokemon = await this.pokemonService.findOne(id);
  // ......
  }
}
// pokemon.service.ts

// 此行Injectable裝飾器也是必要的
// 關於Dependency Injection的更多信息,建議參考Angular官方文檔:https://angular.dev/guide/di
@Injectable()
export class PokemonService {
  constructor(private prisma: PrismaService) {}
  
  parsePokemon(pokemon) {
    // ...
  }
  
  async findAll() {
    const pokemons = await this.prisma.pokemon.findMany({
      include: {
        likes: true,
      },
    });
    return pokemons.map((item) => this.parsePokemon(item));
  }

  async findOne(id: number) {
    const pokemon = await this.prisma.pokemon.findUnique({
      where: { id }
    });
    if (!pokemon) {
      throw new NotFoundException(`pokemon ${id} not found`);
    }
    return this.parsePokemon(pokemon);
  }
}

Provider vs Import

在上面的例子我們可以看到,由於 Injectable 的 PokemonService 被放入了 module 定義的providers中,PokemonController 便可以使用它,那麼它的功能豈不是和imports重疊了?

事實上,如果我們去掉imports,將Prisma Service也放到providers中,我們的代碼依然可以運行:

// pokemon.module.ts
@Module({
  controllers: [PokemonController],
  providers: [PokemonService, PrismaService],
  imports: [],
  exports: [PokemonService],
})

直接先說結論:用 Import

具體來說,只要是用外部服務(也就是除去 PokemonModule 將 PokemonService 作爲 Provider 這種情況),使用 import 更爲合適。

區別在於:import module 會複用已經創建的實例,而每個 provider 都會創建新的實例。

前者佔用更少的內存,且一個可複用的實例在多處被使用在需求中也更加常見。

// pokemon.module.ts

// 推薦
@Module({
  controllers: [PokemonController],
  providers: [PokemonService],
  imports: [PrismaModule],
  exports: [PokemonService],
})
// 不推薦
@Module({
  controllers: [PokemonController],
  providers: [PokemonService, PrismaService],
  imports: [],
  exports: [PokemonService],
})

總結

看一個 Nestjs 應用是怎麼跑的,首先就是看明白 Module 之間是如何互相作用的。

那麼在實現我們自己的 Module 時,只需要記住:

比如這樣:

// pokemon.module.ts
@Module({
  controllers: [PokemonController],
  providers: [PokemonService],
  imports: [PrismaModule],
  exports: [PokemonService],
})

// skill.module.ts
@Module({
  controllers: [SkillController],
  providers: [SkillService],
  imports: [PrismaModule, PokemonModule],
})

好了,恭喜你,你已經完全掌握 Nestjs 了!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/PT7QvvKCSu75R3Z1wYtl7A