中间件

中间件

中间件是在 HTTP 请求到达路由处理器之前执行的一系列函数。链中的每一个函数都可以结束请求,也可以将其转发给下一个中间件。

一个典型的 AdonisJS 应用使用中间件来解析请求体管理用户会话认证请求提供静态资源等。

你也可以创建自定义中间件,在 HTTP 请求期间执行额外的任务。

中间件栈

为了让你更好地控制中间件管道的执行,AdonisJS 将中间件栈拆分为以下三组。

服务端中间件栈

服务端中间件在每个 HTTP 请求上都会运行,即使你还没有为当前请求 URL 定义任何路由。

它们非常适合为你的应用添加不依赖框架路由系统的额外功能。例如,静态资源中间件就被注册为服务端中间件。

你可以在 start/kernel.ts 文件中使用 server.use 方法注册服务端中间件。

import server from '@adonisjs/core/services/server'
server.use([
() => import('@adonisjs/static/static_middleware')
])

路由中间件栈

路由中间件也被称为全局中间件。它们会在每个有匹配路由的 HTTP 请求上执行。

Bodyparser、auth 和 session 中间件都注册在路由中间件栈之下。

你可以在 start/kernel.ts 文件中使用 router.use 方法注册路由中间件。

import router from '@adonisjs/core/services/router'
router.use([
() => import('@adonisjs/core/bodyparser_middleware')
])

具名中间件集合

具名中间件是一个中间件集合,除非被显式分配给某个路由或组,否则不会执行。

我们建议你创建专门的控制器类,将它们存放在具名中间件集合中,然后再分配给路由,而不是在路由文件中将中间件定义为内联回调。

你可以在 start/kernel.ts 文件中使用 router.named 方法定义具名中间件。请确保导出这个具名集合,以便能够在路由文件内使用它。

import router from '@adonisjs/core/services/router'
router.named({
auth: () => import('#middleware/auth_middleware')
})

创建中间件

中间件存放在 ./app/middleware 目录中,你可以通过运行 make:middleware ace 命令创建一个新的中间件文件。

另请参阅:Make middleware 命令

node ace make:middleware user_location

上述命令会在中间件目录下创建 user_location_middleware.ts 文件。

中间件用一个带有 handle 方法的类表示。在执行期间,AdonisJS 会自动调用此方法,并将 HttpContext 作为第一个参数传给它。

app/middleware/user_location_middleware.ts
import { HttpContext } from '@adonisjs/core/http'
import { NextFn } from '@adonisjs/core/types/http'
export default class UserLocationMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
}
}

handle 方法内部,中间件必须决定是继续处理请求、通过发送响应结束请求,还是抛出异常来中止请求。

中止请求

如果中间件抛出了一个异常,后续所有中间件和路由处理器都不会执行,该异常会被交给全局异常处理器。

import { Exception } from '@adonisjs/core/exceptions'
import { NextFn } from '@adonisjs/core/types/http'
export default class UserLocationMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
throw new Exception('Aborting request')
}
}

继续处理请求

你必须调用 next 方法来继续处理请求。否则,中间件栈内其余的动作都不会执行。

export default class UserLocationMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
// 调用 `next` 函数以继续
await next()
}
}

发送响应,且不调用 next 方法

最后,你可以通过发送响应来结束请求。这种情况下,不要调用 next 方法。

export default class UserLocationMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
// 发送响应 + 不调用 next
ctx.response.send('Ending request')
}
}

将中间件分配给路由和路由组

具名中间件集合默认未被使用,你必须显式地将它们分配给路由或路由组。

在下面的示例中,我们先导入 middleware 集合,并将 userLocation 中间件分配给一个路由。

import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
router
.get('posts', () => {})
.use(middleware.userLocation())

可以应用多个中间件,既可以使用数组,也可以通过多次调用 use 方法。

router
.get('posts', () => {})
.use([
middleware.userLocation(),
middleware.auth()
])

类似地,你也可以将中间件分配给一个路由组。组中间件会自动应用到组内所有路由上。

import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
router.group(() => {
router.get('posts', () => {})
router.get('users', () => {})
router.get('payments', () => {})
}).use(middleware.userLocation())

中间件参数

注册在具名中间件集合下的中间件,可以将一个额外的参数作为 handle 方法参数的一部分。例如,auth 中间件接受身份认证守卫(guard)作为配置选项。

type AuthGuards = 'web' | 'api'
export default class AuthMiddleware {
async handle(ctx, next, options: { guard: AuthGuards }) {
}
}

在将中间件分配给路由时,你可以指定要使用的守卫。

import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
router.get('payments', () => {}).use(
middleware.auth({ guard: 'web' })
)

依赖注入

中间件类是使用 IoC 容器 实例化的;因此,你可以在中间件构造函数中类型提示依赖,容器会为你注入它们。

假设你有一个 GeoIpService 类,用于根据请求 IP 查找用户位置,你可以使用 @inject 装饰器将其注入到中间件中。

app/services/geoip_service.ts
export class GeoIpService {
async lookup(ipAddress: string) {
// 查找位置并返回
}
}
import { inject } from '@adonisjs/core'
import { GeoIpService } from '#services/geoip_service'
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
@inject()
export default class UserLocationMiddleware {
constructor(protected geoIpService: GeoIpService) {
}
async handle(ctx: HttpContext, next: NextFn) {
const ip = ctx.request.ip()
ctx.location = await this.geoIpService.lookup(ip)
}
}

中间件执行流程

