插件与扩展
🎯 引言
学完这篇文章,你能说清楚 Egg 的插件机制是怎么回事,会用 egg-validate 插件给接口加上参数校验,还能通过 extend 给框架对象扩展方法,封装出统一的响应格式。
🧱 插件机制是什么
Egg 本身非常精简,很多能力都是以插件的形式组装的:你前面遇到的 bodyParser、安全校验,其实都是内置插件。
插件就是一个可复用的功能包:别人写好后发布到 npm,我们安装并在配置里启用,就能直接使用它提供的能力。整个流程固定为两步:
npm install安装插件包。- 在
config/plugin.js中声明启用。
接下来用一个高频场景来体验:参数校验。
🛠 用 egg-validate 做参数校验
接口收到请求体后,第一件事是检查参数是否完整、类型是否正确。手写一堆 if 判断很繁琐,egg-validate 插件可以用声明式的规则完成校验。
以下基于 Node.js v22 LTS、egg 3.x、egg-validate 2.0.2 验证通过。先安装:
npm install egg-validate
然后在 config/plugin.js 中启用:
module.exports = {
validate: {
enable: true,
package: 'egg-validate',
},
};
启用后,ctx 上就多了一个 validate 方法。在 Controller 里声明校验规则:
class UserController extends Controller {
async create() {
const { ctx } = this;
// 默认校验 ctx.request.body:name 必须是字符串,age 必须是数字
ctx.validate({
name: { type: 'string', required: true },
age: { type: 'number', required: true },
});
const user = await ctx.service.user.create(ctx.request.body);
ctx.status = 201;
ctx.body = user;
}
}
用 curl 发一个缺少 age 的请求试试:
curl -X POST http://127.0.0.1:7001/users \
-H 'Content-Type: application/json' \
-d '{"name":"小刚"}'
校验不通过时会抛出 422 错误,后面的代码不会执行。想返回自定义的错误格式,用 try/catch 接住:
async create() {
const { ctx } = this;
try {
ctx.validate({
name: { type: 'string', required: true },
age: { type: 'number', required: true },
});
} catch (err) {
ctx.status = 422;
ctx.body = { message: '参数不完整或格式不正确' };
return;
}
// 校验通过后的逻辑
}
🧱 extend 扩展是什么
插件是引入别人的能力,extend 则是自己动手给框架的内置对象添加方法。Egg 允许我们在 app/extend 目录下扩展这些常用对象:
| 文件 | 扩展的对象 | 典型用途 |
|---|---|---|
| application.js | app | 全局共享的方法或数据 |
| context.js | ctx | 每个请求可用的工具方法 |
| helper.js | ctx.helper | 视图和响应相关的辅助函数 |
| request.js | ctx.request | 请求相关的扩展 |
| response.js | ctx.response | 响应相关的扩展 |
其中比较常用的是 helper.js,我们用它来解决一个实际问题:统一响应格式。
🛠 封装统一响应格式
一个项目里,所有接口的返回格式应该统一,前端处理起来才省心。常见的约定是:
{ "code": 0, "data": {}, "message": "ok" }
code 为 0 表示成功,非 0 表示失败。如果每个接口都手写这个结构太重复,把它封装成 helper 方法:
module.exports = {
// 成功响应
success(data = null, message = 'ok') {
this.ctx.body = { code: 0, data, message };
},
// 失败响应
fail(message = '操作失败', code = 1) {
this.ctx.body = { code, data: null, message };
},
};
Controller 里调用起来非常简洁:
class UserController extends Controller {
async show() {
const { ctx } = this;
const user = await ctx.service.user.findById(ctx.params.id);
if (!user) {
ctx.status = 404;
ctx.helper.fail('用户不存在');
return;
}
ctx.helper.success(user);
}
}
this 指向被扩展的对象本身。helper 中通过 this.ctx 就能拿到上下文,设置响应、读取配置都没问题。🛠 扩展 context 示例
再给 ctx 扩展一个实用方法:判断当前请求是不是 Ajax 请求。新建 app/extend/context.js:
module.exports = {
// getter 写法:使用时像访问属性一样 ctx.isAjax
get isAjax() {
return this.get('X-Requested-With') === 'XMLHttpRequest';
},
};
之后在任何 Controller、Service 里都能直接用 ctx.isAjax。可以看到,extend 的写法就是导出一个普通对象,Egg 会把它合并到对应对象上。
🧾 小节总结
- Egg 的能力大量通过插件提供,安装后在
config/plugin.js中声明启用。 egg-validate提供ctx.validate方法,用声明式规则校验参数,失败抛出 422,可用try/catch自定义错误返回。- extend 用于给框架内置对象扩展方法,文件放在
app/extend下,按文件名对应扩展对象。 - 用
app/extend/helper.js封装success/fail,可以让全项目的响应格式统一。 - extend 方法中的
this指向被扩展的对象,helper 里用this.ctx访问上下文。
❓ 知识问答
Q1:插件和中间件有什么区别?
中间件只处理请求链条上的一环;插件是一个完整的功能包,可以包含中间件、extend、配置、定时任务等多种内容。可以理解为:插件是更大的组织单位。
Q2:ctx.validate 校验的是哪份数据?
默认校验 ctx.request.body。想校验其他数据可以传第二个参数,例如 ctx.validate(rules, ctx.query)。
Q3:extend 定义的方法名会和框架自带方法冲突吗?
会的,同名会覆盖框架原有方法,容易引发隐蔽问题。起名时尽量用业务相关的名字,如 success、fail,避开 body、status 这类已有属性。
Q4:helper 和 Service 都能封装逻辑,怎么选?
Service 封装的是业务逻辑(和数据、规则相关),helper 封装的是和请求响应相关的工具方法(如格式化输出、统一响应)。拿不准时,业务归 Service,展示辅助归 helper。
🧪 小练习
- 在 helper 中新增一个
page(list, total)方法,返回{ code: 0, data: { list, total }, message: 'ok' }的分页格式。
module.exports = {
// success、fail 保持原有实现
page(list, total) {
// 请在这里编写代码
},
};
- 给
create接口加上校验规则:name长度在 2 到 10 之间(提示:规则支持min和max)。
ctx.validate({
// 请在这里编写代码
});
🎉 恭喜你已经掌握插件与扩展的用法啦!下一篇是综合实战:用前面学过的全部知识,实现一个完整的用户管理 RESTful 接口。
