响应

本指南介绍 AdonisJS 的 Response 类,以及用于构建 HTTP 响应的可用方法。你将了解:

  • 以不同格式发送响应体
  • 处理请求头和 Cookie
  • 处理重定向和文件下载
  • 理解响应序列化如何工作
  • 用自定义方法扩展 Response 类

概述

Response 类为构建 AdonisJS 应用中的 HTTP 响应提供了辅助函数。与其直接使用 Node.js 的原始响应对象,Response 类为发送 JSON、设置请求头、处理重定向和流式传输文件下载等常见任务提供了流畅、富有表现力的 API。

Response 类可通过 ctx.response 属性访问。你可以在应用的路由处理器、中间件和异常处理器中访问它。对于许多简单的响应,你可以直接从路由处理器返回值,AdonisJS 会自动使用 Response 类来发送它们。

发送响应体

Response 类提供了多种发送响应体的方式。你可以直接从路由处理器返回值,也可以使用显式的响应方法。

从路由处理器返回值

最简单的方式是直接从路由处理器返回值。AdonisJS 会自动序列化该值并设置适当的内容类型请求头。

start/routes.ts
import router from '@adonisjs/core/services/router'

router.get('/', async () => {
  /**
   * 返回纯文本,content-type: text/plain
   */
  return 'This is the homepage.'
})

router.get('/welcome', async () => {
  /**
   * 返回 HTML 片段,content-type: text/html
   */
  return '<p>This is the homepage</p>'
})

router.get('/api/page', async () => {
  /**
   * 返回 JSON,content-type: application/json
   */
  return { page: 'home' }
})

router.get('/timestamp', async () => {
  /**
   * Date 实例会被转换为 ISO 字符串
   */
  return new Date()
})

使用 response.send()

你也可以显式使用 response.send() 方法,它提供相同的自动内容类型检测。

start/routes.ts
import router from '@adonisjs/core/services/router'

router.get('/', async ({ response }) => {
  /**
   * send() 方法与返回值的工作方式完全相同。
   * 当你需要在发送前设置请求头或状态时很有用。
   */
  response.send('This is the homepage')
})

router.get('/data', async ({ response }) => {
  /**
   * 对象和数组会自动被字符串化
   */
  response.send({ page: 'home' })
})

强制发送 JSON 响应

当你需要确保响应以 JSON 形式发送(即使它可能被检测为 HTML)时,使用 response.json() 方法。

另请参阅: 响应体序列化

app/controllers/posts_controller.ts
import Post from '#models/post'
import type { HttpContext } from '@adonisjs/core/http'

export default class PostsController {
  async index({ response }: HttpContext) {
    const posts = await Post.all()
    
    /**
     * 显式将 content-type 设置为 application/json
     * 并序列化 posts 数组
     */
    response.json(posts)
  }
}

处理请求头

Response 类提供了设置、追加和移除 HTTP 请求头的方法。请求头必须在响应体发送之前设置。

设置请求头

使用 response.header() 方法设置响应头。如果该请求头已存在,它将被覆盖。

app/controllers/api_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class ApiController {
  async index({ response }: HttpContext) {
    /**
     * 设置用于 API 版本控制的自定义请求头
     */
    response.header('X-API-Version', 'v1')
    
    /**
     * 设置缓存控制请求头
     */
    response.header('Cache-Control', 'public, max-age=3600')
    
    return { status: 'ok' }
  }
}

安全地设置请求头

response.safeHeader() 方法仅在请求头尚不存在时才设置它。当你想提供默认值而不覆盖现有请求头时,这很有用。

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

export default class CorsMiddleware {
  async handle({ response }: HttpContext, next: NextFn) {
    /**
     * 仅当其他中间件尚未设置时才设置 CORS 请求头
     */
    response.safeHeader('Access-Control-Allow-Origin', '*')
    
    await next()
  }
}

追加到请求头

