{T}

Koa 上下文对象 Context

概述

Context 对象是 Koa 的核心概念,它将 Node.js 的 requestresponse 对象封装到一个对象中,为一次 HTTP 请求-响应的生命周期提供了完整的上下文。通常将其简称为 ctx

核心特性

特性描述
统一接口将请求和响应封装在单一对象中
别名机制提供便捷的属性别名,如 ctx.url 等价于 ctx.request.url
生命周期管理每个请求创建独立的 Context 实例
状态共享通过 ctx.state 在中间件间传递数据

Context 对象属性

在每个中间件中,ctx 都是第一个参数:

javascript
app.use(async (ctx, next) => {
  ctx                    // Context 对象
  ctx.request            // Koa 封装的 Request 对象
  ctx.response           // Koa 封装的 Response 对象
  ctx.req                // Node.js 原生 request 对象
  ctx.res                // Node.js 原生 response 对象
  ctx.app                // Application 实例
  ctx.state              // 跨中间件共享数据
  
  await next()
})

常用别名速查表

别名属性完整路径描述
ctx.methodctx.request.method请求方法
ctx.urlctx.request.url请求 URL
ctx.headerctx.request.header请求头对象
ctx.headersctx.request.headers请求头对象(别名)
ctx.queryctx.request.query解析后的查询字符串对象
ctx.querystringctx.request.querystring原始查询字符串
ctx.pathctx.request.path请求路径
ctx.hostctx.request.host主机名
ctx.bodyctx.response.body响应体
ctx.statusctx.response.status响应状态码
ctx.typectx.response.typeContent-Type
ctx.set()ctx.response.set()设置响应头
ctx.redirect()ctx.response.redirect()重定向

Context 对象结构

对象关系图

code
Context (ctx)
│
├── Request 对象 (ctx.request)
│   ├── method          请求方法
│   ├── url             请求 URL
│   ├── header          请求头
│   ├── query           查询参数
│   ├── body            请求体(需中间件)
│   └── ...             其他属性
│
├── Response 对象 (ctx.response)
│   ├── status          响应状态码
│   ├── message         状态消息
│   ├── body            响应体
│   ├── header          响应头
│   └── ...             其他属性
│
├── State 对象 (ctx.state)
│   └── 自定义属性      跨中间件共享数据
│
├── 原生对象
│   ├── req             Node.js 原生 request
│   └── res             Node.js 原生 response
│
└── App 对象
    └── app             Application 实例引用

请求生命周期

code
请求进入
    │
    ├─→ 创建 Context 实例
    │       │
    │       ├─→ ctx.request 封装原生 req
    │       ├─→ ctx.response 封装原生 res
    │       └─→ ctx.state = {} 初始化状态
    │
    ├─→ 中间件执行(洋葱模型)
    │       │
    │       ├─→ 中间件 1
    │       ├─→ 中间件 2
    │       └─→ 中间件 N
    │
    ├─→ 响应发送
    │       │
    │       └─→ ctx.response.body → 客户端
    │
    └─→ Context 销毁

Request 对象详解

ctx.request 对象提供对客户端请求的丰富封装,包含请求的所有信息。

请求基本信息

method - 请求方法

javascript
app.use(async (ctx) => {
  console.log(ctx.method)  // GET, POST, PUT, DELETE, PATCH 等
  
  // 方法判断
  if (ctx.method === "POST") {
    // 处理 POST 请求
  }
  
  // 使用 route 中间件自动处理
  router.get("/users", handler)   // 只处理 GET
  router.post("/users", handler)  // 只处理 POST
})

url - 请求 URL

javascript
app.use(async (ctx) => {
  console.log(ctx.url)         // /users?id=123&page=1
  console.log(ctx.path)        // /users
  console.log(ctx.querystring) // id=123&page=1
  console.log(ctx.query)       // { id: '123', page: '1' }
  
  // 获取原始 URL
  console.log(ctx.request.originalUrl)  // 包含查询字符串的完整路径
})

GET 请求参数

query - 查询参数对象

javascript
// 访问: http://localhost:3000/search?q=koa&page=2&limit=20

app.use(async (ctx) => {
  const { q, page, limit } = ctx.query
  
  console.log(q)      // "koa"
  console.log(page)   // "2"
  console.log(limit)  // "20"
  
  // 注意:query 参数都是字符串类型
  const pageNum = parseInt(page) || 1
  const limitNum = parseInt(limit) || 10
  
  ctx.body = {
    query: q,
    page: pageNum,
    limit: limitNum
  }
})

querystring - 原始查询字符串

javascript
app.use(async (ctx) => {
  console.log(ctx.querystring)  // q=koa&page=2&limit=20
  
  // 解析查询字符串
  const params = new URLSearchParams(ctx.querystring)
  console.log(params.get("q"))  // "koa"
})

POST 请求参数

Koa 核心不内置请求体解析,需要使用中间件:

使用 koa-bodyparser

bash
npm install koa-bodyparser
javascript
const Koa = require("koa")
const bodyParser = require("koa-bodyparser")
const app = new Koa()

app.use(bodyParser())

// 处理 JSON 请求
app.post("/api/json", async (ctx) => {
  const data = ctx.request.body
  console.log(data)  // { name: "Koa", version: "2" }
  
  ctx.body = {
    success: true,
    data
  }
})

// 处理表单请求
app.post("/api/form", async (ctx) => {
  const { username, password } = ctx.request.body
  
  ctx.body = {
    username,
    password: "***"
  }
})

使用 koa-body

bash
npm install koa-body
javascript
const { koaBody } = require("koa-body")

app.use(koaBody({
  multipart: true,        // 支持文件上传
  formidable: {
    maxFileSize: 200 * 1024 * 1024,  // 200MB
    uploadDir: "./uploads"
  }
}))

// 获取请求体
app.post("/api/data", async (ctx) => {
  const data = ctx.request.body        // 表单数据
  const files = ctx.request.files      // 上传文件
  
  ctx.body = { success: true }
})

请求头

获取请求头

javascript
app.use(async (ctx) => {
  // 方式 1: 获取所有请求头
  console.log(ctx.header)
  console.log(ctx.headers)  // 别名
  
  // 方式 2: 获取单个请求头
  const userAgent = ctx.get("User-Agent")
  const contentType = ctx.get("Content-Type")
  const authorization = ctx.get("Authorization")
  
  // 方式 3: 使用 request.header
  const host = ctx.request.header.host
  
  ctx.body = { userAgent }
})

常用请求头

javascript
app.use(async (ctx) => {
  const headers = {
    "user-agent": ctx.get("User-Agent"),    // 浏览器信息
    "content-type": ctx.get("Content-Type"), // 内容类型
    "accept": ctx.get("Accept"),             // 接受的类型
    "authorization": ctx.get("Authorization"), // 认证信息
    "referer": ctx.get("Referer"),           // 来源页面
    "cookie": ctx.get("Cookie")              // Cookie
  }
  
  ctx.body = headers
})

客户端信息

IP 地址

javascript
// 启用代理支持
app.proxy = true

app.use(async (ctx) => {
  console.log(ctx.ip)   // 客户端 IP
  
  // 当使用代理时
  console.log(ctx.ips)  // IP 数组 [客户端IP, 代理1IP, 代理2IP]
  
  // 获取真实 IP
  const realIp = ctx.ips.length > 0 ? ctx.ips[0] : ctx.ip
  
  ctx.body = { ip: realIp }
})

主机信息

javascript
app.use(async (ctx) => {
  console.log(ctx.host)      // localhost:3000
  console.log(ctx.hostname)  // localhost
  console.log(ctx.protocol)  // http 或 https
  console.log(ctx.secure)    // 是否 HTTPS
  console.log(ctx.origin)    // http://localhost:3000
  
  // 构建完整 URL
  const fullUrl = `${ctx.origin}${ctx.url}`
  console.log(fullUrl)       // http://localhost:3000/api/users?id=1
})

内容协商

Koa 提供了强大的内容协商功能:

javascript
app.use(async (ctx) => {
  // 检查客户端接受的内容类型
  if (ctx.accepts("json")) {
    ctx.type = "json"
    ctx.body = { message: "Hello" }
  } else if (ctx.accepts("html")) {
    ctx.type = "html"
    ctx.body = "<h1>Hello</h1>"
  } else if (ctx.accepts("xml")) {
    ctx.type = "xml"
    ctx.body = "<message>Hello</message>"
  } else {
    ctx.type = "text"
    ctx.body = "Hello"
  }
  
  // 检查内容类型
  if (ctx.is("json")) {
    // 处理 JSON 请求
  }
  
  if (ctx.is("urlencoded")) {
    // 处理表单请求
  }
  
  if (ctx.is("multipart")) {
    // 处理文件上传
  }
})

Request API 速查

属性/方法描述示例
ctx.method请求方法GET, POST
ctx.url请求 URL/users?id=1
ctx.path请求路径/users
ctx.query查询参数对象{ id: '1' }
ctx.querystring查询字符串id=1
ctx.header请求头对象{ host: '...' }
ctx.host主机名localhost:3000
ctx.hostname主机名(无端口)localhost
ctx.protocol协议http, https
ctx.secure是否 HTTPStrue, false
ctx.ip客户端 IP127.0.0.1
ctx.ipsIP 数组['IP1', 'IP2']
ctx.get(field)获取请求头ctx.get('User-Agent')
ctx.accepts(types)内容协商ctx.accepts('json')
ctx.is(types)检查类型ctx.is('json')

Response 对象详解

ctx.response 对象用于控制对客户端的响应,包含响应状态、头部、内容等信息。

响应状态

status - 状态码

