Koa 上下文对象 Context
概述
Context 对象是 Koa 的核心概念,它将 Node.js 的 request 和 response 对象封装到一个对象中,为一次 HTTP 请求-响应的生命周期提供了完整的上下文。通常将其简称为 ctx。
核心特性
| 特性 | 描述 |
|---|---|
| 统一接口 | 将请求和响应封装在单一对象中 |
| 别名机制 | 提供便捷的属性别名,如 ctx.url 等价于 ctx.request.url |
| 生命周期管理 | 每个请求创建独立的 Context 实例 |
| 状态共享 | 通过 ctx.state 在中间件间传递数据 |
Context 对象属性
在每个中间件中,ctx 都是第一个参数:
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.method | ctx.request.method | 请求方法 |
ctx.url | ctx.request.url | 请求 URL |
ctx.header | ctx.request.header | 请求头对象 |
ctx.headers | ctx.request.headers | 请求头对象(别名) |
ctx.query | ctx.request.query | 解析后的查询字符串对象 |
ctx.querystring | ctx.request.querystring | 原始查询字符串 |
ctx.path | ctx.request.path | 请求路径 |
ctx.host | ctx.request.host | 主机名 |
ctx.body | ctx.response.body | 响应体 |
ctx.status | ctx.response.status | 响应状态码 |
ctx.type | ctx.response.type | Content-Type |
ctx.set() | ctx.response.set() | 设置响应头 |
ctx.redirect() | ctx.response.redirect() | 重定向 |
Context 对象结构
对象关系图
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 实例引用请求生命周期
请求进入
│
├─→ 创建 Context 实例
│ │
│ ├─→ ctx.request 封装原生 req
│ ├─→ ctx.response 封装原生 res
│ └─→ ctx.state = {} 初始化状态
│
├─→ 中间件执行(洋葱模型)
│ │
│ ├─→ 中间件 1
│ ├─→ 中间件 2
│ └─→ 中间件 N
│
├─→ 响应发送
│ │
│ └─→ ctx.response.body → 客户端
│
└─→ Context 销毁Request 对象详解
ctx.request 对象提供对客户端请求的丰富封装,包含请求的所有信息。
请求基本信息
method - 请求方法
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
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 - 查询参数对象
// 访问: 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 - 原始查询字符串
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
npm install koa-bodyparserconst 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
npm install koa-bodyconst { 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 }
})请求头
获取请求头
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 }
})常用请求头
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 地址
// 启用代理支持
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 }
})主机信息
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 提供了强大的内容协商功能:
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 | 是否 HTTPS | true, false |
ctx.ip | 客户端 IP | 127.0.0.1 |
ctx.ips | IP 数组 | ['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 - 状态码
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 | 成功 | |
| 200 | OK | 请求成功 |
| 201 | Created | 资源创建成功 |
| 204 | No Content | 删除成功,无返回内容 |
| 3xx | 重定向 | |
| 301 | Moved Permanently | 永久重定向 |
| 302 | Found | 临时重定向 |
| 304 | Not Modified | 缓存有效 |
| 4xx | 客户端错误 | |
| 400 | Bad Request | 请求参数错误 |
| 401 | Unauthorized | 未认证 |
| 403 | Forbidden | 无权限 |
| 404 | Not Found | 资源不存在 |
| 422 | Unprocessable Entity | 验证失败 |
| 429 | Too Many Requests | 请求过于频繁 |
| 5xx | 服务器错误 | |
| 500 | Internal Server Error | 服务器内部错误 |
| 502 | Bad Gateway | 网关错误 |
| 503 | Service Unavailable | 服务不可用 |
响应体
body - 响应内容
ctx.body 支持多种数据类型:
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
})流式响应
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 - 设置响应头
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"
})常用响应头设置
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 - 内容类型
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 - 页面重定向
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 - 文件附件
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
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.etag | ETag | ctx.etag = "abc123" |
ctx.fresh | 缓存是否有效 | if (ctx.fresh) { ... } |
Delegate 机制详解
Koa 的 context.js 使用 delegates 库将 request 和 response 对象的属性与方法委托到 ctx 上,使得开发者可以直接通过 ctx.xxx 访问,而无需写成 ctx.request.xxx 或 ctx.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.ip → ctx.request.ip |
access | 委托读写属性 | 同时定义 getter 和 setter | ctx.body → ctx.response.body |
其中 setter 是 access 的组成部分,单独使用时只委托写操作:proto.__defineSetter__(key, function(val){ return this[target][key] = val })。
委托链路示例
// 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 中的委托声明
// 将 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') // 只读当 request 和 response 上存在同名属性时,后声明的委托会覆盖先声明的。Koa 源码中先委托 response,再委托 request,因此同名属性最终会指向 request 上的值。开发时应注意避免歧义,必要时显式使用 ctx.request.xxx 或 ctx.response.xxx。
State 状态对象
ctx.state 是推荐的命名空间,用于在中间件之间传递信息或向视图模板暴露数据。这是在请求生命周期内共享数据的标准方式。
基本使用
// 中间件 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}`
})典型应用场景
用户认证
// 认证中间件
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
}
})数据预加载
// 预加载中间件
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
})
})请求追踪
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`)
})最佳实践
// ✅ 推荐:使用 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,无需额外安装中间件。
基本操作
设置 Cookie
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"
})获取 Cookie
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 }
})Cookie 配置选项
| 选项 | 类型 | 描述 | 示例 |
|---|---|---|---|
maxAge | Number | 过期时间(毫秒) | maxAge: 86400000 |
expires | Date | 过期日期 | expires: new Date('2025-12-31') |
path | String | 生效路径 | path: '/' |
domain | String | 生效域名 | domain: 'example.com' |
secure | Boolean | 仅 HTTPS | secure: true |
httpOnly | Boolean | 仅 HTTP 访问 | httpOnly: true |
sameSite | String | SameSite 策略 | sameSite: 'strict' |
signed | Boolean | 签名 Cookie | signed: true |
overwrite | Boolean | 覆盖同名 Cookie | overwrite: true |
Cookie 签名
签名可以防止 Cookie 被篡改:
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 }
})实际应用示例
访问计数器
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.`
})用户偏好设置
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
}
})Session Cookie
// 简单的 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()
})Cookie 安全最佳实践
// 生产环境 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,无需额外安装中间件
Cookie 基本操作
ctx.cookies.get(name, [options]):获取指定名称的 Cookiectx.cookies.set(name, value, [options]):设置 Cookie
示例:
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.`
})Cookie 配置选项
set 方法的 options 参数可以精细地控制 Cookie 的行为:
| 选项 | 描述 |
|---|---|
maxAge | Cookie 过期的毫秒数 |
expires | Cookie 过期的 Date 对象 |
path | Cookie 生效的路径,默认为 / |
domain | Cookie 生效的域名 |
secure | true 表示仅通过 HTTPS 发送 |
httpOnly | true 表示无法通过客户端 JavaScript 访问 |
signed | true 表示需要对 Cookie 进行签名,防止篡改。需要设置 app.keys |
overwrite | true 表示覆盖同名 Cookie,默认为 false |
Cookie 签名
为了防止 Cookie 被篡改,可以使用签名 Cookie:
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 状态码的错误:
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() 用于断言检查,失败时抛出错误:
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 }
})全局错误处理
错误处理中间件
// 错误处理中间件(必须放在最前面)
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 }
})自定义错误类
// 自定义错误类
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 }
})错误事件监听
// 监听错误事件
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 处理
// 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.req | Object | Node.js 原生 request 对象 |
ctx.res | Object | Node.js 原生 response 对象 |
ctx.request | Object | Koa Request 对象 |
ctx.response | Object | Koa Response 对象 |
ctx.state | Object | 推荐的命名空间,用于传递数据 |
ctx.app | Object | Application 实例引用 |
方法
| 方法 | 描述 | 示例 |
|---|---|---|
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.method | String | 请求方法 |
request.url | String | 请求 URL |
request.path | String | 请求路径 |
request.query | Object | 查询参数对象 |
request.querystring | String | 查询字符串 |
request.header | Object | 请求头对象 |
request.host | String | 主机名 |
request.hostname | String | 主机名(无端口) |
request.protocol | String | 协议 |
request.secure | Boolean | 是否 HTTPS |
request.ip | String | 客户端 IP |
request.ips | Array | IP 数组 |
request.body | Any | 请求体(需中间件) |
request.get(field) | String | 获取请求头 |
request.accepts(types) | String/Boolean | 内容协商 |
request.is(types) | String/Boolean | 检查类型 |
Response API
| 属性/方法 | 类型 | 描述 |
|---|---|---|
response.status | Number | 状态码 |
response.message | String | 状态消息 |
response.body | Any | 响应体 |
response.type | String | 内容类型 |
response.length | Number | 内容长度 |
response.header | Object | 响应头对象 |
response.lastModified | Date | 最后修改时间 |
response.etag | String | ETag |
response.fresh | Boolean | 缓存是否有效 |
response.set(field, value) | - | 设置响应头 |
response.append(field, value) | - | 追加响应头 |
response.remove(field) | - | 删除响应头 |
response.redirect(url) | - | 重定向 |
response.attachment(filename) | - | 文件下载 |
常见问题
Q1: ctx.body 赋值后还可以修改吗?
可以,在响应发送前都可以修改:
app.use(async (ctx, next) => {
ctx.body = { message: "Initial" }
await next()
// 在这里还可以修改
ctx.body = {
...ctx.body,
timestamp: Date.now()
}
})Q2: 如何获取 POST 请求体?
使用 koa-bodyparser 或 koa-body 中间件:
const bodyParser = require("koa-bodyparser")
app.use(bodyParser())
app.post("/data", async (ctx) => {
const body = ctx.request.body
ctx.body = { received: body }
})Q3: 如何设置跨域?
// 简单配置
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 查询参数本身就是字符串,需要手动转换:
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:
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?
// 启用代理支持
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. 统一响应格式
// 响应格式化中间件
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 传递数据
// ✅ 推荐
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()
})3. 合理设置 Cookie
// 生产环境 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. 错误处理
// 全局错误处理
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. 请求验证
// 验证中间件
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. 性能优化
// 条件请求
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 的核心,它提供了:
- 统一接口:封装了请求和响应对象
- 便捷别名:简化常用操作
- 状态管理:通过
ctx.state共享数据 - Cookie 管理:内置 Cookie 操作 API
- 错误处理:提供
throw和assert方法
掌握 Context 对象是使用 Koa 的基础,建议结合实际项目多加练习。
下一步学习
最后更新:2026年2月