某些请求头可以有多个值。使用 response.append() 添加额外的值,而不移除现有的值。

app/controllers/downloads_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class DownloadsController {
  async show({ response }: HttpContext) {
    /**
     * 为不同的 Cookie 追加多个 Set-Cookie 请求头
     */
    response.append('Set-Cookie', 'session=abc123; HttpOnly')
    response.append('Set-Cookie', 'preferences=dark-mode; Path=/')
    
    return { download: 'ready' }
  }
}

移除请求头

使用 response.removeHeader() 移除之前设置的请求头。

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

export default class SecurityMiddleware {
  async handle({ response }: HttpContext, next: NextFn) {
    await next()
    
    /**
     * 移除 server 请求头以隐藏服务器实现细节
     */
    response.removeHeader('X-Powered-By')
  }
}

处理重定向

response.redirect() 方法返回 Redirect 类的实例,该类提供流畅的 API 用于创建带有不同目标和选项的重定向响应。

重定向到路径

使用 response.redirect().toPath() 重定向到特定的 URI 或外部 URL。

app/controllers/auth_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class AuthController {
  async logout({ response, auth }: HttpContext) {
    await auth.logout()
    
    /**
     * 退出登录后重定向到主页
     */
    response.redirect().toPath('/')
  }
  
  async external({ response }: HttpContext) {
    /**
     * 重定向到外部网站
     */
    response.redirect().toPath('https://adonisjs.com')
  }
}

重定向到命名路由

response.redirect().toRoute() 方法接受路由标识符及其参数,使得在 URL 变更时重定向仍然易于维护。

app/controllers/posts_controller.ts
import Post from '#models/post'
import { createPostValidator } from '#validators/post'
import type { HttpContext } from '@adonisjs/core/http'

export default class PostsController {
  async store({ request, response }: HttpContext) {
    /**
     * 验证传入的请求数据
     */
    const payload = await request.validateUsing(createPostValidator)
    
    /**
     * 创建文章
     */
    const post = await Post.create(payload)
    
    /**
     * 重定向到新创建文章的展示页面
     */
    response.redirect().toRoute('posts.show', [post.id])
  }
}

重定向返回

使用 response.redirect().back() 重定向到上一页。该方法读取 Referer 请求头,并验证引用来源的主机是否与当前请求的 Host 请求头匹配。如果引用来源缺失、无效,或来自未知主机,重定向将回退到 /

你可以向 back() 提供一个自定义的回退 URL 作为参数。

app/controllers/comments_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class CommentsController {
  async destroy({ response, params }: HttpContext) {
    /**
     * 重定向回用户来源的页面。
     * 如果找不到有效的引用来源,则回退到 /posts。
     */
    response.redirect().back('/posts')
  }
}

允许外部引用来源主机

默认情况下,back() 只接受与当前请求主机匹配的引用来源。这可以防止开放重定向攻击,即恶意的引用来源可能将用户发送到外部站点。

如果你的应用跨越多个域名(例如 app.example.comadmin.example.com),你可以将受信任的主机添加到 redirect.allowedHosts 配置中,这样 back() 也会接受来自这些域名的引用来源。

config/app.ts
import { defineConfig } from '@adonisjs/core/http'

export const http = defineConfig({
  redirect: {
    allowedHosts: [
      'app.example.com',
      'admin.example.com',
    ],
  },
})

自定义上一页 URL 的解析

当使用 @adonisjs/session 包时,你可以通过设置 redirect.previousUrl 属性将会话中的上一页 URL 存储起来。设置后,back() 将使用会话中的值,而不是 Referer 请求头。

当你的应用将用户重定向到第三方网站(如 OAuth 提供商)时,这很有用。当用户返回时,浏览器的 Referer 请求头将指向外部站点,因此 back() 会回退到 / 而不是用户原本所在的页面。通过在重定向前将预期的返回 URL 存储在会话中,你可以确保 back() 将他们带到正确的位置。

