Koa 中 Context 对象 ctx 有哪些核心属性和用法?
Koa 的 Context 对象是什么?
Koa 的 Context 对象(即 ctx)是 Koa 框架中最核心的概念之一。它将 Node.js 原生的 request 和 response 对象封装到一个统一的对象中,并通过代理机制让开发者可以直接在 ctx 上访问请求和响应的属性,不必反复切换 req/res。
理解 ctx,本质上就是理解 Koa 的设计哲学——用更少的代码完成更多的事情。
ctx 的代理机制是怎么工作的?
很多开发者只知道 ctx.query 能拿到查询参数,但并不清楚它为什么能直接用。实际上,ctx 上许多属性并不是自己定义的,而是通过 Object.defineProperty 代理到 ctx.request 和 ctx.response 上的。
具体来说,当你访问 ctx.query 时,实际执行的是 ctx.request.query;当你设置 ctx.body 时,实际设置的是 ctx.response.body。这种代理机制的好处是减少了代码嵌套层级,让中间件的写法更加扁平。
需要注意的一点是,并非所有 request/response 上的属性都被代理了。对于没有被代理的属性,你仍然需要通过 ctx.request.xxx 或 ctx.response.xxx 来访问。
请求相关属性有哪些?
ctx 提供了两组请求属性的访问方式:便捷访问和完整访问。
便捷访问(直接通过 ctx):
ctx.url— 请求路径,包含查询字符串ctx.method— 请求方法(GET、POST 等)ctx.header— 请求头对象ctx.query— 解析后的查询字符串对象,例如/api?name=koa会得到{ name: 'koa' }ctx.path— 请求路径,不包含查询字符串ctx.host— 请求的主机名
完整访问(通过 ctx.request):
ctx.request.querystring— 原始查询字符串(未解析),例如name=koactx.request.search— 包含?的原始查询字符串ctx.request.type— 请求的 Content-Typectx.request.accept— 客户端接受的内容类型ctx.request.ip— 客户端 IP 地址
实际开发中,ctx.query 和 ctx.method 是使用频率最高的两个请求属性。获取请求体数据(ctx.request.body)则需要额外引入 koa-bodyparser 中间件,Koa 本身不内置 body 解析功能。
javascriptapp.use(async (ctx) => { // 获取查询参数 const { page, size } = ctx.query; // 获取请求方法和路径 console.log(ctx.method, ctx.path); // 获取客户端 IP const ip = ctx.request.ip; });
响应相关属性有哪些?
和请求类似,响应也有便捷访问和完整访问两种方式。
便捷访问(直接通过 ctx):
ctx.body— 响应体,支持字符串、Buffer、Stream、Object(自动序列化为 JSON)ctx.status— HTTP 状态码ctx.type— 响应的 Content-Typectx.redirect(url)— 重定向到指定 URL
完整访问(通过 ctx.response):
ctx.response.header— 响应头对象ctx.response.length— 响应 Content-Lengthctx.response.lastModified— Last-Modified 时间戳ctx.response.etag— ETag 值
设置 ctx.body 时有一些细节值得注意:如果 body 是一个对象,Koa 会自动设置 Content-Type 为 application/json;如果 body 是字符串,则默认为 text/plain。你也可以通过 ctx.type 手动覆盖。
javascriptapp.use(async (ctx) => { // 返回 JSON ctx.body = { code: 0, data: { list: [] } }; // 返回 HTML ctx.type = 'html'; ctx.body = '<h1>Hello</h1>'; // 设置状态码后重定向 ctx.status = 302; ctx.redirect('/login'); });
ctx.state 有什么用?
ctx.state 是 Koa 官方推荐的命名空间,用于在中间件之间传递数据。它的设计初衷是避免在 ctx 上随意挂载属性导致命名冲突。
javascript// 认证中间件 app.use(async (ctx, next) => { const token = ctx.header.authorization; if (token) { ctx.state.user = verifyToken(token); // 将用户信息挂到 state 上 } await next(); }); // 业务中间件 app.use(async (ctx) => { const user = ctx.state.user; // 从 state 取出用户信息 ctx.body = { name: user.name }; });
这个模式在实际项目中非常常见。除了用户信息,你还可以用它存储请求 ID、权限标识、分页参数等中间件间需要共享的数据。
ctx.cookies 怎么操作?
Koa 内置了 Cookie 操作能力,不需要额外安装中间件。ctx.cookies 提供了 get 和 set 两个方法。
javascriptapp.use(async (ctx) => { // 读取 Cookie const sessionId = ctx.cookies.get('sid'); // 设置 Cookie ctx.cookies.set('sid', 'abc123', { maxAge: 86400000, // 有效期 1 天,单位毫秒 httpOnly: true, // 禁止 JS 访问,防止 XSS secure: true, // 仅 HTTPS 传输 sameSite: 'lax', // 防止 CSRF }); });
设置 Cookie 时,httpOnly 和 sameSite 是两个安全相关的选项,生产环境中建议始终配置。maxAge 比 expires 更常用,因为它指定的是相对时间,不受时区影响。
ctx.throw 和 ctx.assert 怎么用?
Koa 提供了两种错误处理方式:ctx.throw() 和 ctx.assert()。
ctx.throw() 用于主动抛出 HTTP 错误:
javascriptapp.use(async (ctx) => { const user = await findUser(ctx.query.id); if (!user) { ctx.throw(404, '用户不存在'); } });
ctx.assert() 是 ctx.throw() 的断言封装,条件为 false 时抛出错误:
javascriptapp.use(async (ctx) => { ctx.assert(ctx.query.id, 400, '缺少用户 ID'); ctx.assert(ctx.state.user, 401, '未登录'); });
两种方式抛出的错误都会被 Koa 的错误事件捕获,你可以在 app.on('error') 中统一处理日志记录和监控上报。相比之下,ctx.assert() 的写法更简洁,适合做参数校验。
ctx.app 是什么?
ctx.app 是当前 Koa 应用实例的引用。通过它可以访问应用级别的配置和回调,比如 ctx.app.env 获取运行环境、ctx.app.proxy 判断是否信任代理头等。日常开发中用得不多,但在编写通用中间件时偶尔需要。
ctx.req 和 ctx.res 与 ctx.request 和 ctx.response 有什么区别?
这是初学者容易混淆的一对概念:
ctx.req/ctx.res— Node.js 原生的 http 模块请求和响应对象,功能原始,不经过 Koa 封装ctx.request/ctx.response— Koa 封装后的对象,提供了更友好的 API
除非你需要操作一些 Koa 没有封装的底层功能(比如 ctx.res.writeHead()),否则应始终优先使用 ctx.request 和 ctx.response。直接操作 ctx.res 可能会绕过 Koa 的中间件机制,导致响应处理逻辑失效。
实际项目中的 ctx 使用模式
了解了各个属性之后,更重要的是知道在实际项目中如何组织 ctx 的使用。以下是一个典型的中间件链中 ctx 的流转过程:
javascriptconst Koa = require('koa'); const app = new Koa(); // 请求日志中间件 app.use(async (ctx, next) => { const start = Date.now(); await next(); const duration = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ctx.status} - ${duration}ms`); }); // 认证中间件 app.use(async (ctx, next) => { const token = ctx.header.authorization; ctx.assert(token, 401, '缺少认证信息'); ctx.state.user = verifyToken(token); await next(); }); // 业务路由 app.use(async (ctx) => { const { page = 1, size = 10 } = ctx.query; const list = await getList(ctx.state.user.id, page, size); ctx.status = 200; ctx.body = { code: 0, data: { list, total: list.length } }; }); app.listen(3000);
这个例子展示了 ctx 在整个请求生命周期中的角色:从日志中间件读取 method 和 url,到认证中间件校验 header 和写入 state,再到业务层读取 query 和设置 body,ctx 始终是贯穿所有中间件的数据枢纽。