Egg.js 简介

Service 业务逻辑层

理解为什么要把业务逻辑拆到 Service 层,学会编写 Service 并在 Controller 中调用它。

🎯 引言

学完这篇文章,你能说清楚 Controller 和 Service 的分工,会把数据操作代码写到 app/service 下,并在 Controller 中通过 ctx.service 调用它,让项目结构更符合 Egg 的规范。


🧱 为什么需要 Service

上一篇我们把取用户数据、存用户数据的逻辑都写在了 Controller 里。功能能跑,但随着业务变复杂,Controller 会越来越臃肿。

可以这样想:Controller 像餐厅的前台,负责接待客人、确认点单、把菜端上桌;Service 像后厨,负责真正的切菜、炒菜。让前台去炒菜,客人一多就乱套了。

拆出 Service 层还有实际好处:

  • 同一段业务逻辑可以被多个 Controller 复用。
  • 将来换成数据库时,只改 Service,Controller 一行不动。
  • Controller 变薄,一眼就能看出接口的处理流程。

🛠 编写第一个 Service

app/service 下新建 user.js,用 class 继承 Service

app/service/user.js
const { Service } = require('egg');

class UserService extends Service {
    async findAll() {
        // 先用固定数据模拟,后面会换成真实数据操作
        return [
            { id: 1, name: '小明', age: 18 },
            { id: 2, name: '小红', age: 20 },
        ];
    }
}

module.exports = UserService;

挂载规则和 Controller 一样:app/service/user.js 里的 findAll 方法,通过 ctx.service.user.findAll() 调用。Controller 中这样使用:

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

class UserController extends Controller {
    async index() {
        const { ctx } = this;
        const users = await ctx.service.user.findAll();
        ctx.body = users;
    }
}

module.exports = UserController;

访问 GET /users,就能拿到用户列表了。在 Service 里同样可以通过 this.ctx 访问上下文,需要时也能用 this.app 拿到应用实例。


🛠 用内存数组模拟数据操作

学习阶段还没接数据库,我们用一个内存数组模拟,把增删改查封装成 Service 方法:

app/service/user.js
const { Service } = require('egg');

// 用内存数组模拟数据库:定义在模块顶层,所有请求共享,服务重启后数据还原
const users = [
    { id: 1, name: '小明', age: 18 },
    { id: 2, name: '小红', age: 20 },
];
let nextId = 3;

class UserService extends Service {
    // 查询全部
    async findAll() {
        return users;
    }

    // 按 id 查询单个
    async findById(id) {
        return users.find(item => item.id === Number(id));
    }

    // 新增
    async create(data) {
        const user = { id: nextId++, name: data.name, age: data.age };
        users.push(user);
        return user;
    }

    // 修改
    async update(id, data) {
        const user = await this.findById(id);
        if (!user) return null;

        if (data.name !== undefined) user.name = data.name;
        if (data.age !== undefined) user.age = data.age;
        return user;
    }

    // 删除
    async remove(id) {
        const index = users.findIndex(item => item.id === Number(id));
        if (index === -1) return false;

        users.splice(index, 1);
        return true;
    }
}

module.exports = UserService;
注意数组要定义在 class 外面(模块顶层)。Egg 会为每个请求创建新的 Service 实例,如果数组挂在实例上(如 this.users),每个请求拿到的都是全新的空数据,新增后立刻查询会看不到。定义在模块顶层才能让所有请求共享同一份数据。当然这只是学习用的临时方案,真实项目要换成数据库。

🛠 Controller 调用 Service 的完整流程

配合上一篇的 router.resources('users', '/users', controller.user),Controller 变成纯粹的“调度员”:

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

class UserController extends Controller {
    // GET /users
    async index() {
        const { ctx } = this;
        ctx.body = await ctx.service.user.findAll();
    }

    // GET /users/:id
    async show() {
        const { ctx } = this;
        const user = await ctx.service.user.findById(ctx.params.id);

        if (!user) {
            ctx.status = 404;
            ctx.body = { message: '用户不存在' };
            return;
        }

        ctx.body = user;
    }

    // POST /users
    async create() {
        const { ctx } = this;
        const user = await ctx.service.user.create(ctx.request.body);
        ctx.status = 201;
        ctx.body = user;
    }
}

module.exports = UserController;

可以看到 Controller 里已经没有任何数据操作的细节了,只有取参数、调 Service、返回结果三步。


⚡ 为什么 Service 方法都写成 async

你可能会疑惑:操作内存数组明明是同步的,为什么要加 async

这是为了面向未来编程。真实项目中 Service 会操作数据库,而数据库操作是异步的。现在统一用 async 定义、用 await 调用,将来换成数据库时,Service 内部的实现变了,但方法签名不变,Controller 不用做任何修改。


🧾 小节总结

  • Service 是业务逻辑层,负责数据操作和具体业务;Controller 只负责接收请求、调用 Service、返回结果。
  • app/service/user.js 中定义 class 继承 Service,通过 ctx.service.user.xxx() 调用。
  • Service 里可以用 this.ctx 访问上下文、this.app 访问应用实例。
  • 学习阶段可以用内存数组模拟增删改查,但它只是临时方案,真实项目要接数据库。
  • Service 方法统一写成 async,为将来接入数据库留好接口。

❓ 知识问答

Q1:Controller 和 Service 的代码看起来很薄,会不会是过度设计?

小 demo 确实感觉不明显,但业务一复杂差距就出来了。而且 Egg 项目都遵循这个约定,从一开始就按规范写,能养成好习惯,团队协作时也更顺畅。

Q2:Service 里能调用另一个 Service 吗?

可以,通过 this.ctx.service.另一个Service名 调用。比如订单 Service 里调用用户 Service 查用户信息。

Q3:为什么数组要定义在 class 外面?

Egg 默认每个请求都会创建新的 Service 实例,挂在实例上的数据互不影响。定义在模块顶层后,模块只会加载一次,数组就能被所有请求共享,新增、修改才能持续生效。

Q4:Service 必须继承 Service 基类吗?

按 Egg 的约定需要继承,这样框架才会自动挂载到 ctx.service 上,并注入 this.ctxthis.app


🧪 小练习

新建 app/service/goods.js,封装一个商品 Service,包含 findAllfindById 两个方法(内存数组模拟),并在 Controller 中调用。

app/service/goods.js
const { Service } = require('egg');

// 模块顶层的模拟数据
const goods = [
    { id: 1, name: '键盘', price: 99 },
    { id: 2, name: '鼠标', price: 49 },
];

class GoodsService extends Service {
    async findAll() {
        // 请在这里编写代码
    }

    async findById(id) {
        // 请在这里编写代码
    }
}

module.exports = GoodsService;

🎉 恭喜你已经掌握 Service 业务逻辑层啦!下一篇我们学习配置与中间件,看看 Egg 的配置体系和自定义中间件怎么用。