app/controllers/auth_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class AuthController {
  async redirectToProvider({ session, response }: HttpContext) {
    /**
     * 存储当前 URL,以便在 OAuth 流程完成之后
     * back() 返回到这里
     */
    session.put('redirect.previousUrl', '/dashboard')
    response.redirect().toPath('https://accounts.google.com/o/oauth2/auth')
  }

  async handleCallback({ response }: HttpContext) {
    /**
     * 这将重定向到 /dashboard(来自会话)
     * 而不是使用 Referer 请求头
     */
    response.redirect().back()
  }
}
Note

基于会话的上一页 URL 解析需要 @adonisjs/session 包。没有它,back() 将始终使用 Referer 请求头。

设置重定向状态码

默认情况下,重定向使用 302 状态码。使用 status() 方法设置不同的重定向状态。

app/controllers/pages_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class PagesController {
  async oldPage({ response }: HttpContext) {
    /**
     * 为已移动的页面使用永久重定向(301)
     */
    response.redirect().status(301).toPath('/new-page')
  }
}

转发查询字符串

查询字符串转发在 AdonisJS 起步套件中默认通过服务器配置中的 redirect.forwardQueryString 选项启用。你可以在 config/app.ts 文件中验证这一点。

config/app.ts
import { defineConfig } from '@adonisjs/core/http'

export const http = defineConfig({
  redirect: {
    forwardQueryString: true,
  },
})

启用后,所有重定向都会自动将当前 URL 的查询字符串携带到目标。例如,如果当前 URL 是 /search?q=adonis&sort=date,而你重定向到 /results,用户将被发送到 /results?q=adonis&sort=date

你可以通过向 withQs() 传递 false 来禁用特定重定向的转发。

app/controllers/auth_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class AuthController {
  async logout({ response }: HttpContext) {
    /**
     * 禁用此重定向的查询字符串转发。
     * 用户将被发送到 /,不带任何查询参数。
     */
    response.redirect().withQs(false).toPath('/')
  }
}

如果默认未启用查询字符串转发,你可以通过不带参数调用 withQs() 为特定重定向启用它。

app/controllers/search_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class SearchController {
  async filter({ response }: HttpContext) {
    /**
     * 将现有的查询参数转发到新 URL。
     * 如果当前 URL 是 /search?q=adonis&sort=date,
     * 这将重定向到 /results?q=adonis&sort=date
     */
    response.redirect().withQs().toPath('/results')
  }
}

设置自定义查询字符串

withQs() 传递一个对象来设置自定义查询参数。多次链式调用该方法来追加额外的参数。

app/controllers/products_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class ProductsController {
  async search({ response }: HttpContext) {
    /**
     * 使用自定义查询参数重定向。
     * 结果为 /products?category=electronics&sort=price
     */
    response
      .redirect()
      .withQs({ category: 'electronics' })
      .withQs({ sort: 'price' })
      .toPath('/products')
  }
}

重定向到预期 URL

toIntended() 方法重定向到之前通过 session.setIntendedUrl() 存储在会话中的 URL。如果没有存储预期的 URL,它会重定向到提供的回退地址。存储的 URL 在使用后会被消费(从会话中移除)。

withIntendedUrl() 方法将当前请求 URL 作为预期目标存储在会话中。它只对匹配了路由的 GET、导航类请求进行存储。

这些方法在安装了 @adonisjs/session 包时可用。请参阅 登录后重定向到预期 URL 获取完整示例。

app/controllers/session_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class SessionController {
  async store({ auth, request, response }: HttpContext) {
    await auth.use('web').login(
      await User.verifyCredentials(
        request.input('email'),
        request.input('password')
      )
    )

    /**
     * 重定向到预期 URL 或 /dashboard
     */
    return response.redirect().toIntended('/dashboard')
  }
}

流式传输与下载

Response 类提供了流式传输数据和服务文件下载的方法。这些方法会自动处理适当的请求头、缓存和清理。