javascript
app.use(async (ctx) => {
  // 设置状态码
  ctx.status = 200  // OK
  ctx.status = 201  // Created
  ctx.status = 204  // No Content
  ctx.status = 400  // Bad Request
  ctx.status = 401  // Unauthorized
  ctx.status = 403  // Forbidden
  ctx.status = 404  // Not Found
  ctx.status = 500  // Internal Server Error
  
  // 设置状态消息
  ctx.message = "Success"
  
  ctx.body = "OK"
})

常用状态码速查

状态码含义使用场景
2xx成功
200OK请求成功
201Created资源创建成功
204No Content删除成功,无返回内容
3xx重定向
301Moved Permanently永久重定向
302Found临时重定向
304Not Modified缓存有效
4xx客户端错误
400Bad Request请求参数错误
401Unauthorized未认证
403Forbidden无权限
404Not Found资源不存在
422Unprocessable Entity验证失败
429Too Many Requests请求过于频繁
5xx服务器错误
500Internal Server Error服务器内部错误
502Bad Gateway网关错误
503Service Unavailable服务不可用

响应体

body - 响应内容

ctx.body 支持多种数据类型:

javascript
app.use(async (ctx) => {
  // 字符串
  ctx.body = "Hello World"
  ctx.type = "text/plain"
  
  // HTML
  ctx.body = "<h1>Hello World</h1>"
  ctx.type = "text/html"
  
  // JSON(对象会自动序列化)
  ctx.body = {
    success: true,
    data: {
      id: 1,
      name: "Koa"
    }
  }
  // 自动设置 Content-Type: application/json
  
  // Buffer
  ctx.body = Buffer.from("Hello World")
  
  // Stream(流)
  const fs = require("fs")
  ctx.body = fs.createReadStream("./large-file.txt")
  
  // null(无内容)
  ctx.body = null
})

流式响应

javascript
const fs = require("fs")
const path = require("path")

// 文件下载
app.use(async (ctx) => {
  const filePath = path.join(__dirname, "files", "report.pdf")
  
  ctx.set("Content-Disposition", "attachment; filename=report.pdf")
  ctx.type = "application/pdf"
  ctx.body = fs.createReadStream(filePath)
})

// 大文件流式传输
app.use(async (ctx) => {
  const filePath = "./large-video.mp4"
  const stat = fs.statSync(filePath)
  
  ctx.set("Content-Length", stat.size)
  ctx.type = "video/mp4"
  ctx.body = fs.createReadStream(filePath)
})

响应头

set - 设置响应头

javascript
app.use(async (ctx) => {
  // 设置单个响应头
  ctx.set("X-Custom-Header", "value")
  
  // 设置多个响应头
  ctx.set({
    "Cache-Control": "no-cache",
    "X-Powered-By": "Koa",
    "X-Response-Time": "10ms"
  })
  
  // 追加响应头
  ctx.append("Set-Cookie", "name=value")
  ctx.append("Set-Cookie", "token=abc123")
  
  ctx.body = "OK"
})

常用响应头设置

javascript
app.use(async (ctx) => {
  // 缓存控制
  ctx.set("Cache-Control", "public, max-age=31536000")
  
  // CORS 跨域
  ctx.set("Access-Control-Allow-Origin", "*")
  ctx.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
  ctx.set("Access-Control-Allow-Headers", "Content-Type, Authorization")
  
  // 安全头
  ctx.set("X-Content-Type-Options", "nosniff")
  ctx.set("X-Frame-Options", "DENY")
  ctx.set("X-XSS-Protection", "1; mode=block")
  
  ctx.body = "OK"
})

type - 内容类型

javascript
app.use(async (ctx) => {
  // 设置 Content-Type
  ctx.type = "json"                           // application/json
  ctx.type = "html"                           // text/html
  ctx.type = "text"                           // text/plain
  ctx.type = "xml"                            // application/xml
  ctx.type = "application/octet-stream"       // 二进制流
  ctx.type = "image/png"                      // PNG 图片
  
  // 使用简写
  ctx.type = ".json"  // application/json
  ctx.type = ".html"  // text/html
  
  ctx.body = { message: "OK" }
})

重定向

redirect - 页面重定向

javascript
app.use(async (ctx) => {
  // 临时重定向 (302)
  ctx.redirect("/login")
  
  // 永久重定向 (301)
  ctx.status = 301
  ctx.redirect("/new-url")
  
  // 重定向到来源页
  ctx.redirect("back")
  
  // 重定向到外部网站
  ctx.redirect("https://example.com")
  
  // 自定义重定向状态码
  ctx.status = 307  // 临时重定向,保持请求方法
  ctx.redirect("/new-url")
})

文件下载

attachment - 文件附件

javascript
app.use(async (ctx) => {
  // 设置为附件下载
  ctx.attachment("report.pdf")
  ctx.body = fs.readFileSync("./files/report.pdf")
  
  // 或者
  ctx.set("Content-Disposition", "attachment; filename=report.pdf")
  ctx.body = fs.createReadStream("./files/report.pdf")
})