AdonisJS 的中间件层构建在 责任链 设计模式之上。一个中间件有两个执行阶段:下游阶段上游阶段

  • 下游阶段是你在调用 next 方法之前编写的代码块。在这个阶段,你处理请求。
  • 上游阶段是你在调用 next 方法之后可能编写的代码块。在这个阶段,你可以检查响应,或者完全修改它。

中间件与异常处理

AdonisJS 会自动捕获由中间件管道或路由处理器抛出的异常,并使用全局异常处理器 将其转换为 HTTP 响应。

因此,你不必将 next 函数调用包裹在 try/catch 语句中。此外,自动异常处理确保了中间件的上游逻辑总是在 next 函数调用之后执行。

从中间件修改响应

中间件的上游阶段可以修改响应体、响应头和状态码。这样做会丢弃路由处理器或任何其他中间件设置的旧响应。

在修改响应之前,你必须确保处理的是正确的响应类型。以下是 Response 类中的响应类型列表。

  • 标准响应指使用 response.send 方法发送数据值。其值可能是 ArrayObjectStringBooleanBuffer
  • 流式响应指使用 response.stream 方法将流管道连接到响应 socket。
  • 文件下载响应指使用 response.download 方法下载文件。

根据你处理的响应类型,你能否访问特定的响应属性会有所不同。

处理标准响应

修改标准响应时,你可以使用 response.content 属性访问它。务必先检查 content 是否存在。

import { HttpContext } from '@adonisjs/core/http'
import { NextFn } from '@adonisjs/core/types/http'
export default class {
async handle({ response }: HttpContext, next: NextFn) {
await next()
if (response.hasContent) {
console.log(response.content)
console.log(typeof response.content)
response.send(newResponse)
}
}
}

处理流式响应

使用 response.stream 方法设置的响应流不会立即管道连接到传出 HTTP 响应。相反,AdonisJS 会等待路由处理器和中间件管道完成。

因此,在中间件内部,你可以用一个新流替换现有流,或者定义事件处理器来监控该流。

import { HttpContext } from '@adonisjs/core/http'
import { NextFn } from '@adonisjs/core/types/http'
export default class {
async handle({ response }: HttpContext, next: NextFn) {
await next()
if (response.hasStream) {
response.outgoingStream.on('data', (chunk) => {
console.log(chunk)
})
}
}
}

处理文件下载

使用 response.downloadresponse.attachment 方法执行的文件下载,会推迟到路由处理器和中间件管道完成后才进行。

因此,在中间件内部,你可以替换要下载文件的路径。

import { HttpContext } from '@adonisjs/core/http'
import { NextFn } from '@adonisjs/core/types/http'
export default class {
async handle({ response }: HttpContext, next: NextFn) {
await next()
if (response.hasFileToStream) {
console.log(response.fileToStream.generateEtag)
console.log(response.fileToStream.path)
}
}
}

测试中间件类

将中间件创建为类,可以让你轻松地对单个中间件进行隔离测试(即单元测试中间件)。有几种不同的方式来测试中间件。让我们探索所有可用的选项。

最简单的选项是创建中间件类的新实例,并使用 HTTP 上下文和 next 回调函数来调用 handle 方法。

import testUtils from '@adonisjs/core/services/test_utils'
import GeoIpService from '#services/geoip_service'
import UserLocationMiddleware from '#middleware/user_location_middleware'
const middleware = new UserLocationMiddleware(
new GeoIpService()
)
const ctx = testUtils.createHttpContext()
await middleware.handle(ctx, () => {
console.log('Next function invoked')
})

testUtils 服务仅在 AdonisJS 应用启动后才可用。不过,如果你在一个包内测试中间件,可以使用 HttpContextFactory 类,无需启动应用即可创建一个虚拟的 HTTP 上下文实例。

另请参阅:CORS 中间件测试 的真实示例。

import {
RequestFactory,
ResponseFactory,
HttpContextFactory
} from '@adonisjs/core/factories/http'
const request = new RequestFactory().create()
const response = new ResponseFactory().create()
const ctx = new HttpContextFactory()
.merge({ request, response })
.create()
await middleware.handle(ctx, () => {
console.log('Next function invoked')
})

使用服务端管道

如果你的中间件依赖于其他必须先行执行的中间件,你可以使用 server.pipeline 方法组合一个中间件管道。

  • server.pipeline 方法接受一个中间件类数组。
  • 类实例由 IoC 容器创建。
  • 执行流程与 HTTP 请求期间中间件的原始执行流程相同。
import testUtils from '@adonisjs/core/services/test_utils'
import server from '@adonisjs/core/services/server'
import UserLocationMiddleware from '#middleware/user_location_middleware'
const pipeline = server.pipeline([
UserLocationMiddleware
])
const ctx = testUtils.createHttpContext()
await pipeline.run(ctx)

你可以在调用 pipeline.run 方法之前定义 finalHandlererrorHandler 函数。

  • final 处理器在所有中间件执行完毕后被执行。当任何中间件在未调用 next 方法的情况下结束了链时,final 处理器不会被执行。
  • error 处理器在中间件抛出异常时被执行。上游流程会在 error 处理器被调用之后开始。
const ctx = testUtils.createHttpContext()
await pipeline
.finalHandler(() => {
console.log('all middleware called next')
console.log('the upstream logic starts from here')
})
.errorHandler((error) => {
console.log('an exception was raised')
console.log('the upstream logic starts from here')
})
.run(ctx)
console.log('pipeline executed')

server 服务在应用启动后才可用。不过,如果你正在创建一个包,可以使用 ServerFactory 来创建一个 Server 类的实例,而无需启动应用。

import { ServerFactory } from '@adonisjs/core/factories/http'
const server = new ServerFactory().create()
const pipeline = server.pipeline([
UserLocationMiddleware
])