5月27日 18:29

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.xxxctx.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=koa
  • ctx.request.search — 包含 ? 的原始查询字符串
  • ctx.request.type — 请求的 Content-Type
  • ctx.request.accept — 客户端接受的内容类型
  • ctx.request.ip — 客户端 IP 地址

实际开发中,ctx.queryctx.method 是使用频率最高的两个请求属性。获取请求体数据(ctx.request.body)则需要额外引入 koa-bodyparser 中间件,Koa 本身不内置 body 解析功能。

javascript
app.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-Type
  • ctx.redirect(url) — 重定向到指定 URL

完整访问(通过 ctx.response):

  • ctx.response.header — 响应头对象
  • ctx.response.length — 响应 Content-Length
  • ctx.response.lastModified — Last-Modified 时间戳
  • ctx.response.etag — ETag 值

设置 ctx.body 时有一些细节值得注意:如果 body 是一个对象,Koa 会自动设置 Content-Type 为 application/json;如果 body 是字符串,则默认为 text/plain。你也可以通过 ctx.type 手动覆盖。

javascript
app.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 两个方法。

javascript
app.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 时,httpOnlysameSite 是两个安全相关的选项,生产环境中建议始终配置。maxAgeexpires 更常用,因为它指定的是相对时间,不受时区影响。

ctx.throw 和 ctx.assert 怎么用?

Koa 提供了两种错误处理方式:ctx.throw()ctx.assert()

ctx.throw() 用于主动抛出 HTTP 错误:

javascript
app.use(async (ctx) => { const user = await findUser(ctx.query.id); if (!user) { ctx.throw(404, '用户不存在'); } });

ctx.assert()ctx.throw() 的断言封装,条件为 false 时抛出错误:

javascript
app.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.requestctx.response。直接操作 ctx.res 可能会绕过 Koa 的中间件机制,导致响应处理逻辑失效。

实际项目中的 ctx 使用模式

了解了各个属性之后,更重要的是知道在实际项目中如何组织 ctx 的使用。以下是一个典型的中间件链中 ctx 的流转过程:

javascript
const 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 始终是贯穿所有中间件的数据枢纽。

标签:Koa