缓存控制

lastModified 和 etag

javascript
app.use(async (ctx) => {
  // 设置最后修改时间
  ctx.lastModified = new Date()
  
  // 设置 ETag
  ctx.etag = "unique-identifier"
  
  // 检查客户端缓存
  if (ctx.fresh) {
    ctx.status = 304
    return
  }
  
  ctx.body = { data: "..." }
})

Response API 速查

属性/方法描述示例
ctx.status状态码ctx.status = 200
ctx.message状态消息ctx.message = "OK"
ctx.body响应体ctx.body = { data }
ctx.type内容类型ctx.type = "json"
ctx.length内容长度ctx.length = 1024
ctx.set(field, value)设置响应头ctx.set("X-Custom", "value")
ctx.append(field, value)追加响应头ctx.append("Set-Cookie", "...")
ctx.remove(field)删除响应头ctx.remove("X-Powered-By")
ctx.redirect(url)重定向ctx.redirect("/login")
ctx.attachment(filename)文件下载ctx.attachment("file.pdf")
ctx.lastModified最后修改时间ctx.lastModified = new Date()
ctx.etagETagctx.etag = "abc123"
ctx.fresh缓存是否有效if (ctx.fresh) { ... }

Delegate 机制详解

Koa 的 context.js 使用 delegates 库将 requestresponse 对象的属性与方法委托到 ctx 上,使得开发者可以直接通过 ctx.xxx 访问,而无需写成 ctx.request.xxxctx.response.xxx

委托类型

delegates 库提供三种委托方式:

委托方式作用内部实现示例
method委托方法调用proto[key] = function(){ return this[target][key].apply(this[target], arguments) }ctx.redirect()ctx.response.redirect()
getter委托只读属性proto.__defineGetter__(key, function(){ return this[target][key] })ctx.ipctx.request.ip
access委托读写属性同时定义 getter 和 setterctx.bodyctx.response.body

其中 setteraccess 的组成部分,单独使用时只委托写操作:proto.__defineSetter__(key, function(val){ return this[target][key] = val })

委托链路示例

javascript
// delegates 源码核心逻辑
function Delegator(proto, target) {
  if (!(this instanceof Delegator)) return new Delegator(proto, target)
  this.proto = proto
  this.target = target
  this.getters = []
  this.setters = []
  this.methods = []
}

// 委托方法
Delegator.prototype.method = function(name) {
  const proto = this.proto
  const target = this.target
  this.methods.push(name)
  proto[name] = function() {
    return this[target][name].apply(this[target], arguments)
  }
  return this
}

// 委托 getter
Delegator.prototype.getter = function(name) {
  const proto = this.proto
  const target = this.target
  this.getters.push(name)
  proto.__defineGetter__(name, function() {
    return this[target][name]
  })
  return this
}

// 委托 access(getter + setter)
Delegator.prototype.access = function(name) {
  return this.getter(name).setter(name)
}

context.js 中的委托声明

javascript
// 将 response 的方法/属性委托到 ctx
delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('set')
  .method('append')
  .access('status')    // 可读可写
  .access('body')      // 可读可写
  .access('type')      // 可读可写
  .getter('writable')  // 只读

// 将 request 的方法/属性委托到 ctx
delegate(proto, 'request')
  .method('accepts')
  .method('get')
  .access('query')     // 可读可写
  .access('method')    // 可读可写
  .access('url')       // 可读可写
  .getter('ip')        // 只读
  .getter('headers')   // 只读
WARNING

requestresponse 上存在同名属性时,后声明的委托会覆盖先声明的。Koa 源码中先委托 response,再委托 request,因此同名属性最终会指向 request 上的值。开发时应注意避免歧义,必要时显式使用 ctx.request.xxxctx.response.xxx

State 状态对象

ctx.state 是推荐的命名空间,用于在中间件之间传递信息或向视图模板暴露数据。这是在请求生命周期内共享数据的标准方式。

基本使用

javascript
// 中间件 1: 设置用户信息
app.use(async (ctx, next) => {
  ctx.state.user = { 
    id: 1, 
    name: "Alice", 
    role: "admin" 
  }
  await next()
})

// 中间件 2: 设置其他数据
app.use(async (ctx, next) => {
  ctx.state.startTime = Date.now()
  await next()
})

// 中间件 3: 使用共享数据
app.use(async (ctx) => {
  console.log(ctx.state.user.name)      // Alice
  console.log(ctx.state.startTime)       // 时间戳
  
  ctx.body = `Hello, ${ctx.state.user.name}`
})

典型应用场景

用户认证

