NestJS 控制器

2023-09-07 21:51 更新

控制器負責處理傳入請求并將響應返回給客戶端。

3

控制器的目的是接收應用的特定請求。路由機制控制哪個控制器接收哪些請求。通常,每個控制器有多個路由,不同的路由可以執(zhí)行不同的操作。

為了創(chuàng)建一個基本的控制器,我們使用類和裝飾器。裝飾器將類與所需的元數據相關聯,并使 Nest 能夠創(chuàng)建路由映射(將請求綁定到相應的控制器)。

路由

在下面的例子中,我們使用 @Controller() 裝飾器定義一個基本的控制器??蛇x 路由路徑前綴設置為 cats。在 @Controller() 裝飾器中使用路徑前綴可以使我們輕松地對一組相關的路由進行分組,并最大程度地減少重復代碼。例如,我們可以選擇將一組用于管理與 /customers 下的客戶實體進行互動的路由進行分組。這樣,我們可以在 @Controller() 裝飾器中指定路徑前綴 customers,這樣就不必為文件中的每個路由重復路徑的那部分。

cats.controller.ts
import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

要使用 CLI 創(chuàng)建控制器,只需執(zhí)行 $ nest g controller cats 命令。

findAll() 方法之前的 @Get() HTTP 請求方法裝飾器告訴 Nest 為 HTTP 請求的特定端點創(chuàng)建處理程序。端點對應于 HTTP 請求方法(在本例中為 GET )和路由路徑(如 GET /customer )。什么是路由路徑 ? 一個處理程序的路由路徑是通過連接為控制器 (Controller) 聲明的(可選)前綴和請求裝飾器中指定的任何路徑來確定的。由于我們已經為每個 route(cats) 聲明了一個前綴,并且沒有在裝飾器中添加任何路由信息,因此 Nest 會將 GET /cats 請求映射到此處理程序。如上所述,該路徑包括可選的控制由路徑前綴和請求方法裝飾器中聲明的任何路徑字符串。例如,路徑前綴 customers 與裝飾器 @Get('profile') 組合會為 GET /customers/profile 請求生成路由映射。

在上面的示例中,當對此端點發(fā)出 GET 請求時, Nest 會將請求路由到我們的自定義的 findAll() 方法。請注意,我們在此處選擇的函數名稱完全是任意的。顯然,我們必須聲明一個綁定到路由的函數,但 Nest 不會對所選的函數名稱附加任何意義。(譯者注:即路由與處理函數命名無關)

此函數將返回 200 狀態(tài)代碼和相關的響應,在本例中只返回了一個字符串。為什么會這樣? 為了解釋原因,首先我們將介紹 Nest 使用兩種不同的操作響應選項的概念:

標準(推薦)使用這個內置方法,當請求處理程序返回一個 JavaScript 對象或數組時,它將自動序列化為 JSON。但是,當它返回一個 JavaScript 基本類型(例如string、number、boolean)時, Nest 將只發(fā)送值,而不嘗試序列化它。這使響應處理變得簡單:只需要返回值,其余的由 Nest 負責。
類庫特有的我們可以在函數簽名處通過 @Res() 注入類庫特定的響應對象(例如, Express)。使用此方法,你就能使用由該響應對象暴露的原生響應處理函數。例如,使用 Express,您可以使用 response.status(200).send() 構建響應

注意!Nest 檢測處理程序何時使用 @Res() 或 @Next(),表明你選擇了特定于庫的選項。如果在一個處理函數上同時使用了這兩個方法,那么此處的標準方式就是自動禁用此路由, 你將不會得到你想要的結果。如果需要在某個處理函數上同時使用這兩種方法(例如,通過注入響應對象,單獨設置 cookie / header,但把其余部分留給框架),你必須在裝飾器 @Res({ passthrough: true }) 中將 passthrough 選項設為 true

Request

處理程序有時需要訪問客戶端的請求細節(jié)。Nest 提供了對底層平臺(默認為 Express)的請求對象(request)的訪問方式。我們可以在處理函數的簽名中使用 @Req() 裝飾器,指示 Nest 將請求對象注入處理程序。

