HTTP 上下文
本指南介绍 AdonisJS 中的 HTTP 上下文对象。你将学习:
- 什么是 HTTP 上下文,以及它为什么存在
- 如何在路由处理器和中间件中访问它
- HTTP 上下文暴露了哪些属性,以及每个属性何时可用
- 如何使用依赖注入将其注入到服务中
- 如何向上下文中添加自定义属性
- 如何通过异步本地存储(async local storage)访问它
概述
HTTP 上下文 是一个请求作用域的对象,包含了处理 HTTP 请求所需的全部内容。它包含 request、response、auth、logger、session 等属性。AdonisJS 会为每个传入的请求创建一个全新的 HTTP 上下文实例,并将其传递给你的路由处理器和中间件。
HTTP 上下文提供了一种干净、类型安全的方式,在一个地方访问所有请求特定的数据和服务,而不是使用全局变量或从不同的模块导入 request/response 对象。上下文上的每个属性都特定于当前请求,确保并发请求之间完全隔离。
访问 HTTP 上下文
在路由处理器中
访问 HTTP 上下文最常见的方式是在路由处理器中将其作为参数接收。你通常只解构所需的属性,而不是使用完整的上下文对象。
import router from '@adonisjs/core/services/router'
router.get('/posts/:id', async ({ params, request, response }) => {
const id = params.id
const include = request.qs().include
return response.json({ id, include })
})
使用控制器时,模式完全相同。控制器方法将 HTTP 上下文作为其第一个参数接收:
import { HttpContext } from '@adonisjs/core/http'
import Post from '#models/post'
export default class PostsController {
async show({ params, response }: HttpContext) {
const post = await Post.findOrFail(params.id)
return response.json(post)
}
}
在中间件中
中间件函数同样将 HTTP 上下文作为其第一个参数接收。第二个参数是 next 函数,用于将控制权传递给链中的下一个中间件。下面使用的 logger 已经是请求作用域的,因此每条日志行都会自动标记当前请求 id。
import { HttpContext } from '@adonisjs/core/http'
import { NextFn } from '@adonisjs/core/types/http'
export default class LogRequestMiddleware {
async handle({ request, logger }: HttpContext, next: NextFn) {
logger.info(`${request.method()} ${request.url()}`)
await next()
}
}
可用属性
HTTP 上下文将处理请求所需的全部内容捆绑在一起:传入的数据、你正在构建的响应、日志记录、身份验证等。上下文上的属性来自三个来源:核心框架、你安装的可选包,以及项目内部的中间件或服务提供者。
始终可用
这些属性由框架本身附加到每个 HTTP 上下文实例上。
request
HttpRequest
实例暴露传入的请求数据:查询字符串、请求体、请求头、Cookie、上传的文件、客户端 IP、主机名以及 HTTP 方法。每当需要读取客户端发送的内容时,都可以使用它。
const page = ctx.request.input('page', 1)
const agent = ctx.request.header('user-agent')
response
HttpResponse
实例用于构建并发送响应。它提供了设置状态码、请求头和 Cookie,发送 JSON、HTML、文件或渲染的视图,重定向客户端,流式传输内容,以及提前中止请求的方法。
return ctx.response.status(201).json({ id: post.id })
params
一个普通对象,保存从 URL 中解析出的路由参数。对于定义为 /posts/:id/comments/:commentId 的路由,向 /posts/1/comments/42 发起的请求会使 params 等于 { id: '1', commentId: '42' }。参数值始终是字符串。如果你需要其他类型,请在处理器内部转换它们。
route
对匹配当前请求的
路由
定义的引用,包含模式、HTTP 方法、已注册的中间件栈以及处理器。对于未匹配任何路由的请求,该值为 undefined,这通常只发生在异常处理器内部。
logger
一个请求作用域的
Logger
实例。通过 ctx.logger 写入的日志行会自动标记一个唯一的请求 id,这使得在应用中追踪属于单个请求的所有输出变得简单。
ctx.logger.info({ postId: id }, 'Loading post')
随可选包可用
这些属性由可选包提供。只有当相应的包已安装并且其(适用的)中间件已注册时,它们才存在于上下文中。
session
一个
Session
实例,用于读取和写入当前请求的会话数据(包括闪现消息)。当 @adonisjs/session 已安装且会话中间件已注册时可用。
auth
一个
Authenticator
实例,用于验证请求并访问当前登录的用户。当 @adonisjs/auth 已安装且 auth 中间件已注册时可用。
view
一个作用于当前请求的
Edge
渲染器,用于渲染服务端模板。当 edge.js 已安装并通过 view provider 注册时可用。
inertia
一个
Inertia
实例,用于通过 Inertia.js 渲染 React 或 Vue 页面。当 @adonisjs/inertia 已安装且 Inertia 中间件已注册时可用。
bouncer
一个
Bouncer
实例,用于通过 Bouncer 能力和策略针对已认证用户对操作进行授权。当 @adonisjs/bouncer 已安装时可用。
i18n
一个作用于请求所检测语言的
I18n
实例,用于翻译消息以及格式化日期、数字和货币。当 @adonisjs/i18n 已安装且 i18n 中间件已注册时可用。
由项目脚手架添加
这些属性由项目内部的中间件或服务提供者附加,而非由框架或已安装的包提供。它们的可用性取决于你的项目脚手架包含哪些文件,因此即使框架和包的版本发生变化,它们也会随你的项目保留。
containerResolver
一个请求作用域的 IoC 容器解析器,由每个新 AdonisJS 项目自带的 container_bindings_middleware 文件附加到上下文。当你需要手动从容器中解析类同时保留当前请求作用域时使用,例如实例化一个在其构造函数中类型提示了 HttpContext 的服务。
serialize
一个用于类型安全序列化响应负载的辅助函数,由 Inertia 和 API 起步套件自带的 providers/api_provider.ts 文件附加到上下文。从其他起步套件脚手架生成的项目,或者移除了该 provider 的项目,将没有 ctx.serialize 可用。
使用依赖注入
当你需要在服务类或代码库的其他部分访问 HTTP 上下文时,可以使用依赖注入。这种模式比通过多个函数调用手动传递上下文更干净。
注入到服务中
要将 HTTP 上下文注入到服务中,在构造函数中进行类型提示,并用 @inject() 装饰器标记该类。
import { inject } from '@adonisjs/core'
import { HttpContext } from '@adonisjs/core/http'
import Post from '#models/post'
@inject()
export default class PostService {
constructor(protected ctx: HttpContext) {}
async getVisiblePosts() {
const user = this.ctx.auth.user
if (user?.isAdmin) {
return Post.all()
}
return Post.query().where('published', true)
}
}
在控制器中使用服务
要启用自动依赖注入,你必须将 PostService 注入到控制器方法中。当容器调用 index 方法时,它会构建完整的依赖树,并将 HTTP 上下文提供给 PostService。
import { inject } from '@adonisjs/core'
import { HttpContext } from '@adonisjs/core/http'
import PostService from '#services/post_service'
export default class PostsController {
@inject()
async index({}: HttpContext, postService: PostService) {
return postService.getVisiblePosts()
}
}
使用异步本地存储
异步本地存储(ALS)允许你从应用的任何位置访问 HTTP 上下文,而无需显式地通过函数参数传递它。
优先将依赖注入或通过引用传递 HttpContext 作为你的默认模式。只有在那些方式不可行时才使用 ALS,例如在无法重构为显式接受上下文的工具函数或第三方库中。
要使用 ALS,你必须先在 config/app.ts 文件的 http 块中启用它。
import { defineConfig } from '@adonisjs/core/http'
export const http = defineConfig({
useAsyncLocalStorage: true
})
启用后,你可以使用静态的 getOrFail() 方法访问当前 HTTP 上下文。
import { HttpContext } from '@adonisjs/core/http'
export default class AnalyticsService {
trackEvent(eventName: string) {
const ctx = HttpContext.getOrFail()
ctx.logger.info({
event: eventName,
userId: ctx.auth.user?.id,
ip: ctx.request.ip(),
})
}
}
启用异步本地存储会带来轻微的性能开销,因此除非你确实需要使用 HttpContext.getOrFail(),否则请保持禁用。
在 HTTP 请求生命周期之外调用 HttpContext.getOrFail() 会抛出异常。只应从你确定在请求期间运行的代码路径中调用它(路由处理器、中间件、控制器,以及它们调用的服务)。对于可能在请求之外运行的代码,例如后台任务或 CLI 命令,请改为显式注入或传递 HttpContext。
添加自定义属性
你可以通过在中间件文件中扩展 HttpContext 接口,向 HTTP 上下文添加自定义属性。当你需要在同一请求期间跨多个中间件和路由处理器共享数据时,这很有用。
import { HttpContext } from '@adonisjs/core/http'
import { NextFn } from '@adonisjs/core/types/http'
import Tenant from '#models/tenant'
/**
* 扩展 HttpContext 接口以添加 tenant 属性。
* 这让 TypeScript 知晓这个新属性。
*/
declare module '@adonisjs/core/http' {
interface HttpContext {
tenant: Tenant
}
}
export default class IdentifyTenantMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
/**
* 从请求中提取租户标识符。
* 这可以来自子域名、请求头或其他来源。
*/
const subdomain = ctx.request.hostname().split('.')[0]
/**
* 查找租户并将其附加到上下文。
* 现在所有路由处理器都可以访问 ctx.tenant。
*/
ctx.tenant = await Tenant.findByOrFail('subdomain', subdomain)
await next()
}
}
为测试创建虚拟上下文
编写测试时,你经常需要一个虚拟的 HTTP 上下文实例来测试路由处理器、中间件或服务。使用 testUtils 服务来创建这些测试实例。
import testUtils from '@adonisjs/core/services/test_utils'
const ctx = testUtils.createHttpContext()
创建的上下文实例未附加到任何路由,因此 ctx.route 和 ctx.params 为 undefined。如果被测试的代码读取它们,请在运行断言之前在返回的上下文上手动赋值。
createHttpContext 方法默认对底层的 Node.js req 和 res 对象使用伪值。如果你需要使用真实的请求和响应对象进行测试,可以提供它们。
import { createServer } from 'node:http'
import testUtils from '@adonisjs/core/services/test_utils'
createServer((req, res) => {
const ctx = testUtils.createHttpContext({ req, res })
})
如果你在 AdonisJS 应用之外构建包,可以直接使用 HttpContextFactory 类。testUtils 服务仅在 AdonisJS 应用内可用,但工厂类在任何地方都能工作。
import { HttpContextFactory } from '@adonisjs/core/factories/http'
const ctx = new HttpContextFactory().create()