javascript
// 认证中间件
const authMiddleware = async (ctx, next) => {
  const token = ctx.header.authorization
  
  if (!token) {
    ctx.throw(401, "Unauthorized")
  }
  
  try {
    const user = await verifyToken(token)
    ctx.state.user = user
    ctx.state.isAuthenticated = true
    await next()
  } catch (err) {
    ctx.throw(401, "Invalid token")
  }
}

// 路由处理
router.get("/profile", authMiddleware, async (ctx) => {
  ctx.body = {
    user: ctx.state.user
  }
})

数据预加载

javascript
// 预加载中间件
app.use(async (ctx, next) => {
  ctx.state.config = await loadConfig()
  ctx.state.menus = await loadMenus()
  await next()
})

// 使用预加载数据
app.use(async (ctx) => {
  await ctx.render("page", {
    config: ctx.state.config,
    menus: ctx.state.menus
  })
})

请求追踪

javascript
app.use(async (ctx, next) => {
  ctx.state.requestId = generateRequestId()
  ctx.state.startTime = Date.now()
  
  await next()
  
  const duration = Date.now() - ctx.state.startTime
  console.log(`[${ctx.state.requestId}] ${ctx.method} ${ctx.url} - ${duration}ms`)
})

最佳实践

javascript
// ✅ 推荐:使用 ctx.state
app.use(async (ctx, next) => {
  ctx.state.user = await getUser()
  await next()
})

// ❌ 不推荐:直接在 ctx 上添加属性(可能冲突)
app.use(async (ctx, next) => {
  ctx.user = await getUser()
  await next()
})

// ✅ 推荐:结构化数据
app.use(async (ctx, next) => {
  ctx.state = {
    ...ctx.state,
    user: await getUser(),
    permissions: await getPermissions()
  }
  await next()
})

Cookies 操作

Koa 通过 ctx.cookies 提供方便的 API 来操作 Cookie,无需额外安装中间件。

基本操作

javascript
app.use(async (ctx) => {
  // 基本设置
  ctx.cookies.set("name", "koa")
  
  // 带选项设置
  ctx.cookies.set("name", "koa", {
    maxAge: 24 * 60 * 60 * 1000,  // 1 天
    httpOnly: true
  })
  
  ctx.body = "Cookie set"
})
javascript
app.use(async (ctx) => {
  const name = ctx.cookies.get("name")
  console.log(name)  // "koa"
  
  // 获取签名 Cookie
  const sessionId = ctx.cookies.get("sessionId", { signed: true })
  
  ctx.body = { name }
})
选项类型描述示例
maxAgeNumber过期时间(毫秒)maxAge: 86400000
expiresDate过期日期expires: new Date('2025-12-31')
pathString生效路径path: '/'
domainString生效域名domain: 'example.com'
secureBoolean仅 HTTPSsecure: true
httpOnlyBoolean仅 HTTP 访问httpOnly: true
sameSiteStringSameSite 策略sameSite: 'strict'
signedBoolean签名 Cookiesigned: true
overwriteBoolean覆盖同名 Cookieoverwrite: true

签名可以防止 Cookie 被篡改:

javascript
const Koa = require("koa")
const app = new Koa()

// 设置签名密钥
app.keys = ["secret-key-1", "secret-key-2"]

app.use(async (ctx) => {
  // 设置签名 Cookie
  ctx.cookies.set("userId", "123", {
    signed: true,
    httpOnly: true,
    maxAge: 7 * 24 * 60 * 60 * 1000  // 7 天
  })
  
  // 获取签名 Cookie(自动验证)
  const userId = ctx.cookies.get("userId", { signed: true })
  
  ctx.body = { userId }
})

实际应用示例

访问计数器

javascript
app.use(async (ctx) => {
  let count = parseInt(ctx.cookies.get("view_count")) || 0
  count++
  
  ctx.cookies.set("view_count", String(count), {
    maxAge: 1000 * 60 * 60,  // 1 小时
    httpOnly: true
  })
  
  ctx.body = `You have viewed this page ${count} times.`
})

用户偏好设置

javascript
app.use(async (ctx) => {
  // 设置用户偏好
  if (ctx.method === "POST") {
    const { theme, language } = ctx.request.body
    
    ctx.cookies.set("preferences", JSON.stringify({ theme, language }), {
      maxAge: 365 * 24 * 60 * 60 * 1000,  // 1 年
      httpOnly: false  // 允许客户端 JavaScript 访问
    })
    
    ctx.body = { success: true }
  }
  
  // 获取用户偏好
  if (ctx.method === "GET") {
    const preferences = JSON.parse(ctx.cookies.get("preferences") || "{}")
    ctx.body = preferences
  }
})
javascript
// 简单的 Session 实现
const sessions = new Map()