cats.controller.ts
import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Req() request: Request): string {
    return 'This action returns all cats';
  }
}

為了在 express 中使用 Typescript (如 request: Request 上面的參數示例所示),請安裝 @types/express 。

Request 對象代表 HTTP 請求,并具有查詢字符串,請求參數參數,HTTP 標頭(HTTP header) 和 正文(HTTP body)的屬性(在這里閱讀更多)。在多數情況下,不必手動獲取它們。 我們可以使用專用的裝飾器,比如開箱即用的 @Body() 或 @Query() 。 下面是 Nest 提供的裝飾器及其代表的底層平臺特定對象的對照列表。

@Request(),@Req()req
@Response(),@Res()*res
@Next()next
@Session()req.session
@Param(key?: string)req.params/req.params[key]
@Body(key?: string)req.body/req.body[key]
@Query(key?: string)req.query/req.query[key]
@Headers(name?: string)req.headers/req.headers[name]
@Ip()req.ip
@HostParam()req.hosts

為了與底層 HTTP 平臺(例如,Express 和 Fastify)之間的類型兼容, Nest 提供了 @Res()和 @Response() 裝飾器。@Res() 只是 @Response() 的別名。兩者都直接暴露了底層平臺的 response 對象接口。在使用它們時,您還應該導入底層庫的類型聲明(如:@types/express)以充分利用它們。需要注意的是,在請求處理函數中注入 @Res()或 @Response() 時,會將 Nest 置于該處理函數的特定于庫(Library-specific mode)的模式下,并負責管理響應。這樣做時,必須通過調用 response 對象(例如,res.json(…) 或 res.send(…))發(fā)出某種響應,否則 HTTP 服務器將掛起。

資源

我們已經創(chuàng)建了一個端點來獲取 cats 的數據(GET 路由)。我們通常還希望提供一個創(chuàng)建新記錄的端點。為此,讓我們創(chuàng)建 POST 處理程序:

cats.controller.ts
import { Controller, Get, Post } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Post()
  create(): string {
    return 'This action adds a new cat';
  }

  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

就這么簡單。 Nest 為所有標準的 HTTP 方法提供了相應的裝飾器:@Put()、@Delete()、@Patch()、@Options()、以及 @Head()。此外,@All() 則用于定義一個用于處理所有 HTTP 請求方法的處理程序。

路由通配符

路由同樣支持模式匹配。例如,星號被用作通配符,將匹配任何字符組合。

@Get('ab*cd')
findAll() {
  return 'This route uses a wildcard';
}

路由路徑 'ab*cd' 將匹配 abcd 、ab_cd 、abecd 等。字符 ? 、+ 、 * 以及 () 是它們的正則表達式對應項的子集。連字符(-) 和點(.)按字符串路徑逐字解析。

狀態(tài)碼

如上所述,默認情況下,響應的狀態(tài)碼總是默認為 200,除了 POST 請求(默認響應狀態(tài)碼為 201),我們可以通過在處理函數外添加 @HttpCode(...) 裝飾器來輕松更改此行為。

@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}

HttpCode 需要從 @nestjs/common 包導入。

通常,狀態(tài)碼不是固定的,而是取決于各種因素。在這種情況下,您可以使用類庫特有(library-specific)的 response (通過 @Res()注入 )對象(或者在出現錯誤時,拋出異常)。

Headers

要指定自定義響應頭,可以使用 @header() 裝飾器或類庫特有的響應對象,(并直接調用 res.header())。

@Post()
@Header('Cache-Control', 'none')
create() {
  return 'This action adds a new cat';
}

Header 需要從 @nestjs/common 包導入。

重定向

要將響應重定向到特定的 URL,可以使用 @Redirect() 裝飾器或特定于庫的響應對象(并直接調用 res.redirect())。

@Redirect() 裝飾器有兩個可選參數,url 和 statusCode。 如果省略,則 statusCode 默認為 302。

@Get()
@Redirect('https://nestjs.com', 301)

有時您可能想動態(tài)地決定 HTTP 狀態(tài)代碼或重定向 URL。通過從路由處理方法返回一個如下格式的對象:

{
  "url": string,
  "statusCode": number
}

返回的值將覆蓋傳遞給 @Redirect()裝飾器的所有參數。 例如:

@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version) {
  if (version && version === '5') {
    return { url: 'https://docs.nestjs.com/v5/' };
  }
}

路由參數

當您需要接受動態(tài)數據(dynamic data)作為請求的一部分時(例如,使用GET /cats/1 來獲取 id 為 1 的 cat),帶有靜態(tài)路徑的路由將無法工作。為了定義帶參數的路由,我們可以在路由路徑中添加路由參數標記(token)以捕獲請求 URL 中該位置的動態(tài)值。下面的 @Get() 裝飾器示例中的路由參數標記(route parameter token)演示了此用法。以這種方式聲明的路由參數可以使用 @Param() 裝飾器訪問,該裝飾器應添加到函數簽名中。

@Get(':id')
findOne(@Param() params): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}

@Param() 用于修飾一個方法的參數(上面示例中的 params),并在該方法內將路由參數作為被修飾的方法參數的屬性。如上面的代碼所示,我們可以通過引用 params.id來訪問(路由路徑中的) id 參數。 您還可以將特定的參數標記傳遞給裝飾器,然后在方法主體中按參數名稱直接引用路由參數。

Param 需要從 @nestjs/common 包導入。

@Get(':id')
findOne(@Param('id') id): string {
  return `This action returns a #${id} cat`;
}

子域路由

@Controller 裝飾器可以接受一個 host 選項,以要求傳入請求的 HTTP 主機匹配某個特定值。

@Controller({ host: 'admin.example.com' })
export class AdminController {
  @Get()
  index(): string {
    return 'Admin page';
  }
}

由于 Fastify 缺乏對嵌套路由器的支持,因此當使用子域路由時,應該改用(默認) Express 適配器(Express adapter)。

與一個路由路徑 path 類似,該 hosts 選項可以使用參數標識(token)來捕獲主機名中該位置的動態(tài)值。下面的 @Controller() 裝飾器示例中的主機參數標識(host parameter token)演示了此用法??梢允褂?nbsp;@HostParam() 裝飾器訪問以這種方式聲明的主機參數,該裝飾器應添加到方法簽名中。

@Controller({ host: ':account.example.com' })
export class AccountController {
  @Get()
  getInfo(@HostParam('account') account: string) {
    return account;
  }

作用域

對于來自不同編程語言背景的人來說,可能對 Nest 中幾乎所有內容都可以在傳入的請求之間共享感到非常意外。例如,我們有一個數據庫連接池,具有全局狀態(tài)的單例服務等。請記住,Node.js 并不遵循請求/響應多線程無狀態(tài)模型(在該模型中,每個請求都由單獨的線程處理),在 Nest 中,每個請求都由主線程處理。因此,使用單例實例對我們的應用程序來說是完全安全的。

但是,存在基于請求的控制器生命周期可能是期望行為的邊緣情況,例如 GraphQL 應用程序中的請求緩存,請求跟蹤或多租戶。在這里學習如何控制作用域。

異步性

我們酷愛現代 Javascript,并且我們知道數據讀取(data extraction)大多是異步的.這就是為什么 Nest 完美支持異步函數(Async Function)特性的原因。

了解更多關于 Async / await 請點擊這里

每個異步函數都必須返回一個 Promise。這意味著您可以返回延遲值,而 Nest 將自行解析它。讓我們看看下面這個例子:

cats.controller.ts
@Get()
async findAll(): Promise<any[]> {
  return [];
}

這是完全有效的。此外,通過返回 RxJS observable 流,Nest 路由處理程序將更加強大。 Nest 將自動訂閱下面的源并獲取最后發(fā)出的值(在流完成后)。

cats.controller.ts
@Get()
findAll(): Observable<any[]> {
  return of([]);
}

上述的兩種方法都是可行的,你可以選擇你喜歡的方式。

請求負載

此前我們列舉的的 POST 路由處理程序樣例中,處理程序沒有接受任何客戶端參數。我們在這里通過添加 @Body() 參數來解決這個問題。

首先(如果您使用 TypeScript),我們需要確定 DTO(數據傳輸對象)模式。DTO是一個對象,它定義了如何通過網絡發(fā)送數據。我們可以通過使用 TypeScript 接口(Interface)或簡單的類(Class)來定義 DTO 模式。有趣的是,我們在這里推薦使用類。為什么?類是 JavaScript ES6 標準的一部分,因此它們在編譯后的 JavaScript 中被保留為實際實體。另一方面,由于 TypeScript 接口在轉換過程中被刪除,所以 Nest 不能在運行時引用它們。這一點很重要,因為諸如管道(Pipe)之類的特性為在運行時訪問變量的元類型提供更多的可能性。

現在,我們來創(chuàng)建 CreateCatDto 類:

/*
  create-cat.dto.ts
*/
export class CreateCatDto {
  readonly name: string;
  readonly age: number;
  readonly breed: string;
}

它只有三個基本屬性。 之后,我們可以在 CatsController 中使用新創(chuàng)建的DTO:

cats.controller.ts
@Post()
async create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}

