Skip to content

事件处理

所有服务端代码都围绕 H3 的 event 对象进行请求和响应处理。event 是服务端开发的核心对象,理解它就能掌握服务端数据流转。

H3Event 对象

ts
export default defineEventHandler((event) => {
  // event 是 H3Event 实例
  // 包含请求和响应的所有信息
})

属性一览

属性类型说明
event.pathstring请求路径(如 /api/users?page=1 中的 /api/users
event.methodstringHTTP 方法(GETPOSTPUTDELETE 等)
event.headersHeaders请求头对象
event.contextobject上下文数据(中间件设置,路由读取)
event.node.reqIncomingMessageNode.js 原始请求对象
event.node.resServerResponseNode.js 原始响应对象

event.context 是最重要的属性

它是在中间件和路由之间传递数据的桥梁。中间件设置 event.context.userevent.context.db 等,路由直接读取使用。

请求处理

readBody —— 读取请求体

ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  // POST /api/users  body: { name: 'Alice', email: 'alice@example.com' }
  // body = { name: 'Alice', email: 'alice@example.com' }

  return { received: body }
})

readBody 的智能解析

  • Content-Type: application/json → 自动 JSON 解析
  • Content-Type: application/x-www-form-urlencoded → 自动 URL 编码解析
  • Content-Type: multipart/form-data → 自动解析 FormData
  • 其他 → 返回字符串

INFO

readBody 只能调用一次:请求体是流式数据 读取一次后就被消费了。如果需要多次使用,将结果存到变量中

readValidatedBody —— 校验请求体

使用 Zod 等 Schema 校验库验证请求体:

ts
import { z } from 'zod'

const schema = z.object({
  name: z.string().min(2, '姓名至少2个字符'),
  email: z.string().email('邮箱格式不正确'),
  age: z.number().min(0).max(150).optional(),
})

export default defineEventHandler(async (event) => {
  const body = await readValidatedBody(event, schema.parse)
  // 校验失败时,自动抛出 400 错误
  // 校验成功时,body 的类型由 schema 推断
  return { user: body }
})

readValidatedBody 的优势

  • 自动校验 + 自动错误响应(400)
  • TypeScript 类型推断(body 的类型由 schema 决定)
  • 统一的错误格式

推荐所有 POST/PUT 请求都使用 readValidatedBody

避免手动校验。

getQuery —— 获取查询参数

ts
// GET /api/search?q=nuxt&page=1
export default defineEventHandler((event) => {
  const query = getQuery(event)
  // query = { q: 'nuxt', page: '1' }  注意:值都是 string!
  return { query }
})

INFO

查询参数的值都是 string 类型:即使 URL 中是 ?page=1query.page 也是字符串 '1'。需要手动转换或使用 getValidatedQuery

getValidatedQuery —— 校验查询参数

ts
const schema = z.object({
  page: z.coerce.number().min(1).default(1),
  size: z.coerce.number().min(1).max(100).default(20),
  sort: z.enum(['name', 'date', 'price']).default('date'),
})

export default defineEventHandler(async (event) => {
  const { page, size, sort } = await getValidatedQuery(event, schema.parse)
  // page 和 size 已经是 number 类型!
  return { page, size, sort }
})

z.coerce.number()

Zod 的 coerce 会自动将字符串转为数字,非常适合查询参数校验。

getRouterParam —— 获取路由参数

ts
// server/api/users/[id].ts → /api/users/123
export default defineEventHandler((event) => {
  const id = getRouterParam(event, 'id')
  // id = '123'(string 类型)

  if (!id || !/^\d+$/.test(id)) {
    throw createError({ statusCode: 400, statusMessage: 'Bad Request', message: 'Invalid ID' })
  }

  return { id: Number(id) }
})

路由参数也是 string 类型

需要手动校验和转换。推荐封装为工具函数:

ts
// server/utils/params.ts
export const getNumericId = (event: H3Event, name = 'id') => {
const param = getRouterParam(event, name)
if (!param || !/^\d+$/.test(param)) {
throw createError({ statusCode: 400, statusMessage: 'Bad Request', message: `Invalid ${name}` })
}
return Number(param)
}

getRequestHeader —— 获取请求头

ts
export default defineEventHandler((event) => {
  const auth = getRequestHeader(event, 'authorization')
  const contentType = getRequestHeader(event, 'content-type')
  const userAgent = getRequestHeader(event, 'user-agent')

  return { auth, contentType, userAgent }
})

响应处理

setResponseStatus —— 设置状态码

ts
export default defineEventHandler((event) => {
  setResponseStatus(event, 201)  // Created
  return { created: true }
})