流式传输响应

使用 response.stream() 将可读流作为响应发送。AdonisJS 会等待路由处理器和上游中间件完成,然后才开始从流中读取。

app/controllers/exports_controller.ts
import { createReadStream } from 'node:fs'
import type { HttpContext } from '@adonisjs/core/http'

export default class ExportsController {
  async generate({ response }: HttpContext) {
    /**
     * 从文件创建一个可读流
     */
    const stream = createReadStream('./exports/data.csv')
    
    /**
     * 将文件流式传输给客户端。
     * AdonisJS 自动处理背压和清理。
     */
    response.stream(stream)
  }
}

下载文件

response.download() 方法流式传输一个文件,并为下载设置适当的请求头。它接受一个文件的绝对路径。

app/controllers/invoices_controller.ts
import app from '@adonisjs/core/services/app'
import type { HttpContext } from '@adonisjs/core/http'

export default class InvoicesController {
  async download({ response, params }: HttpContext) {
    /**
     * 构建指向发票文件的绝对路径
     */
    const filePath = app.makePath(`storage/invoices/${params.id}.pdf`)
    
    /**
     * 流式传输文件以供下载,并支持 ETag。
     * 只要文件内容保持不变,浏览器就会缓存该响应。
     */
    response.download(filePath)
  }
}

强制下载并自定义文件名

使用 response.attachment() 强制下载并指定自定义文件名。浏览器将提示用户以给定名称保存文件。

app/controllers/reports_controller.ts
import app from '@adonisjs/core/services/app'
import type { HttpContext } from '@adonisjs/core/http'

export default class ReportsController {
  async export({ response, params }: HttpContext) {
    /**
     * 指向报表文件的绝对路径
     */
    const filePath = app.makePath(`storage/reports/${params.id}.xlsx`)
    
    /**
     * 以用户友好的文件名强制下载。
     * 第二个参数在 Content-Disposition 请求头中设置文件名。
     */
    response.attachment(filePath, `monthly-report-${params.month}.xlsx`)
  }
}

设置响应状态

Response 类提供了设置 HTTP 状态码的方法。状态码向客户端传达请求的结果。

设置状态码

使用 response.status() 设置 HTTP 状态码。该方法会覆盖任何之前设置的状态码。

app/controllers/api/posts_controller.ts
import Post from '#models/post'
import type { HttpContext } from '@adonisjs/core/http'

export default class PostsController {
  async store({ request, response }: HttpContext) {
    const post = await Post.create(request.all())
    
    /**
     * 成功创建资源时返回 201 Created
     */
    response.status(201)
    
    return post
  }
}

安全地设置状态

response.safeStatus() 方法仅在尚未设置状态码时才设置它。这在中间件中很有用,当你想提供默认值而不覆盖显式设置的状态码时。

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

export default class JsonApiMiddleware {
  async handle({ response }: HttpContext, next: NextFn) {
    await next()
    
    /**
     * 仅在尚未设置时才设置默认的成功状态。
     * 控制器特定的状态码优先。
     */
    response.safeStatus(200)
  }
}

状态简写方法

AdonisJS 提供了在一次调用中同时设置状态码和响应体的简写方法。

app/controllers/users_controller.ts
import User from '#models/user'
import type { HttpContext } from '@adonisjs/core/http'

export default class UsersController {
  async show({ response, params }: HttpContext) {
    const user = await User.find(params.id)
    
    if (!user) {
      /**
       * 一次调用中设置状态 404 并发送响应体
       */
      return response.notFound({ error: 'User not found' })
    }
    
    return user
  }
  
  async destroy({ response, params, auth }: HttpContext) {
    const user = await User.findOrFail(params.id)
    
    if (user.id !== auth.user.id) {
      /**
       * 设置状态 403 并发送错误消息
       */
      return response.forbidden({ error: 'Cannot delete other users' })
    }
    
    await user.delete()
    return response.noContent()
  }
}