處理錯誤

這里有一個關于處理錯誤(即處理異常)的單獨章節(jié)。

完整示例

下面是一個示例,該示例利用幾個可用的裝飾器來創(chuàng)建基本控制器。 該控制器暴露了幾個訪問和操作內部數據的方法。

cats.controller.ts
import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat';
  }

  @Get()
  findAll(@Query() query: ListAllEntities) {
    return `This action returns all cats (limit: ${query.limit} items)`;
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return `This action returns a #${id} cat`;
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
    return `This action updates a #${id} cat`;
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return `This action removes a #${id} cat`;
  }
}

Nest CLI 提供了一個能夠自動生成所有這些模板代碼的生成器,它幫助我們規(guī)避手動建立這些文件,并使開發(fā)體驗變得更加簡單。在這里閱讀關于該功能的更多信息。

最后一步

控制器已經準備就緒,可以使用,但是 Nest 依然不知道 CatsController 是否存在,所以它不會創(chuàng)建這個類的一個實例。

控制器總是屬于模塊,這就是為什么我們在 @Module() 裝飾器中包含 controllers 數組的原因。 由于除了根模塊 AppModule之外,我們還沒有定義其他模塊,所以我們將使用它來介紹 CatsController:

app.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';

@Module({
  controllers: [CatsController],
})
export class AppModule {}

我們使用 @Module() 裝飾器將元數據附加到模塊類中,現在,Nest 可以輕松反射(reflect)出哪些控制器(controller)必須被安裝。

類庫特有方式

到目前為止,我們已經討論了 Nest 操作響應的標準方式。操作響應的第二種方法是使用類庫特有的響應對象(Response)。為了注入特定的響應對象,我們需要使用 @Res() 裝飾器。為了對比差異,讓我們來重寫 CatsController:

cats.controller.ts
import { Controller, Get, Post, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Res() res: Response) {
    res.status(HttpStatus.CREATED).send();
  }

  @Get()
  findAll(@Res() res: Response) {
    res.status(HttpStatus.OK).json([]);
  }
}

盡管此方法有效,并且實際上通過提供對響應對象的完全控制(標頭操作,特定于庫的功能等)在某些方面提供了更大的靈活性,但應謹慎使用此種方法。通常來說,這種方式非常不清晰,并且有一些缺點。 主要的缺點是你的代碼變得依賴于平臺(因為不同的底層庫在響應對象(Response)上可能具有不同的 API),并且更加難以測試(您必須模擬響應對象等)。

而且,在上面的示例中,你失去與依賴于 Nest 標準響應處理的 Nest 功能(例如,攔截器(Interceptors) 和 @HttpCode()/@Header() 裝飾器)的兼容性。要解決此問題,可以將 passthrough 選項設置為 true,如下所示:

@Get()
findAll(@Res({ passthrough: true }) res: Response) {
  res.status(HttpStatus.OK);
  return [];
}

現在,你就能與底層框架原生的響應對象(Response)進行交互(例如,根據特定條件設置 Cookie 或 HTTP 頭),并將剩余的部分留給 Nest 處理。


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號