Context 对象是 Koa 的核心概念,它将 Node.js 的 request 和 response 对象封装到一个对象中,为一次 HTTP 请求-响应的生命周期提供了完整的上下文。通常将其简称为 ctx。
| 特性 | 描述 |
|---|---|
| 统一接口 | 将请求和响应封装在单一对象中 |
| 别名机制 | 提供便捷的属性别名,如 ctx.url 等价于 ctx.request.url |
| 生命周期管理 | 每个请求创建独立的 Context 实例 |
| 状态共享 | 通过 ctx.state 在中间件间传递数据 |
在每个中间件中,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 (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 销毁ctx.request 对象提供对客户端请求的丰富封装,包含请求的所有信息。
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
})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) // 包含查询字符串的完整路径
})// 访问: 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
}
})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"
})Koa 核心不内置请求体解析,需要使用中间件:
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: "***"
}
})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
})// 启用代理支持
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")) {
// 处理文件上传
}
})| 属性/方法 | 描述 | 示例 |
|---|---|---|
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') |
ctx.response 对象用于控制对客户端的响应,包含响应状态、头部、内容等信息。
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 | 服务不可用 |
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)
})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.body = "OK"
})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" }
})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")
})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")
})app.use(async (ctx) => {
// 设置最后修改时间
ctx.lastModified = new Date()
// 设置 ETag
ctx.etag = "unique-identifier"
// 检查客户端缓存
if (ctx.fresh) {
ctx.status = 304
return
}
ctx.body = { data: "..." }
})| 属性/方法 | 描述 | 示例 |
|---|---|---|
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) { ... } |
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)
}// 将 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。
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()
})Koa 通过 ctx.cookies 提供方便的 API 来操作 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"
})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 }
})| 选项 | 类型 | 描述 | 示例 |
|---|---|---|---|
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 被篡改:
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 实现
const sessions = new Map()
app.use(async (ctx, next) => {
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 配置
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 // 防止篡改
})
})Koa 提供了多种错误处理方式,可以灵活地处理 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() 用于断言检查,失败时抛出错误:
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 处理中间件(放在最后)
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
}
}
})| 属性 | 类型 | 描述 |
|---|---|---|
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.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.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) | - | 文件下载 |
可以,在响应发送前都可以修改:
app.use(async (ctx, next) => {
ctx.body = { message: "Initial" }
await next()
// 在这里还可以修改
ctx.body = {
...ctx.body,
timestamp: Date.now()
}
})使用 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 }
})// 简单配置
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: "*" }))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 }
})使用 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
}
})// 启用代理支持
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 }
})// 响应格式化中间件
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)
})// ✅ 推荐
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()
})// 生产环境 Cookie
ctx.cookies.set("session", sessionId, {
maxAge: 7 * 24 * 60 * 60 * 1000,
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
signed: true
})// 全局错误处理
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)
}
})// 验证中间件
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)
}
)// 条件请求
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 共享数据throw 和 assert 方法掌握 Context 对象是使用 Koa 的基础,建议结合实际项目多加练习。
最后更新:2026年2月