app.use(async (ctx) => {
  let sessionId = ctx.cookies.get("sessionId")
  
  if (!sessionId || !sessions.has(sessionId)) {
    sessionId = generateSessionId()
    sessions.set(sessionId, { userId: null, createdAt: Date.now() })
    
    ctx.cookies.set("sessionId", sessionId, {
      maxAge: 24 * 60 * 60 * 1000,  // 1 天
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      signed: true
    })
  }
  
  const session = sessions.get(sessionId)
  ctx.state.session = session
  
  await next()
})
javascript
// 生产环境 Cookie 配置
app.use(async (ctx) => {
  ctx.cookies.set("session", sessionId, {
    maxAge: 7 * 24 * 60 * 60 * 1000,  // 7 天
    httpOnly: true,      // 防止 XSS 攻击
    secure: true,        // 仅 HTTPS
    sameSite: "strict",  // 防止 CSRF 攻击
    signed: true         // 防止篡改
  })
})

Cookies 操作

Koa 通过 ctx.cookies 提供方便的 API 来操作 Cookie,无需额外安装中间件

  • ctx.cookies.get(name, [options]):获取指定名称的 Cookie
  • ctx.cookies.set(name, value, [options]):设置 Cookie

示例

javascript
app.use(async (ctx) => {
  // 检查是否存在一个名为 'view_count' 的 cookie
  let count = ctx.cookies.get("view_count") || 0
  count = Number(count) + 1

  // 设置或更新 cookie
  ctx.cookies.set("view_count", count, {
    maxAge: 1000 * 60 * 60, // 1 小时过期
    httpOnly: true // 仅服务器可访问
  })

  ctx.body = `You have viewed this page ${count} times.`
})

set 方法的 options 参数可以精细地控制 Cookie 的行为:

选项描述
maxAgeCookie 过期的毫秒数
expiresCookie 过期的 Date 对象
pathCookie 生效的路径,默认为 /
domainCookie 生效的域名
securetrue 表示仅通过 HTTPS 发送
httpOnlytrue 表示无法通过客户端 JavaScript 访问
signedtrue 表示需要对 Cookie 进行签名,防止篡改。需要设置 app.keys
overwritetrue 表示覆盖同名 Cookie,默认为 false

为了防止 Cookie 被篡改,可以使用签名 Cookie:

javascript
const app = new Koa()

// 设置密钥,用于签名 Cookie
app.keys = ["some secret key", "another secret key"]

app.use(async (ctx) => {
  // 设置签名 Cookie
  ctx.cookies.set("user", "alice", {
    signed: true,
    httpOnly: true
  })

  // 读取签名 Cookie(自动验证签名)
  const user = ctx.cookies.get("user", { signed: true })
  ctx.body = `Hello, ${user}`
})

错误处理

Koa 提供了多种错误处理方式,可以灵活地处理 HTTP 错误和应用程序错误。

ctx.throw - 抛出 HTTP 错误

ctx.throw() 是辅助方法,用于抛出带有 HTTP 状态码的错误:

javascript
app.use(async (ctx) => {
  // 基本用法
  ctx.throw(400, "Name is required")
  ctx.throw(401, "Unauthorized")
  ctx.throw(403, "Forbidden")
  ctx.throw(404, "User not found")
  ctx.throw(500, "Internal server error")
  
  // 附带额外属性
  ctx.throw(401, "Authentication failed", { 
    code: "AUTH_001",
    retry: true 
  })
  
  // 只传状态码(使用默认消息)
  ctx.throw(400)  // Bad Request
  ctx.throw(404)  // Not Found
})

ctx.assert - 断言检查

ctx.assert() 用于断言检查,失败时抛出错误:

javascript
app.use(async (ctx) => {
  // 断言用户存在
  const user = await User.findById(ctx.params.id)
  ctx.assert(user, 404, "User not found")
  
  // 断言用户已登录
  ctx.assert(ctx.state.user, 401, "Please login first")
  
  // 断言权限
  ctx.assert(ctx.state.user.role === "admin", 403, "Admin only")
  
  // 等价于
  if (!user) {
    ctx.throw(404, "User not found")
  }
  
  ctx.body = { user }
})

全局错误处理

错误处理中间件

javascript
// 错误处理中间件(必须放在最前面)
app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    // 设置状态码
    ctx.status = err.status || err.statusCode || 500
    
    // 设置响应体
    ctx.body = {
      success: false,
      message: err.message,
      code: err.code || "ERROR",
      // 开发环境显示堆栈
      ...(process.env.NODE_ENV === "development" && { 
        stack: err.stack 
      })
    }
    
    // 触发错误事件
    ctx.app.emit("error", err, ctx)
  }
})

// 路由处理
router.get("/users/:id", async (ctx) => {
  const user = await User.findById(ctx.params.id)
  ctx.assert(user, 404, "User not found")
  ctx.body = { user }
})

自定义错误类

javascript
// 自定义错误类
class AppError extends Error {
  constructor(message, code, status = 400) {
    super(message)
    this.code = code
    this.status = status
    this.name = "AppError"
  }
}