响应简写方法

AdonisJS 为常见的 HTTP 状态码提供了简写方法。每个方法都会设置状态码并可选择发送响应体。

方法状态码
response.continue()100
response.switchingProtocols()101
response.ok(body?, generateEtag?)200
response.created(body?, generateEtag?)201
response.accepted(body?, generateEtag?)202
response.nonAuthoritativeInformation(body?, generateEtag?)203
response.noContent()204
response.resetContent()205
response.partialContent(body?, generateEtag?)206
response.multipleChoices(body?, generateEtag?)300
response.movedPermanently(body?, generateEtag?)301
response.movedTemporarily(body?, generateEtag?)302
response.seeOther(body?, generateEtag?)303
response.notModified(body?, generateEtag?)304
response.useProxy(body?, generateEtag?)305
response.temporaryRedirect(body?, generateEtag?)307
response.badRequest(body?, generateEtag?)400
response.unauthorized(body?, generateEtag?)401
response.paymentRequired(body?, generateEtag?)402
response.forbidden(body?, generateEtag?)403
response.notFound(body?, generateEtag?)404
response.methodNotAllowed(body?, generateEtag?)405
response.notAcceptable(body?, generateEtag?)406
response.proxyAuthenticationRequired(body?, generateEtag?)407
response.requestTimeout(body?, generateEtag?)408
response.conflict(body?, generateEtag?)409
response.gone(body?, generateEtag?)410
response.lengthRequired(body?, generateEtag?)411
response.preconditionFailed(body?, generateEtag?)412
response.requestEntityTooLarge(body?, generateEtag?)413
response.requestUriTooLong(body?, generateEtag?)414
response.unsupportedMediaType(body?, generateEtag?)415
response.requestedRangeNotSatisfiable(body?, generateEtag?)416
response.expectationFailed(body?, generateEtag?)417
response.unprocessableEntity(body?, generateEtag?)422
response.tooManyRequests(body?, generateEtag?)429
response.internalServerError(body?, generateEtag?)500
response.notImplemented(body?, generateEtag?)501
response.badGateway(body?, generateEtag?)502
response.serviceUnavailable(body?, generateEtag?)503
response.gatewayTimeout(body?, generateEtag?)504
response.httpVersionNotSupported(body?, generateEtag?)505

Response 类提供了设置不同安全级别 Cookie 的方法。AdonisJS 支持签名 Cookie、加密 Cookie 和普通 Cookie。

response.cookie() 方法创建一个签名 Cookie。签名 Cookie 不能被篡改,但其内容对客户端可见。

app/controllers/preferences_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class PreferencesController {
  async update({ response, request }: HttpContext) {
    const theme = request.input('theme')
    
    /**
     * 设置一个 2 小时后过期的签名 Cookie。
     * 签名可防止客户端篡改。
     */
    response.cookie('theme', theme, {
      maxAge: '2h'
    })
    
    return { success: true }
  }
}

使用 response.encryptedCookie() 对 Cookie 值进行加密。app.appKey 会对值进行加密,使其对客户端不可读。如果 app key 发生变化,该 Cookie 将无法解密。

app/controllers/sessions_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class SessionsController {
  async create({ response, auth }: HttpContext) {
    /**
     * 将敏感的会话数据存储在加密 Cookie 中。
     * 该值使用 app.appKey 加密。
     */
    response.encryptedCookie('session_data', {
      userId: auth.user.id,
      loginAt: new Date()
    })
    
    return { success: true }
  }
}

response.plainCookie() 方法创建一个 base64 编码的 Cookie,不进行签名或加密。

app/controllers/tracking_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class TrackingController {
  async view({ response }: HttpContext) {
    /**
     * 为非敏感的追踪数据设置普通 Cookie
     */
    response.plainCookie('last_visit', new Date().toISOString())
    
    return { success: true }
  }
}

