3、请求与响应处理:ctx 对象详解
好,咱们进入第三章。这一章我打算把 Koa2 最核心的 ctx 对象彻底讲透。你想想看,后端开发说白了就是处理请求、返回响应。而 ctx 就是这两件事的枢纽。我个人习惯把 ctx 比作一个「快递中转站」——请求是包裹进来,响应是包裹发出去,所有操作都在这个站里完成。
3.1 ctx 对象:请求与响应的统一入口
在 Koa2 中,每个中间件都接收一个 ctx 参数。它身上挂着两个最重要的属性:ctx.request 和 ctx.response。但说实话,Koa 做了很多别名代理,你直接操作 ctx 本身就能完成大部分工作。
核心要点:ctx 是 request 和 response 的语法糖封装。你写 ctx.url 等价于 ctx.request.url,写 ctx.body 等价于 ctx.response.body。
// 示例:ctx 的代理特性
app.use(async (ctx) => {
// 下面两行完全等价
console.log(ctx.url);
console.log(ctx.request.url);
// 下面两行也完全等价
ctx.body = 'Hello';
ctx.response.body = 'Hello';
});
嗯,这里要注意:虽然代理很方便,但有些属性是 request 或 response 独有的。比如 ctx.request.accepts() 就不能简写为 ctx.accepts()。我在项目中遇到过有人混用导致 bug,排查了半天才发现是代理没覆盖到。
3.2 请求处理:从 URL 到请求体
咱们先看请求这一端。Koa2 的 ctx.request 提供了非常丰富的 API。我挑几个最常用的来讲。
3.2.1 URL 与查询参数
获取请求路径和查询参数,是每个接口的第一步。
app.use(async (ctx) => {
// 获取完整 URL
console.log(ctx.url); // /api/users?id=123
// 获取路径部分
console.log(ctx.path); // /api/users
// 获取查询参数字符串
console.log(ctx.querystring); // id=123
// 获取解析后的查询参数对象
console.log(ctx.query); // { id: '123' }
// 获取请求方法
console.log(ctx.method); // GET / POST / PUT / DELETE
});
我个人习惯用 ctx.query 而不是手动解析 querystring。Koa 内部已经帮你用 querystring 模块解析好了,省事。
小技巧:如果你需要获取动态路由参数(比如 /users/:id),那就得用路由中间件了。Koa 本身不解析路由参数,这是框架的设计哲学——保持核心精简。
3.2.2 请求头与请求体
请求头里藏着很多信息,比如 Content-Type、User-Agent 等。获取方式很简单:
app.use(async (ctx) => {
// 获取单个请求头
const contentType = ctx.get('Content-Type');
const userAgent = ctx.get('User-Agent');
// 获取所有请求头
console.log(ctx.headers);
});
至于请求体(request body),Koa2 原生并不解析。你需要引入 koa-bodyparser 中间件。我曾经踩过这个坑——以为 ctx.request.body 直接就能用,结果一直 undefined。
const bodyParser = require('koa-bodyparser');
app.use(bodyParser());
app.use(async (ctx) => {
// 现在可以获取 POST 请求的 body 了
const { name, age } = ctx.request.body;
console.log(name, age);
});
避坑指南:我曾经在生产环境遇到过一个问题——客户端传的是 JSON,但 Content-Type 写成了 text/plain,导致 bodyParser 解析失败。所以前端后端一定要约定好 Content-Type,建议统一用 application/json。
3.3 响应处理:状态码、JSON 与 HTML
响应这块,核心就是 ctx.body 和 ctx.status。Koa2 会根据你赋给 ctx.body 的类型,自动设置 Content-Type。
3.3.1 状态码设置
状态码是客户端判断请求结果的依据。设置方式有两种:
app.use(async (ctx) => {
// 方式一:直接赋值
ctx.status = 200;
// 方式二:通过 throw 抛出(后面章节会细讲)
ctx.throw(400, '参数错误');
// 常见状态码速查
// 200 - 成功
// 201 - 创建成功
// 204 - 无内容
// 301 - 永久重定向
// 302 - 临时重定向
// 400 - 请求参数错误
// 401 - 未授权
// 403 - 禁止访问
// 404 - 资源不存在
// 500 - 服务器内部错误
});
我个人习惯在成功时显式设置 ctx.status = 200,虽然不写默认也是 200。但代码可读性会更好,别人一看就知道这里预期是成功响应。
3.3.2 JSON 响应
现在前后端分离是主流,JSON 响应用得最多。Koa2 处理 JSON 特别简单:
app.use(async (ctx) => {
// 直接赋一个对象,Koa 自动序列化为 JSON
ctx.body = {
code: 0,
message: 'success',
data: {
id: 1,
name: '张三'
}
};
// 此时 Content-Type 自动变为 application/json
});
嗯,这里有个细节:如果你赋的是字符串,Content-Type 会是 text/html;赋的是对象或数组,才是 application/json。我在项目中见过有人手动设置 Content-Type,其实完全没必要。
3.3.3 HTML 响应
虽然现在很少直接返回 HTML,但做 SSR(服务端渲染)或简单页面时还是会用到。
app.use(async (ctx) => {
// 直接返回 HTML 字符串
ctx.body = `
<!DOCTYPE html>
<html>
<head><title>Koa2 实战</title></head>
<body>
<h1>Hello, Koa2!</h1>
</body>
</html>
`;
// 此时 Content-Type 自动变为 text/html; charset=utf-8
});
你想想看,如果 HTML 内容很多,写在字符串里会很难维护。这时候可以用模板引擎,比如 koa-views 配合 ejs 或 pug。后面章节我会专门讲模板引擎的集成。
3.4 文件下载:让用户拿到文件
文件下载是后端常见的需求。Koa2 本身没有专门的下载方法,但我们可以通过设置响应头来实现。我个人习惯封装一个工具函数。
const fs = require('fs');
const path = require('path');
// 文件下载中间件
app.use(async (ctx) => {
const filePath = path.join(__dirname, 'files', 'report.pdf');
// 设置响应头
ctx.set('Content-Type', 'application/octet-stream');
ctx.set('Content-Disposition', `attachment; filename="report.pdf"`);
// 读取文件流并返回
ctx.body = fs.createReadStream(filePath);
});
这里有几个关键点:
- Content-Type 设为
application/octet-stream,告诉浏览器这是二进制流 - Content-Disposition 设为
attachment,强制触发下载而不是直接打开 - 使用流 而不是
fs.readFileSync,避免大文件占用内存
小技巧:如果你需要支持断点续传,还得处理 Range 请求头。不过那是进阶话题了,咱们课程后面会单独讲。
3.5 实战:一个完整的 CRUD 接口
最后,咱们把今天学的知识串起来,写一个完整的用户查询接口。
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const app = new Koa();
app.use(bodyParser());
// 模拟数据库
const users = [
{ id: 1, name: '张三', age: 25 },
{ id: 2, name: '李四', age: 30 }
];
app.use(async (ctx) => {
const { method, path } = ctx;
// GET /users - 获取用户列表
if (method === 'GET' && path === '/users') {
ctx.status = 200;
ctx.body = {
code: 0,
data: users
};
return;
}
// POST /users - 创建用户
if (method === 'POST' && path === '/users') {
const { name, age } = ctx.request.body;
if (!name || !age) {
ctx.status = 400;
ctx.body = { code: 1, message: '参数不完整' };
return;
}
const newUser = { id: users.length + 1, name, age };
users.push(newUser);
ctx.status = 201;
ctx.body = { code: 0, data: newUser };
return;
}
// 未匹配到任何路由
ctx.status = 404;
ctx.body = { code: 1, message: '接口不存在' };
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
这个例子虽然简单,但涵盖了请求方法判断、路径匹配、参数校验、状态码设置、JSON 响应等核心知识点。我在实际项目中,早期就是这样写接口的。后来路由多了才引入 koa-router。
本章小结:
- ctx 是 request 和 response 的代理,大部分操作可以直接在 ctx 上完成
- 请求处理:URL、查询参数、请求头、请求体(需 bodyParser)
- 响应处理:状态码、JSON 自动序列化、HTML 字符串
- 文件下载:设置 Content-Type 和 Content-Disposition,使用流读取
- 实战中注意参数校验和状态码的合理使用
下一章咱们会深入中间件机制,看看 Koa2 的洋葱模型到底是怎么工作的。到时候我会分享一个我踩过的中间件顺序的坑,保证让你印象深刻。