本系列文已出版成書「NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式」,感謝 iT 邦幫忙與博碩文化的協助。如果對 NestJS 有興趣、覺得這個系列文對你有幫助的話,歡迎前往購書,你的支持是我最大的寫作動力!
Middleware 是一種執行於路由處理之前的函式,可以存取請求物件與回應物件,並透過 next()
繼續完成後續的流程,比如說:執行下一個 Middleware、進入正式的請求資源流程。如果有使用過 Express 的話,可能對 Middleware 不會太陌生,事實上 Nest 的 Middleware 與 Express 是一樣的。那 Middleware 有哪些功用呢?下方為 Express 官方的說明:
next()
呼叫下一個執行步驟。Middleware 有兩種設計方式,一般的 function
或 帶有 @Injectable
裝飾器並實作 NestMiddleware
介面的 class
:
這種 Middleware 十分單純,就是一個普通的 function
,不過有三個參數,分別是:Request
、Response
以及 NextFunction
,使用方法與 Express middleware 是一樣的。下方為一個簡單的範例,可以看到在函式的結尾呼叫了 next()
,表示將執行下一個執行步驟:
import { Request, Response, NextFunction } from 'express';
export function logger(req: Request, res: Response, next: NextFunction) {
console.log('Hello Request!');
next();
}
這種 Middleware 可以透過 CLI 產生:
$ nest generate middleware <MIDDLEWARE_NAME>
注意:
<MIDDLEWARE_NAME>
可以含有路徑,如:middlewares/logger
,這樣就會在src
資料夾下建立該路徑並含有 Middleware。
這邊我建立一個 LoggerMiddleware
在 middlewares
資料夾下:
$ nest generate middleware middlewares/logger
在 src
底下會看見一個名為 middlewares
的資料夾,裡面有 logger.middleware.ts
以及 logger.middleware.spec.ts
:
建立出來的 Middleware 骨架如下,會看到有一個 use(req: any, res: any, next: () => void)
方法,那正是處理邏輯的地方:
import { Injectable, NestMiddleware } from '@nestjs/common';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
next();
}
}
等等,為何參數型別是 any
?原因是要看使用的底層為何,假設是 Express 就改成下方的樣子:
import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
next();
}
}
Middleware 的使用方式並不是透過裝飾器的方式來設定,而是在 Module 實作 NestModule
介面並設計 configure()
方法,再透過 MiddlewareConsumer
這個 Helper Class 來管理各個 Middleware。下方來實作一遍最基礎的 Middleware 使用方式,先將 LoggerMiddleware
調整為下方的樣子:
import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Hello Request!');
next();
}
}
接著建立 TodoModule
與 TodoController
:
$ nest generate module features/todo
$ nest generate controller features/todo
提醒:如果已經建立過可以略過此步驟。
調整 todo.controller.ts
:
import { Controller, Get, Param } from '@nestjs/common';
@Controller('todos')
export class TodoController {
@Get()
getAll() {
return [];
}
@Get(':id')
get(@Param('id') id: string) {
return { id };
}
}
在 AppModule
實作 NestModule
與 configure(consumer: MiddlewareConsumer)
方法,並透過 apply
來套用 Middleware,再透過 forRoutes
設置要採用此 Middleware 的路由:
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodoModule } from './features/todo/todo.module';
import { LoggerMiddleware } from './middlewares/logger.middleware';
@Module({
imports: [TodoModule],
controllers: [AppController],
providers: [
AppService
]
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes('/todos')
}
}
打開瀏覽器查看 http://localhost:3000/todos 與 http://localhost:3000/todos/1,會在終端機看到下方結果;而查看 http://localhost:3000 終端機則不會有任何顯示:
Hello Request!
forRoutes
支援多個路由,只需要添加路由到參數中即可,比較特別的是可以指定特定 Http Method 與路由,將含有 path
與 method
的物件帶入 forRoutes
中即可。這裡調整一下 AppModule
:
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodoModule } from './features/todo/todo.module';
import { LoggerMiddleware } from './middlewares/logger.middleware';
@Module({
imports: [TodoModule],
controllers: [AppController],
providers: [
AppService
]
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes(
{ path: '/todos', method: RequestMethod.POST }, // POST /todos 會生效
{ path: '/', method: RequestMethod.GET } // GET / 會生效
)
}
}
透過瀏覽器查看 http://localhost:3000/todos 在終端機不會有任何顯示結果;但如果查看 http://localhost:3000 就會看到下方結果:
Hello Request!
提醒:
forRoutes
也支援萬用路由。
forRoutes
也支援套用整個 Controller,只要在該 Controller 下的資源都能觸發指定的 Middleware。這裡調整 AppModule
:
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodoController } from './features/todo/todo.controller';
import { TodoModule } from './features/todo/todo.module';
import { LoggerMiddleware } from './middlewares/logger.middleware';
@Module({
imports: [TodoModule],
controllers: [AppController],
providers: [
AppService
]
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes(TodoController)
}
}
透過瀏覽器查看 http://localhost:3000/todos 以及 http://localhost:3000/todos/1 都可以在終端機看到以下結果:
Hello Request!
可以透過 exclude
來指定要被排除的路由,使用方式與 forRoutes
差不多,透過給定含有 path
與 method
的物件來設置。調整一下 AppModule
:
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodoController } from './features/todo/todo.controller';
import { TodoModule } from './features/todo/todo.module';
import { LoggerMiddleware } from './middlewares/logger.middleware';
@Module({
imports: [TodoModule],
controllers: [AppController],
providers: [
AppService
]
})
export class AppModule implements NestModule{
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).exclude(
{ path: '/todos', method: RequestMethod.GET } // 排除 GET /todos
).forRoutes(TodoController)
}
}
透過瀏覽器查看 http://localhost:3000/todos 會發現終端機沒有任何結果。
apply
支援採用多個 Middleware,只需把 Middleware 添加到參數中即可。這裡先新增一個 HelloWorldMiddleware
:
$ nest generate middleware middlewares/hello-world
並修改 hello-world.middleware.ts
的內容:
import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class HelloWorldMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Hello World!');
next();
}
}
接著調整 AppModule
的內容:
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodoController } from './features/todo/todo.controller';
import { TodoModule } from './features/todo/todo.module';
import { HelloWorldMiddleware } from './middlewares/hello-world.middleware';
import { LoggerMiddleware } from './middlewares/logger.middleware';
@Module({
imports: [TodoModule],
controllers: [AppController],
providers: [
AppService
]
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware, HelloWorldMiddleware).forRoutes(TodoController)
}
}
透過瀏覽器查看 http://localhost:3000/todos 可以在終端機看到以下結果:
Hello Request!
Hello World!
如果要將 Middleware 套用在每一個資源上,可以在 main.ts
進行調整,只需要使用 use
方法即可:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { logger } from './middlewares/logger.middleware';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);
}
bootstrap();
注意:在
main.ts
的方式僅支援 Function Middleware。
如果是 Class Middleware 則在 AppModule
實作 NestModule
,並指定路由為 *
即可:
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodoModule } from './features/todo/todo.module';
import { LoggerMiddleware } from './middlewares/logger.middleware';
@Module({
imports: [TodoModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes('*');
}
}
Middleware 的用途非常廣泛,有許多應用都是基於 Middleware 進行實作的,比如:cors
。
提醒:有關 cors 的介紹後續會再說明,或是可以看這篇。
這邊附上今天的懶人包:
NestModule
介面並設計 configure()
方法,再透過 MiddlewareConsumer
管理各個 Middleware。您好,在这一块
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes('/todos')
}
}
打開瀏覽器查看 http://localhost:3000/todos,會在終端機看到下方結果;而查看 http://localhost:3000 或 http://localhost:3000/todos/1 終端機則不會有任何顯示
尝试了一下,路由并不是精确匹配呢,http://localhost:3000/todos/1也是可以看到结果的。
你好,這部分確實是指 /todos
底下的路由,已經修正文章內容,謝謝你的提醒!