Controller
路由
路由名稱會透過 @Controller 傳入元數據 (metadata),在應用程式啟動時建立一個路由表並將這個元數據註冊進去,如:
@Controller('todos')
export class TodoController {}
'todos' 就會被註冊成可以存取的端點,可以透過 http://localhost/todos 這樣的網址發送請求。
@Controller 也會被註冊到 IoC Container 裡面。
請求
必須使用 HTTP method 裝飾器,才會將對應方法 (handler) 註冊到路由表,如:
@Controller('todos')
export class TodoController {
@Get()
getTodos() {
return [];
}
}
此時對 /todos 發送 GET 請求,執行 getTodos 成功的話會並得到 []。
子路由
在 @Get 裡面帶入字串會生成子路由的端點,如 @Get('sub')則表示可以存取 /todos/sub :
@Controller('todos')
export class TodoController {
@Get()
getTodos() {
return [];
}
@Get('sub')
getTodo() {
return '這是子路由';
}
}
通用路由
可以透過 *、+、? 等符號來匹配有滿足特定條件的路徑,如:
// 這樣可以匹配 todos/bulk/goooooooood
@Get('bulk/goo*d')
getGood() {
return '這是 /bulk 下面的通用路由 goo*d';
}
裝飾器
handler 的參數可以用裝飾器來解析資料,如:
@Param@Query@Body@Header
@Param
動態路由可以透過 @Param 來解出路由中的參數,例如下面的 :id:
// 解析 /todos/:id
@Get(':id')
getTodo(@Param() param: { id: string }) {
return `這是 id 為 ${param.id} 的子路由`;
}
// 更簡短的寫法,在裝飾器中指定 key 就不用取出整個物件
@Get(':id')
getTodo(@Param('id') id: string) {
return `這是 id 為 ${id} 的子路由`;
}
@Body
@Post()
createTodo(@Body() data: { content: string }) {
const newTodo = {
id: this.todos.length + 1,
content: data.content,
};
this.todos.push(newTodo);
return newTodo;
}
@Query
// 解析 /todos?limit=3&offset=3
@Get()
getTodos(
@Query('limit') limit?: string,
@Query('offset') offset?: string
) {
if (!limit) {
return this.todos;
}
if (!offset) {
offset = '0';
}
const limitNum = parseInt(limit);
const offsetNum = parseInt(offset);
return this.todos.slice(offsetNum, offsetNum + limitNum);
}
@HttpCode
除了 POST 請求會回應 201 之外,其他方法預設都會回應 200,如果要自訂回傳的狀態碼,可以使用 @HttpCode 裝飾器,並帶入內建的常數 HttpStatus:
// 請求成功時使用 NO_CONTENT 映射出來的 204 作為狀態碼
@Get()
@HttpCode(HttpStatus.NO_CONTENT)
getTodos() {
return [];
}
HttpStatus 裡面是用 enum 型別宣告的映射資料,包含了常見的狀態碼與語意匹配。
回應
controller 有 3 種方式處理回應:
- 標準模式
- RxJS 模式
- 函式庫模式
標準模式
標準模式支援同步和非同步,也是官方推薦的方式:
@Get()
getData() {
return [];
}
// 被 setTimeout 延遲,會晚一點收到回應
@Get()
async getAsyncData() {
return new Promise((resolve) => {
setTimeout(() => resolve([]), 1000);
})
}
RxJS 模式
回傳一個 Observable 物件 of,NestJS 會訂閱這個物件的狀態,of 後面可以鏈式串上各種 RxJS 組織資料的方法,整個鏈式的任務結束後會將最後形成的資料送出。
對呼叫它的前端或是其他服務的請求來說,一樣會收到一個非同步的回應,RxJS 的任務只存在於應用程式內部。
import { catchError, map, of } from 'rxjs';
// 使用 RxJS 的鏈式方法重新組織資料
@Get('data/rxjs')
getRxjsData() {
return of(this.todos).pipe(
map((todos) =>
todos.map((todo) => ({
...todo,
status: 'active',
})),
),
catchError((err) => {
console.error('Error occurred:', err);
return of([]);
}),
);
}
函式庫模式
可以從底層的 API 來控制回應內容,需要在 handler 裡面加入裝飾器標記,如 @Request、@Response、@Next,對應到 Express 的 req、res、next,加上標記後就如使用 Express 一樣產生回應:
@Get('data/lib')
getLibraryData(@Res() res: Response) {
res.status(200).send('這是從 library 來的資料');
}
通常會在:
- 串流任務 (streaming)
- 某些套件只支援 Express 的回應物件
- 完全控制回應程序的設定與資料
才會使用到函式庫模式。因為會繞過 NestJS 的設定與元件流程,所以回應內容與格式要自行重新調整。
小結
controller 的功能與一般 MVC 架構雷同,只是大部分的操作都需要用裝飾器取代,需要花點時間轉換,但只要記得 裝飾器是一種函式,給出對應的參數就能得到相應的操作,減少反覆宣告、賦值等等的程式碼。