iT邦幫忙

2021 iThome 鐵人賽

DAY 11
0
Modern Web

NestJS 帶你飛!系列 第 11

[NestJS 帶你飛!] DAY11 - Middleware

什麼是 Middleware?

Middleware 是一種執行於路由處理之前的函式,可以存取請求物件與回應物件,並透過 next() 繼續完成後續的流程,比如說:執行下一個 Middleware、進入正式的請求資源流程。如果有使用過 Express 的話,可能對 Middleware 不會太陌生,事實上 Nest 的 Middleware 與 Express 是一樣的。那 Middleware 有哪些功用呢?下方為 Express 官方的說明:

  • 可執行任何程式。
  • 更改請求物件或回應物件。
  • 結束整個請求週期。
  • 呼叫下一個執行步驟。
  • 如果在 Middleware 沒有結束掉請求週期,需要使用 next() 呼叫下一個執行步驟。

https://ithelp.ithome.com.tw/upload/images/20210402/201193386ebEEBHfmF.png

設計 Middleware

Middleware 有兩種設計方式,一般的 function 或 帶有 @Injectable 裝飾器並實作 NestMiddleware 介面的 class

Functional middleware

這種 Middleware 十分單純,就是一個普通的 function,不過有三個參數,分別是:RequestResponse 以及 NextFunction,使用方法與 Express middleware 是一樣的。下方為一個簡單的範例,可以看到在函式的結尾呼叫了 next(),表示將執行下一個執行步驟:

import { Request, Response, NextFunction } from 'express';

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log('Hello Request!');
  next();
}

Class middleware

這種 Middleware 可以透過 CLI 產生:

$ nest generate middleware <MIDDLEWARE_NAME>

注意<MIDDLEWARE_NAME> 可以含有路徑,如:middlewares/logger,這樣就會在 src 資料夾下建立該路徑並含有 Middleware。

這邊我建立一個 LoggerMiddlewaremiddlewares 資料夾下:

$ nest generate middleware middlewares/logger

src 底下會看見一個名為 middlewares 的資料夾,裡面有 logger.middleware.ts 以及 logger.middleware.spec.ts
https://ithelp.ithome.com.tw/upload/images/20210403/20119338Z2gyRBgQXT.png

建立出來的 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

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();
  }
}

接著建立 TodoModuleTodoController

$ 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 實作 NestModuleconfigure(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/todoshttp://localhost:3000/todos/1,會在終端機看到下方結果;而查看 http://localhost:3000 終端機則不會有任何顯示:

Hello Request!

套用多個路由與指定 HttpMethod

forRoutes 支援多個路由,只需要添加路由到參數中即可,比較特別的是可以指定特定 Http Method 與路由,將含有 pathmethod 的物件帶入 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 也支援萬用路由。

套用 Controller

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!

排除特定路由與指定 HttpMethod

可以透過 exclude 來指定要被排除的路由,使用方式與 forRoutes 差不多,透過給定含有 pathmethod 的物件來設置。調整一下 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 會發現終端機沒有任何結果。

套用多個 Middleware

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

如果要將 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 的介紹後續會再說明,或是可以看這篇

這邊附上今天的懶人包:

  1. Middleware 是一種執行於路由處理之前的函式,其可以存取請求物件與回應物件。
  2. Middleware 有兩種設計方式:Functional middleware 與 Class middleware。
  3. 在 Module 實作 NestModule 介面並設計 configure() 方法,再透過 MiddlewareConsumer 管理各個 Middleware。
  4. 可以把一個或多個 Middleware 套用在一個或多個路由、HttpMethod 或 Controller 上。
  5. 可以排除特定路由,讓該路由不套用 Middleware。
  6. 可以將 Middleware 套用至全域。

上一篇
[NestJS 帶你飛!] DAY10 - Pipe (下)
下一篇
[NestJS 帶你飛!] DAY12 - Interceptor
系列文
NestJS 帶你飛!32

1 則留言

0
mihuartuanr
iT邦新手 5 級 ‧ 2021-11-04 20:36:21

您好,在这一块

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('/todos')
  }
}

打開瀏覽器查看 http://localhost:3000/todos,會在終端機看到下方結果;而查看 http://localhost:3000http://localhost:3000/todos/1 終端機則不會有任何顯示

尝试了一下,路由并不是精确匹配呢,http://localhost:3000/todos/1也是可以看到结果的。

HAO iT邦新手 3 級 ‧ 2021-11-05 10:12:19 檢舉

你好,這部分確實是指 /todos 底下的路由,已經修正文章內容,謝謝你的提醒!

我要留言

立即登入留言