class ValidationError extends AppError {
  constructor(message, errors = []) {
    super(message, "VALIDATION_ERROR", 400)
    this.errors = errors
  }
}

class UnauthorizedError extends AppError {
  constructor(message = "Unauthorized") {
    super(message, "UNAUTHORIZED", 401)
  }
}

class NotFoundError extends AppError {
  constructor(resource) {
    super(`${resource} not found`, "NOT_FOUND", 404)
  }
}

// 使用
app.use(async (ctx) => {
  const user = await User.findById(ctx.params.id)
  if (!user) {
    throw new NotFoundError("User")
  }
  
  if (!user.isActive) {
    throw new UnauthorizedError("User account is inactive")
  }
  
  ctx.body = { user }
})

错误事件监听

javascript
// 监听错误事件
app.on("error", (err, ctx) => {
  // 记录错误日志
  console.error("Server Error:", {
    message: err.message,
    code: err.code,
    status: err.status,
    stack: err.stack,
    url: ctx.url,
    method: ctx.method,
    ip: ctx.ip
  })
  
  // 发送错误通知(如 Sentry)
  // Sentry.captureException(err)
  
  // 发送邮件通知
  // sendErrorEmail(err, ctx)
})

// 区分错误类型
app.on("error", (err, ctx) => {
  // 忽略客户端断开连接错误
  if (err.code === "ECONNABORTED") {
    return
  }
  
  // 忽略 4xx 错误
  if (err.status >= 400 && err.status < 500) {
    console.warn("Client Error:", err.message)
    return
  }
  
  // 记录 5xx 错误
  console.error("Server Error:", err)
})

404 处理

javascript
// 404 处理中间件(放在最后)
app.use(async (ctx, next) => {
  await next()
  
  if (ctx.status === 404 && !ctx.body) {
    ctx.status = 404
    ctx.body = {
      success: false,
      message: "Not Found",
      code: "NOT_FOUND",
      path: ctx.url
    }
  }
})

API 参考

Context 核心 API

属性

属性类型描述
ctx.reqObjectNode.js 原生 request 对象
ctx.resObjectNode.js 原生 response 对象
ctx.requestObjectKoa Request 对象
ctx.responseObjectKoa Response 对象
ctx.stateObject推荐的命名空间,用于传递数据
ctx.appObjectApplication 实例引用

方法

方法描述示例
ctx.throw(status, msg, properties)抛出 HTTP 错误ctx.throw(404, "Not found")
ctx.assert(value, status, msg, properties)断言检查ctx.assert(user, 404)
ctx.get(field)获取请求头ctx.get("User-Agent")
ctx.set(field, value)设置响应头ctx.set("X-Custom", "value")
ctx.append(field, value)追加响应头ctx.append("Set-Cookie", "...")
ctx.remove(field)删除响应头ctx.remove("X-Powered-By")
ctx.redirect(url)重定向ctx.redirect("/login")
ctx.attachment(filename)设置文件附件ctx.attachment("file.pdf")

Request API

属性/方法类型描述
request.methodString请求方法
request.urlString请求 URL
request.pathString请求路径
request.queryObject查询参数对象
request.querystringString查询字符串
request.headerObject请求头对象
request.hostString主机名
request.hostnameString主机名(无端口)
request.protocolString协议
request.secureBoolean是否 HTTPS
request.ipString客户端 IP
request.ipsArrayIP 数组
request.bodyAny请求体(需中间件)
request.get(field)String获取请求头
request.accepts(types)String/Boolean内容协商
request.is(types)String/Boolean检查类型

Response API

属性/方法类型描述
response.statusNumber状态码
response.messageString状态消息
response.bodyAny响应体
response.typeString内容类型
response.lengthNumber内容长度
response.headerObject响应头对象
response.lastModifiedDate最后修改时间
response.etagStringETag
response.freshBoolean缓存是否有效
response.set(field, value)-设置响应头
response.append(field, value)-追加响应头
response.remove(field)-删除响应头
response.redirect(url)-重定向
response.attachment(filename)-文件下载

常见问题

Q1: ctx.body 赋值后还可以修改吗?

可以,在响应发送前都可以修改:

javascript
app.use(async (ctx, next) => {
  ctx.body = { message: "Initial" }
  
  await next()
  
  // 在这里还可以修改
  ctx.body = { 
    ...ctx.body,
    timestamp: Date.now() 
  }
})

Q2: 如何获取 POST 请求体?

使用 koa-bodyparserkoa-body 中间件:

javascript
const bodyParser = require("koa-bodyparser")
app.use(bodyParser())

app.post("/data", async (ctx) => {
  const body = ctx.request.body
  ctx.body = { received: body }
})

Q3: 如何设置跨域?

javascript
// 简单配置
app.use(async (ctx, next) => {
  ctx.set("Access-Control-Allow-Origin", "*")
  ctx.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
  ctx.set("Access-Control-Allow-Headers", "Content-Type, Authorization")
  
  if (ctx.method === "OPTIONS") {
    ctx.status = 204
    return
  }
  
  await next()
})

