NestJS 控制器

2023-09-07 21:51 更新

控制器負(fù)責(zé)處理傳入請求并將響應(yīng)返回給客戶端。

3

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

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

路由

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

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

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

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

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

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

Request

處理程序有時需要訪問客戶端的請求細(xì)節(jié)。Nest 提供了對底層平臺(默認(rèn)為 Express)的請求對象(request)的訪問方式。我們可以在處理函數(shù)的簽名中使用 @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 上面的參數(shù)示例所示),請安裝 @types/express 。

Request 對象代表 HTTP 請求,并具有查詢字符串,請求參數(shù)參數(shù),HTTP 標(biāo)頭(HTTP header) 和 正文(HTTP body)的屬性(在這里閱讀更多)。在多數(shù)情況下,不必手動獲取它們。 我們可以使用專用的裝飾器,比如開箱即用的 @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 對象接口。在使用它們時,您還應(yīng)該導(dǎo)入底層庫的類型聲明(如:@types/express)以充分利用它們。需要注意的是,在請求處理函數(shù)中注入 @Res()或 @Response() 時,會將 Nest 置于該處理函數(shù)的特定于庫(Library-specific mode)的模式下,并負(fù)責(zé)管理響應(yīng)。這樣做時,必須通過調(diào)用 response 對象(例如,res.json(…) 或 res.send(…))發(fā)出某種響應(yīng),否則 HTTP 服務(wù)器將掛起。

資源

我們已經(jīng)創(chuàng)建了一個端點來獲取 cats 的數(shù)據(jù)(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 為所有標(biāo)準(zhǔn)的 HTTP 方法提供了相應(yīng)的裝飾器:@Put()、@Delete()、@Patch()、@Options()、以及 @Head()。此外,@All() 則用于定義一個用于處理所有 HTTP 請求方法的處理程序。

路由通配符

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

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

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

狀態(tài)碼

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

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

HttpCode 需要從 @nestjs/common 包導(dǎo)入。

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

Headers

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

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

Header 需要從 @nestjs/common 包導(dǎo)入。

重定向

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

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

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

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

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

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

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

路由參數(shù)

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

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

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

Param 需要從 @nestjs/common 包導(dǎo)入。

@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 缺乏對嵌套路由器的支持,因此當(dāng)使用子域路由時,應(yīng)該改用(默認(rèn)) Express 適配器(Express adapter)。

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

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

作用域

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

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

異步性

我們酷愛現(xiàn)代 Javascript,并且我們知道數(shù)據(jù)讀?。╠ata extraction)大多是異步的.這就是為什么 Nest 完美支持異步函數(shù)(Async Function)特性的原因。

了解更多關(guān)于 Async / await 請點擊這里

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

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

這是完全有效的。此外,通過返回 RxJS observable 流,Nest 路由處理程序?qū)⒏訌姶蟆?Nest 將自動訂閱下面的源并獲取最后發(fā)出的值(在流完成后)。

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

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

請求負(fù)載

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

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

現(xiàn)在,我們來創(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';
}

處理錯誤

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

完整示例

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

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ā)體驗變得更加簡單。在這里閱讀關(guān)于該功能的更多信息。

最后一步

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

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

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

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

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

類庫特有方式

到目前為止,我們已經(jīng)討論了 Nest 操作響應(yīng)的標(biāo)準(zhǔn)方式。操作響應(yīng)的第二種方法是使用類庫特有的響應(yīng)對象(Response)。為了注入特定的響應(yīng)對象,我們需要使用 @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([]);
  }
}

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

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

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

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


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號