Service 业务逻辑层
🎯 引言
学完这篇文章,你能说清楚 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:
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 中这样使用:
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 方法:
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;
this.users),每个请求拿到的都是全新的空数据,新增后立刻查询会看不到。定义在模块顶层才能让所有请求共享同一份数据。当然这只是学习用的临时方案,真实项目要换成数据库。🛠 Controller 调用 Service 的完整流程
配合上一篇的 router.resources('users', '/users', controller.user),Controller 变成纯粹的“调度员”:
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.ctx 和 this.app。
🧪 小练习
新建 app/service/goods.js,封装一个商品 Service,包含 findAll 和 findById 两个方法(内存数组模拟),并在 Controller 中调用。
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 的配置体系和自定义中间件怎么用。