// 使用 @koa/cors
const cors = require("@koa/cors")
app.use(cors({ origin: "*" }))

Q4: ctx.query 的值为什么都是字符串?

URL 查询参数本身就是字符串,需要手动转换:

javascript
app.use(async (ctx) => {
  const { page, limit } = ctx.query
  
  // 转换类型
  const pageNum = parseInt(page) || 1
  const limitNum = parseInt(limit) || 10
  const isActive = ctx.query.active === "true"
  
  ctx.body = { page: pageNum, limit: limitNum }
})

Q5: 如何正确处理文件上传?

使用 koa-body@koa/multer

javascript
const { koaBody } = require("koa-body")

app.use(koaBody({
  multipart: true,
  formidable: {
    maxFileSize: 10 * 1024 * 1024,  // 10MB
    uploadDir: "./uploads"
  }
}))

app.post("/upload", async (ctx) => {
  const file = ctx.request.files.file
  ctx.body = { 
    filename: file.originalFilename,
    path: file.filepath
  }
})

Q6: 如何获取客户端真实 IP?

javascript
// 启用代理支持
app.proxy = true

app.use(async (ctx) => {
  // 单层代理
  const ip = ctx.ip
  
  // 多层代理
  const realIp = ctx.ips.length > 0 ? ctx.ips[0] : ctx.ip
  
  // 或从请求头获取
  const forwarded = ctx.get("X-Forwarded-For")
  
  ctx.body = { ip: realIp }
})

最佳实践

1. 统一响应格式

javascript
// 响应格式化中间件
app.use(async (ctx, next) => {
  // 成功响应
  ctx.success = (data, message = "Success") => {
    ctx.body = {
      success: true,
      message,
      data,
      timestamp: Date.now()
    }
  }
  
  // 失败响应
  ctx.fail = (message = "Error", code = 400, status = 400) => {
    ctx.status = status
    ctx.body = {
      success: false,
      message,
      code,
      timestamp: Date.now()
    }
  }
  
  await next()
})

// 使用
router.get("/users", async (ctx) => {
  const users = await User.findAll()
  ctx.success(users)
})

router.post("/users", async (ctx) => {
  if (!ctx.request.body.name) {
    ctx.fail("Name is required", "VALIDATION_ERROR", 400)
    return
  }
  
  const user = await User.create(ctx.request.body)
  ctx.success(user, "User created", 201)
})

2. 使用 state 传递数据

javascript
// ✅ 推荐
app.use(async (ctx, next) => {
  ctx.state.user = await getUser(ctx)
  await next()
})

// ❌ 不推荐
app.use(async (ctx, next) => {
  ctx.user = await getUser(ctx)  // 可能冲突
  await next()
})
javascript
// 生产环境 Cookie
ctx.cookies.set("session", sessionId, {
  maxAge: 7 * 24 * 60 * 60 * 1000,
  httpOnly: true,
  secure: process.env.NODE_ENV === "production",
  sameSite: "strict",
  signed: true
})

4. 错误处理

javascript
// 全局错误处理
app.use(async (ctx, next) => {
  try {
    await next()
    
    // 404 处理
    if (ctx.status === 404 && !ctx.body) {
      ctx.status = 404
      ctx.body = { 
        success: false, 
        message: "Not Found" 
      }
    }
  } catch (err) {
    ctx.status = err.status || 500
    ctx.body = {
      success: false,
      message: err.message
    }
    ctx.app.emit("error", err, ctx)
  }
})

5. 请求验证

javascript
// 验证中间件
const validate = (schema) => {
  return async (ctx, next) => {
    try {
      ctx.request.body = await schema.validateAsync(ctx.request.body)
      await next()
    } catch (err) {
      ctx.throw(400, err.message)
    }
  }
}

// 使用
router.post("/users", 
  validate(userSchema),
  async (ctx) => {
    const user = await User.create(ctx.request.body)
    ctx.success(user)
  }
)

6. 性能优化

javascript
// 条件请求
app.use(async (ctx, next) => {
  await next()
  
  if (ctx.fresh) {
    ctx.status = 304
    return
  }
})

// 响应压缩
const compress = require("koa-compress")
app.use(compress({
  threshold: 1024,  // 超过 1KB 才压缩
  gzip: { flush: require("zlib").constants.Z_SYNC_FLUSH }
}))

总结

Context 对象是 Koa 的核心,它提供了:

  1. 统一接口:封装了请求和响应对象
  2. 便捷别名:简化常用操作
  3. 状态管理:通过 ctx.state 共享数据
  4. Cookie 管理:内置 Cookie 操作 API
  5. 错误处理:提供 throwassert 方法

掌握 Context 对象是使用 Koa 的基础,建议结合实际项目多加练习。

下一步学习


最后更新:2026年2月