常用状态码

状态码含义适用场景
200OK成功(默认)
201Created创建资源成功
204No Content删除成功(无返回体)
301Moved Permanently永久重定向
302Found临时重定向
400Bad Request参数错误
401Unauthorized未认证
403Forbidden无权限
404Not Found资源不存在
429Too Many Requests请求过于频繁

setResponseHeader —— 设置响应头

ts
export default defineEventHandler((event) => {
  setResponseHeader(event, 'cache-control', 'max-age=3600')
  setResponseHeader(event, 'x-custom', 'value')
  return { message: 'Hello' }
})
ts
export default defineEventHandler((event) => {
  // 读取 Cookie
  const token = getCookie(event, 'auth-token')

  // 设置 Cookie
  setCookie(event, 'auth-token', 'new-token-value', {
    httpOnly: true,           // 不允许 JS 访问(防 XSS)
    secure: true,             // 仅 HTTPS 传输
    sameSite: 'lax',         // 防 CSRF
    maxAge: 60 * 60 * 24 * 7, // 7 天(秒)
    path: '/',                // Cookie 路径
  })

  // 删除 Cookie
  deleteCookie(event, 'temp-token')

  return { token }
})

Cookie 安全选项

  • httpOnly: true:JavaScript 无法读取(防止 XSS 窃取)
  • secure: true:仅在 HTTPS 下传输
  • sameSite: 'lax':防止 CSRF 攻击

认证 Token 推荐存储在 Cookie 中

(而非 localStorage),配合 httpOnly 更安全。

sendRedirect —— 重定向

ts
export default defineEventHandler(async (event) => {
  // 302 临时重定向
  await sendRedirect(event, '/new-page', 302)

  // 301 永久重定向
  // await sendRedirect(event, '/new-page', 301)
})

sendStream —— 发送流

ts
import { createReadStream } from 'fs'

export default defineEventHandler((event) => {
  setResponseHeader(event, 'content-type', 'application/pdf')
  return sendStream(event, createReadStream('/path/to/file.pdf'))
})

sendStream 的适用场景

  • 大文件下载(不占用内存)
  • 视频流
  • SSE(Server-Sent Events)

event.$fetch —— 转发请求

在服务端使用 event.$fetch 可以转发请求上下文和头信息:

ts
export default defineEventHandler((event) => {
  // 转发请求头(如 Authorization、Cookies)
  return event.$fetch('/api/internal/data')
})

event.$fetch vs 普通 $fetch

  • event.$fetch:继承当前请求的上下文(headers、cookies),适合服务端内部转发
  • 普通 $fetch:不继承上下文,适合调用外部 API

event.waitUntil —— 后台任务

在响应发送后执行后台任务,不阻塞响应:

ts
export default defineEventHandler((event) => {
  // 响应后执行日志记录(不阻塞当前响应)
  event.waitUntil(
    logActivity(event.context.user.id, 'page_view')
  )

  // 响应后发送通知
  event.waitUntil(
    sendNotification(user.email, 'Your order has been processed')
  )

  return { status: 'ok' }  // 立即返回,不等后台任务
})

event.waitUntil 的适用场景

  • 日志记录
  • 数据统计
  • 发送通知
  • 缓存预热

event.waitUntil 确保后台任务在服务器关闭前完成

即使响应已经发送。

错误处理

ts
export default defineEventHandler((event) => {
  const id = getRouterParam(event, 'id')

  // 抛出错误——自动返回错误响应
  if (!id) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Bad Request',
      message: 'ID is required',
    })
  }

  if (!/^\d+$/.test(id)) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Bad Request',
      message: 'ID must be a number',
    })
  }

  // 自定义错误数据
  const user = getUserById(Number(id))
  if (!user) {
    throw createError({
      statusCode: 404,
      statusMessage: 'Not Found',
      message: 'User not found',
      data: { userId: id },  // 附加数据
    })
  }

  return user
})

createError 的行为

  1. 抛出 H3Error 对象
  2. H3 捕获后自动转为 HTTP 错误响应
  3. 响应体格式:{ statusCode, statusMessage, data, message }
  4. 后续中间件和路由不会执行

INFO

statusMessage 应使用简短的 HTTP 状态描述(如 "Bad Request""Not Found") 不要放中文或长文本。详细的错误描述应放在 message 中。详见 错误创建与抛出

知识脉络

text
API 路由 → 服务路由 → 服务中间件 → 服务插件 → 工具函数 → 你在这里:事件处理

                                                              ├─→ 下一步:预渲染

                                                              └─→ 相关:错误处理(createError 详解)

基于 Nuxt 4 官方文档整理编写