Cookie 值可以是以下任何数据类型:stringnumberbigIntbooleannullobjectarray

app/controllers/cart_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class CartController {
  async add({ response, request }: HttpContext) {
    const cartItems = [
      { id: 1, name: 'Product A', quantity: 2 },
      { id: 2, name: 'Product B', quantity: 1 }
    ]
    
    /**
     * 数组和对象会被自动序列化
     */
    response.encryptedCookie('cart', cartItems)
    
    return { success: true }
  }
}

所有 Cookie 方法都接受一个选项对象来控制 Cookie 的行为。这些选项与 config/app.tshttp.cookie 块下的配置相匹配。

app/controllers/auth_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class AuthController {
  async login({ response, auth }: HttpContext) {
    /**
     * 设置带有安全选项的认证 Cookie
     */
    response.encryptedCookie('auth_token', auth.token, {
      domain: '',
      path: '/',
      maxAge: '2h',
      httpOnly: true,      // 阻止 JavaScript 访问
      secure: true,        // 仅通过 HTTPS 发送
      sameSite: 'lax',     // CSRF 防护
      partitioned: false,  // 实验性:CHIPS
      priority: 'medium'   // 实验性:Cookie 优先级
    })
    
    return { success: true }
  }
}
domain
string

Cookie 域名。空字符串表示当前域名。

path
string

Cookie 路径。默认是 /

maxAge
string | number

Cookie 过期时间。可以是 '2h' 这样的持续时间字符串或毫秒数。

httpOnly
boolean

阻止 JavaScript 访问 Cookie。

secure
boolean

仅通过 HTTPS 连接发送 Cookie。

sameSite
'lax' | 'strict' | 'none'

CSRF 防护。默认是 'lax'

partitioned
boolean

实验性:启用 Cookie 分区(CHIPS)。

priority
'low' | 'medium' | 'high'

实验性:给浏览器的 Cookie 优先级提示。

在响应发送后运行操作

response.onFinish() 方法允许你注册在响应已发送给客户端之后执行的回调。这对于清理任务、日志记录,或不应该阻塞响应的操作很有用。

文件下载后清理

一个常见的用例是在文件流式传输给客户端后删除临时文件。

app/controllers/files_controller.ts
import { unlink } from 'node:fs/promises'
import app from '@adonisjs/core/services/app'
import type { HttpContext } from '@adonisjs/core/http'

export default class FilesController {
  async download({ response, params }: HttpContext) {
    const filePath = app.makePath(`tmp/exports/${params.id}.zip`)
    
    /**
     * 在流式传输文件之前注册清理回调
     */
    response.onFinish(async () => {
      await unlink(filePath)
    })
    
    /**
     * 流式传输文件。下载完成后,
     * 清理回调将自动运行。
     */
    response.download(filePath)
  }
}

记录响应指标

你可以使用 onFinish() 来记录分析数据或指标,而无需延迟响应。

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

export default class AnalyticsMiddleware {
  async handle({ response, request }: HttpContext, next: NextFn) {
    const startTime = Date.now()
    
    /**
     * 在响应发送后注册分析日志记录。
     * 这不会延迟对客户端响应。
     */
    response.onFinish(() => {
      const duration = Date.now() - startTime
      console.log(`${request.method()} ${request.url()} - ${duration}ms`)
    })
    
    await next()
  }
}

响应体序列化

Response 类会自动序列化响应体,并根据你发送的数据类型设置适当的内容类型请求头。

当你发送响应时,AdonisJS 执行两个操作:它将值序列化为适合 HTTP 传输的字符串格式,并设置适当的 content-typecontent-length 请求头。

数组和对象
  • Content-Type: application/json
  • 序列化行为: 使用安全字符串化(类似 JSON.stringify(),但会移除循环引用并序列化 BigInt)
HTML 片段
  • Content-Type: text/html
  • 序列化行为: 原样发送(以 < 开头的字符串)
