Egg.js 简介

路由与 Controller

掌握 Egg.js 的路由定义、Controller 写法、请求参数获取与响应返回,理解 Controller 的职责定位。

🎯 引言

学完这篇文章,你能在 router.js 里定义各种请求方法的路由,用 Controller 接收路径参数、查询参数和请求体,并按需返回数据和状态码。同时你会理解 Controller 应该做什么、不应该做什么。


🧱 路由是什么

路由负责回答一个问题:某个 URL 的请求,交给谁来处理

在 Node.js 课程里我们用 req.url 手写判断实现过简单路由。Egg 把这件事规范化:所有路由统一写在 app/router.js 里,处理逻辑统一放在 Controller 中。

app/router.js
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 方法一一对应:

app/router.js
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 的方式编写控制器:

app/controller/user.js
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.文件名.方法名 的规则挂载。
Egg 也支持函数式(普通对象)写法,但官方脚手架和文档以 class 风格为主,本课程统一使用 class 写法。

🧱 获取请求参数

客户端传参主要有三种途径,获取方式各不相同:

app/controller/user.js
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.paramsctx.query 取到的值都是字符串,比如 ctx.params.id 得到的是 '1',需要数字时要自己转换。


🧱 返回响应与状态码

ctx.body 赋值就是返回响应,赋对象自动转 JSON。状态码用 ctx.status 设置:

app/controller/user.js
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 提供了一个语法糖,一行搞定:

app/router.js
module.exports = app => {
    const { router, controller } = app;
    router.resources('users', '/users', controller.user);
};

它等价的路由映射如下:

请求方法路径Controller 方法说明
GET/usersindex获取列表
GET/users/:idshow获取单个
POST/userscreate新增
PUT/users/:idupdate修改
DELETE/users/:iddestroy删除
resources 还会注册 newedit 两个用于渲染页面的路由,做纯接口开发时用不到,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 里的 idctx.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 形式返回。

app/controller/goods.js
const { Controller } = require('egg');

class GoodsController extends Controller {
    async show() {
        const { ctx } = this;
        // 请在这里编写代码
    }
}

module.exports = GoodsController;
app/router.js
module.exports = app => {
    const { router, controller } = app;
    router.get('/', controller.home.index);
    // 请在这里编写代码
};

🎉 恭喜你已经掌握路由与 Controller 的用法啦!下一篇我们学习 Service 业务逻辑层,把 Controller 里的业务代码拆出去,让项目结构更清晰。