路由与 Controller
🎯 引言
学完这篇文章,你能在 router.js 里定义各种请求方法的路由,用 Controller 接收路径参数、查询参数和请求体,并按需返回数据和状态码。同时你会理解 Controller 应该做什么、不应该做什么。
🧱 路由是什么
路由负责回答一个问题:某个 URL 的请求,交给谁来处理。
在 Node.js 课程里我们用 req.url 手写判断实现过简单路由。Egg 把这件事规范化:所有路由统一写在 app/router.js 里,处理逻辑统一放在 Controller 中。
module.exports = app => {
const { router, controller } = app;
router.get('/users', controller.user.index);
};
controller.user.index 对应的就是 app/controller/user.js 里的 index 方法。这就是约定:文件名即挂载路径,不用手动引入。
🛠 常用请求方法
router 支持 GET、POST、PUT、DELETE 等常用请求方法,和 HTTP 方法一一对应:
module.exports = app => {
const { router, controller } = app;
router.get('/users', controller.user.index); // 获取列表
router.get('/users/:id', controller.user.show); // 获取单个
router.post('/users', controller.user.create); // 新增
router.put('/users/:id', controller.user.update); // 修改
router.delete('/users/:id', controller.user.destroy); // 删除
};
/users/:id 中的 :id 是路径参数,表示这一段是动态的值,稍后可以在 Controller 里取出来。
🧱 Controller 的写法
Egg 官方推荐用 class 继承 Controller 的方式编写控制器:
const { Controller } = require('egg');
class UserController extends Controller {
async index() {
const { ctx } = this;
ctx.body = '用户列表';
}
}
module.exports = UserController;
要点有两个:
- 每个方法都是
async方法,通过this.ctx拿到上下文对象ctx,它和 Koa 里的ctx是同一个东西。 - 文件导出这个 class,Egg 会按
controller.文件名.方法名的规则挂载。
🧱 获取请求参数
客户端传参主要有三种途径,获取方式各不相同:
class UserController extends Controller {
async show() {
const { ctx } = this;
// 1. 路径参数:GET /users/1 中的 1
const id = ctx.params.id;
// 2. 查询参数:GET /users?keyword=小明 中的 keyword
const keyword = ctx.query.keyword;
ctx.body = { id, keyword };
}
async create() {
const { ctx } = this;
// 3. 请求体:POST 提交的数据
const data = ctx.request.body;
ctx.body = { 收到的数据: data };
}
}
ctx.request.body 依赖内置的 bodyParser 能力,请求时需要携带 Content-Type: application/json 请求头,否则可能拿不到解析后的数据。注意 ctx.params 和 ctx.query 取到的值都是字符串,比如 ctx.params.id 得到的是 '1',需要数字时要自己转换。
🧱 返回响应与状态码
给 ctx.body 赋值就是返回响应,赋对象自动转 JSON。状态码用 ctx.status 设置:
class UserController extends Controller {
async create() {
const { ctx } = this;
const user = { id: 1, name: ctx.request.body.name };
// 新增成功,按 RESTful 惯例返回 201
ctx.status = 201;
ctx.body = user;
}
async show() {
const { ctx } = this;
if (ctx.params.id === '999') {
// 资源不存在,返回 404
ctx.status = 404;
ctx.body = { message: '用户不存在' };
return;
}
ctx.body = { id: ctx.params.id, name: '小明' };
}
}
常用状态码:200 成功、201 创建成功、400 参数错误、404 资源不存在、500 服务器错误。
💡 RESTful 风格的 router.resources
上面五条路由是用户管理的标准写法,Egg 提供了一个语法糖,一行搞定:
module.exports = app => {
const { router, controller } = app;
router.resources('users', '/users', controller.user);
};
它等价的路由映射如下:
| 请求方法 | 路径 | Controller 方法 | 说明 |
|---|---|---|---|
| GET | /users | index | 获取列表 |
| GET | /users/:id | show | 获取单个 |
| POST | /users | create | 新增 |
| PUT | /users/:id | update | 修改 |
| DELETE | /users/:id | destroy | 删除 |
resources 还会注册 new、edit 两个用于渲染页面的路由,做纯接口开发时用不到,Controller 里只实现需要的五个方法即可。🪤 Controller 的职责定位
初学时容易把所有代码都堆在 Controller 里。规范的做法是 Controller 只做四件事:
- 接收请求:从
ctx中取出参数。 - 校验参数:检查参数是否完整、合法。
- 调用 Service:把具体业务交给 Service 层处理。
- 返回结果:组织数据并设置响应。
至于查询数据、加工数据这些业务逻辑,应该放到 Service 层。Controller 保持“瘦”一点,项目才好维护,这也是下一篇的主题。
🧾 小节总结
- 路由统一写在
app/router.js,Controller 按controller.文件名.方法名自动挂载。 router.get/post/put/delete对应 HTTP 方法,router.resources一行生成 RESTful 全套路由。- Controller 用 class 继承
Controller,通过this.ctx访问上下文。 - 三种取参方式:
ctx.params路径参数、ctx.query查询参数、ctx.request.body请求体。 ctx.body返回响应,ctx.status设置状态码;Controller 只负责接收、校验、调用、返回。
❓ 知识问答
Q1:ctx.params 和 ctx.query 有什么区别?
ctx.params 取路径中的动态片段,如 /users/:id 里的 id;ctx.query 取问号后面的查询字符串,如 /users?page=2 里的 page。两者取到的都是字符串。
Q2:为什么 ctx.request.body 有时候是空对象?
常见原因是请求没带 Content-Type: application/json 请求头,bodyParser 不知道该按什么格式解析。用 curl、Apifox 或前端代码发请求时注意加上。
Q3:路由和方法名必须叫 index、show、create 吗?
手写 router.get 时方法名随意;用 router.resources 时,方法名必须按约定的 index/show/create/update/destroy 命名,否则映射不上。
Q4:一个路由可以直接写回调,不用 Controller 吗?
技术上可以,但不符合 Egg 的约定。统一走 Controller 能让项目结构保持一致,也方便后续扩展 Service、中间件等能力。
🧪 小练习
给项目添加一个商品模块:定义 GET /goods/:id 路由,Controller 中同时取出路径参数 id 和查询参数 from,以 JSON 形式返回。
const { Controller } = require('egg');
class GoodsController extends Controller {
async show() {
const { ctx } = this;
// 请在这里编写代码
}
}
module.exports = GoodsController;
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
// 请在这里编写代码
};
🎉 恭喜你已经掌握路由与 Controller 的用法啦!下一篇我们学习 Service 业务逻辑层,把 Controller 里的业务代码拆出去,让项目结构更清晰。