数字和布尔值
  • Content-Type: text/plain
  • 序列化行为: 转换为字符串
Date 实例
  • Content-Type: text/plain
  • 序列化行为: 使用 toISOString() 转换为 ISO 字符串
正则表达式
  • Content-Type: text/plain
  • 序列化行为: 使用 toString() 转换为字符串
Error 对象
  • Content-Type: text/plain
  • 序列化行为: 使用 toString() 转换为字符串
JSONP 响应
  • Content-Type: text/javascript
  • 序列化行为: 适当地字符串化
其他普通字符串
  • Content-Type: text/plain
  • 序列化行为: 原样发送
其他数据类型
  • Content-Type: -
  • 序列化行为: 导致异常

这种自动处理意味着你几乎不需要手动设置内容类型请求头。

start/routes.ts
import router from '@adonisjs/core/services/router'

router.get('/api/data', async () => {
  /**
   * 返回 content-type: application/json 的 JSON
   * BigInt 值会被安全地序列化为字符串
   */
  return {
    id: BigInt(9007199254740991),
    timestamp: new Date(),
    active: true
  }
})

router.get('/page', async () => {
  /**
   * 返回 content-type: text/html 的 HTML
   */
  return '<h1>Welcome</h1>'
})

router.get('/text', async () => {
  /**
   * 返回 content-type: text/plain 的纯文本
   */
  return 'Hello, world!'
})

扩展 HttpResponse 类

你可以使用宏(macro)和 getter 向 HttpResponse 类添加自定义方法和属性。这让你能够创建符合应用需求的、可复用的响应辅助函数。

Note

如果你是宏和 getter 概念的新手,请阅读 扩展 AdonisJS 指南。

添加自定义方法

使用 HttpResponse.macro() 为你的应用中所有响应实例添加方法。

providers/app_provider.ts
import { HttpResponse } from '@adonisjs/core/http'

export default class AppProvider {
  async boot() {
    /**
     * 添加一个自定义方法,以一致的结构发送 API 响应
     */
    HttpResponse.macro('api', function (data: any, meta?: Record<string, any>) {
      return this.json({
        success: true,
        data: data,
        meta: meta || {}
      })
    })
    
    /**
     * 添加一个用于分页响应的自定义方法
     */
    HttpResponse.macro('paginated', function (items: any[], pagination: any) {
      return this.json({
        data: items,
        pagination: {
          page: pagination.page,
          perPage: pagination.perPage,
          total: pagination.total
        }
      })
    })
  }
}

扩展 HttpResponse 类类型

添加自定义方法后,你必须通过扩展 HttpResponse 接口来告知 TypeScript 这些方法的存在。否则,在尝试使用自定义方法时会得到类型错误。

types/response.ts
import { HttpResponse } from '@adonisjs/core/http'

declare module '@adonisjs/core/http' {
  export interface HttpResponse {
    /**
     * 发送标准化的 API 响应
     */
    api(data: any, meta?: Record<string, any>): void
    
    /**
     * 发送分页响应
     */
    paginated(items: any[], pagination: {
      page: number
      perPage: number
      total: number
    }): void
  }
}

使用自定义方法

定义后,你的自定义方法在所有响应实例上都可用。

app/controllers/posts_controller.ts
import Post from '#models/post'
import type { HttpContext } from '@adonisjs/core/http'

export default class PostsController {
  async index({ response }: HttpContext) {
    const posts = await Post.all()
    
    /**
     * 使用自定义 api() 方法获得一致的响应
     */
    return response.api(posts, {
      version: 'v1',
      timestamp: new Date()
    })
  }
  
  async paginated({ response, request }: HttpContext) {
    const page = request.input('page', 1)
    const posts = await Post.query().paginate(page, 20)
    
    /**
     * 使用自定义 paginated() 方法
     */
    return response.paginated(posts.all(), posts.getMeta())
  }
}