Skip to content

中间件

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

典型的 AdonisJS 应用程序使用中间件来解析请求体管理用户会话认证请求服务静态资源等。

你也可以创建自定义中间件来在 HTTP 请求期间执行其他任务。

中间件栈

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

服务器中间件栈

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

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

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

ts
import server from '@adonisjs/core/services/server'

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

路由器中间件栈

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

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

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

ts
import router from '@adonisjs/core/services/router'

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

命名中间件集合

命名中间件是除非显式分配给路由或组,否则不会执行的中间件集合。

我们建议你创建专用的中间件类,将它们存储在命名中间件集合中,然后将它们分配给路由,而不是在路由文件中定义中间件作为内联回调。

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

ts
import router from '@adonisjs/core/services/router'

router.named({
  auth: () => import('#middleware/auth_middleware')
})

创建中间件

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

另请参阅:Make middleware 命令

sh
node ace make:middleware user_location

上述命令将在 middleware 目录下创建 user_location_middleware.ts 文件。

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

ts
// title: 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 方法中,中间件必须决定是继续处理请求、通过发送响应来完成请求,还是引发异常来中止请求。

中止请求

如果中间件引发异常,所有即将到来的中间件和路由处理程序将不会执行,异常将交给全局异常处理程序。

ts
import { Exception } from '@adonisjs/core/exceptions'
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'

export default class UserLocationMiddleware {
  async handle(ctx: HttpContext, next: NextFn) {
    const userLocation = ctx.request.header('user-location')

    if (!userLocation) {
      throw new Exception('无法检测用户位置', {
        status: 400
      })
    }

    await next()
  }
}

结束请求

中间件可以通过使用 response.send 方法发送响应来结束请求。此时,next 方法不应该被调用。

ts
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'

export default class MaintenanceModeMiddleware {
  async handle({ response }: HttpContext, next: NextFn) {
    if (isUnderMaintenance) {
      return response.serviceUnavailable('稍后再试')
    }

    await next()
  }
}

调用下一个中间件

如果中间件决定继续处理请求,它必须调用 next 方法。

ts
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'

export default class UserLocationMiddleware {
  async handle({ request }: HttpContext, next: NextFn) {
    const userLocation = request.header('user-location')

    if (userLocation) {
      // 将用户位置附加到请求
    }

    await next()
  }
}

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

命名集合中注册的中间件可以分配给路由或路由组。

ts
// title: start/routes.ts
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'

router
  .get('dashboard', () => {})
  .use(middleware.auth())

你可以将多个中间件作为数组分配。

ts
router
  .get('dashboard', () => {})
  .use([
    middleware.auth(),
    middleware.acl()
  ])

分配给路由组

ts
router
  .group(() => {
    router.get('dashboard', () => {})
    router.get('settings', () => {})
  })
  .use(middleware.auth())

中间件执行流程

AdonisJS 中间件管道使用责任链设计模式。通过此设计,中间件能够在请求(下游)和响应(上游)阶段执行操作。

以下是中间件管道如何处理请求的可视化表示:

┌─────────────────────────────────────────────────────────┐
│ 服务器中间件                                               │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 路由器中间件                                          │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ 命名中间件                                       │ │ │
│ │ │ ┌─────────────────────────────────────────────┐ │ │ │
│ │ │ │                                             │ │ │ │
│ │ │ │ 路由处理程序                                 │ │ │ │
│ │ │ │                                             │ │ │ │
│ │ │ └─────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘

依赖注入

中间件类使用 IoC 容器实例化;因此,你可以在中间件构造函数或 handle 方法中类型提示依赖项。

ts
import { inject } from '@adonisjs/core'
import { HttpContext } from '@adonisjs/core/http'
import { NextFn } from '@adonisjs/core/types/http'
import { GeoIpService } from '#services/geo_ip_service'

@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)
  }
}

向中间件传递数据

你可以在注册中间件时通过传递参数向中间件传递数据。例如:

ts
// title: start/kernel.ts
export const middleware = router.named({
  auth: () => import('#middleware/auth_middleware')
})
ts
// title: start/routes.ts
import { middleware } from '#start/kernel'

router
  .get('dashboard', () => {})
  .use(middleware.auth({
    guards: ['web', 'api']
  }))

在上面的例子中,auth 中间件将在其 handle 方法中接收 options 对象作为第三个参数。

ts
// title: app/middleware/auth_middleware.ts
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'

type AuthMiddlewareOptions = {
  guards: string[]
}

export default class AuthMiddleware {
  async handle(
    ctx: HttpContext,
    next: NextFn,
    options: AuthMiddlewareOptions
  ) {
    console.log(options)
    await next()
  }
}