Egg 的「约定优于配置」落到实处,靠的是插件(Plugin)、中间件(Middleware) 与 框架扩展(Extend) 三套机制。掌握它们,你才能在 Egg 生态里复用能力、封装团队基建。
| 机制 | 目录 | 用途 | 粒度 |
|---|---|---|---|
| 插件 Plugin | 安装到 node_modules / 本地 lib/plugin | 封装可复用能力并暴露给应用 | 组件级 |
| 中间件 Middleware | app/middleware/ | 请求阶段拦截(洋葱模型) | 请求级 |
| 扩展 Extend | app/extend/ | 给 ctx/helper/application 挂方法 | 对象级 |
插件内部通常包含自己的中间件、扩展、配置与 Service;中间件和扩展也可独立存在于应用/插件层。
Egg 中中间件是基于 Koa 洋葱模型包一层工厂函数:
// app/middleware/requestlog.js
module.exports = (options, app) => {
return async function requestLog(ctx, next) {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.logger.info(`${ctx.method} ${ctx.url} - ${ms}ms`);
};
};启用(在中间件或配置中加载):
// config/config.default.js
exports.middleware = ["requestlog"]; // 全局启用
// 传配置
exports.requestlog = { enabled: true };单路由/单 Controller 级使用:
// app/router.js
module.exports = (app) => {
const { router, controller, middleware } = app;
const requestLog = middleware.requestlog({ verbose: true });
router.get("/detail", requestLog, controller.detail.index);
};多个中间件按加载顺序「入」,响应阶段按逆序「出」,与 Koa 一致。注意显式依赖:
// 依赖其他中间件
exports.middleware = [
"compress",
"bodyparser", // 依赖在 bodyparser 之后
"auth",
];// app/middleware/auth.js
module.exports = (options) => async (ctx, next) => {
const token = ctx.get("Authorization");
if (!token) ctx.throw(401, "未登录");
ctx.state.user = ctx.service.token.verify(token);
await next();
};插件把一组完整能力封装成可复用的 Egg 模块。骨架如下:
egg-x (插件根目录)
├── app/
│ ├── extend/
│ │ └── application.js # 挂到 app.* 的方法/属性
│ ├── middleware/ # 插件自带中间件
│ └── service/ # 插件自带 Service
├── config/
│ ├── config.default.js # 默认配置
│ └── config.unittest.js # 环境覆盖
├── lib/ # 底层实现
│ └── client.js
└── index.js # 插件入口// index.js
module.exports = (app) => {
app.addSingleton("redis", createClient); // 通过 addSingleton 提供连接
};app.addSingleton(name, getInstance) 会给 app 挂载 app.${name},并根据 config.${name}.client / clients 自动建连。
// config/plugin.js
exports.redis = {
enable: true,
package: "egg-redis",
client: { host: "127.0.0.1", port: 6379 },
};
// 使用
await app.redis.get("key");// config/plugin.js
exports.redis = {
enable: true,
package: "egg-redis",
dependencies: ["session"], // 依赖别的插件先加载
env: ["prod"], // 仅生产环境
};enable:是否启用package / path:来源(npm 包或本地目录)dependencies:依赖的其它插件env:命名环境(prod/test/unittest)才加载// app/extend/application.js
const SESSION_KEY = Symbol("app#session");
module.exports = {
get session() {
if (!this[SESSION_KEY]) this[SESSION_KEY] = { views: 0 };
return this[SESSION_KEY];
},
execShell(cmd) {
// 业务扩展方法
},
};// app/extend/context.js
module.exports = {
// 统一响应
ok(data = null, msg = "ok") {
this.body = { code: 0, msg, data };
this.status = 200;
},
fail(msg = "失败", code = 1) {
this.body = { code, msg };
this.status = 400;
},
};Controller 中即可 ctx.ok(user) / ctx.fail("密码错误")。
可在 app/extend/request.js / response.js 继续扩展跟请求/响应相关的便捷能力。
Egg 使用一套统一的 Container 让不同来源(框架/插件/应用)的对象能互相访问:
| 宿主对象 | 挂载点 | 使用方 |
|---|---|---|
| Application | app.x | 全局 / 插件 |
| Context | ctx.x | 单请求内 |
| Request | ctx.request.x | 请求级 |
| Response | ctx.response.x | 响应级 |
| Helper | ctx.helper.x | 通用工具 |
加载顺序概览(省略细节):
框架扩展 → 插件扩展 → 应用扩展
中间件在应用加载前注册,路由在中间件注册后加载以「给应用加一个 svc 配置访问器」为例:
lib/plugin/egg-svc,写 config.default.js + app/extend/application.js;config/plugin.js 里 exports.svc = { enable: true, path: path.join(__dirname, "../lib/plugin/egg-svc") };app.svc 读取;test/xxx.test.js 用 mm.app 做单测。// test
const { app, mock } = require("egg-mock/bootstrap");
it("app.svc 可读配置", async () => {
await app.ready();
assert(app.svc && app.svc.baseDir);
});全局放 config.middleware,适合通用横切(日志、鉴权、限流);路由级只为特定接口挂载,降低无关开销。
用 app.addSingleton 把实例化好的客户端挂到 app.x,其它插件即可通过 app.x 访问,并在 plugin.js 用 dependencies 声明依赖顺序。
get?Egg 提供的挂载查询往往涉及无锁重入,用 get 可做懒加载只初始化一次,避免多进程/多请求重复实例化。