中间件是 Koa 用户拓展、定制 Web 框架的机制,egg.js 还有一个独特的插件机制同样用于拓展框架,插件在前面示例中已经使用过 egg-mysql、egg-sequelize 都是插件
为什么需要插件
既然中间件已经可以解决框架的拓展了,为什么还需要插件
- 中间件的定位是拦截用户请求,并在它前后做一些事情,例如:鉴权、安全检查、访问日志等等。但实际情况是,有些功能是和请求无关的,例如:定时任务、消息订阅、后台逻辑等等
- 有些功能包含非常复杂的初始化逻辑,需要在应用启动的时候完成,这显然也不适合放到中间件中去实现
- 中间件加载其实是有先后顺序的,中间件自身无法管理这种顺序,只能交给使用者,但错误的使用顺序会导致功能大相径庭
egg.js 使用插件来管理、编排相对独立的业务逻辑,一个插件其实就是一个『迷你的应用』,是正常 App 的精简版
- 包含 Service、中间件、配置、框架扩展等
- 没有独立的 router 和 controller
- 没有
plugin.js
,只能声明跟其他插件的依赖,而不能决定其他插件的开启与否
插件初始化
脚手架
直接使用 egg-boilerplate-plugin 脚手架来快速上手
$ mkdir egg-plugin-demo && cd egg-plugin-demo
$ npm init egg --type=plugin
目录结构
. egg-plugin-demo
├── package.json
├── app.js (可选)
├── agent.js (可选)
├── app
│ ├── extend (可选)
│ | ├── helper.js (可选)
│ | ├── request.js (可选)
│ | ├── response.js (可选)
│ | ├── context.js (可选)
│ | ├── application.js (可选)
│ | └── agent.js (可选)
│ ├── service (可选)
│ └── middleware (可选)
│ └── mw.js
├── config
| ├── config.default.js
│ ├── config.prod.js (可选)
| ├── config.test.js (可选)
| ├── config.local.js (可选)
| └── config.unittest.js (可选)
└── test
└── plugin-demo.test.js
插件目录结构和应用几乎一样,主要有几个区别
- 插件没有独立的 controller 和 router,这样可以让插件更加通用,同时避免路由冲突
- 插件没有 plugin.js,形成插件的层层依赖就不好玩了
插件需要在 package.json 中
eggPlugin
节点指定插件信息- name:插件名称,配置依赖关系时会指定依赖插件的 name
- env:只有在指定运行环境才能开启
- dependencies:当前插件强依赖的插件列表,只用于声明依赖关系,不引入插件或开启插件
- optionalDependencies:当前插件的可选依赖插件列表
{
"name": "egg-rpc",
"eggPlugin": {
"name": "rpc",
"dependencies": [ "registry" ],
"optionalDependencies": [ "vip" ],
"env": [ "local", "test", "unittest", "prod" ]
}
}
插件寻址
在使用 egg-mysql 插件时,应用用用在 /config/plugin.js 对其进行了配置
当配置了 package 的时候会按照以下顺序查找mysql: {
enable: false,
package: 'egg-mysql',
},
应用根目录下的
node_modules
- 应用依赖框架路径下的
node_modules
- 当前路径下的
node_modules
也可以通过 path
字段配置插件的绝对路径
mysql: {
enable: false,
path: '/dev/egg-mysql',
},
插件实战
扩展内置对象的接口
egg.js 的内置对象包含了一些常用的方法,在插件相应的文件内可以对框架内置对象进行扩展
app/extend/request.js
- 扩展 Koa#Request 类app/extend/response.js
- 扩展 Koa#Response 类app/extend/context.js
- 扩展 Koa#Context 类app/extend/helper.js
- 扩展 Helper 类app/extend/application.js
- 扩展 Application 类app/extend/agent.js
- 扩展 Agent 类
egg-plugin-demo/app/extend/helper.js
const assert = require('assert');
function isObject(obj) {
const objType = Object.prototype.toString.call(obj);
return objType === '[object Object]' || objType === '[object Array]' || objType === '[object Null]';
}
class ResultDto {
constructor(result, code = 200, errorMsg = '', errorStack = null) {
assert(isObject(result), '[ResultDto:constructor]: arg[0] must be an object or null!');
this.result = result;
this.success = code === 200;
this.code = code;
if (code !== 200) {
this.errorMsg = errorMsg;
this.errorStack = errorStack;
}
}
}
module.exports = {
ResultDto,
};
在插件中拓展了 helper,添加了可以统一接口返回结果的 DTO 对象,在 egg-demo 应用中配置插件使用
egg-demo/config/plugin.js(egg-demo 与 egg-plugin-demo 在同级目录)
demoPlugin: {
enable: true,
path: path.join(__dirname, '../../egg-plugin-demo'), // path 模块需要引用
},
配置完成后就可以在 controller 里面使用了
egg-demo/app/controller/test.js
'use strict';
const Controller = require('egg').Controller;
class Test extends Controller {
async index() {
this.ctx.body = new this.ctx.helper.ResultDto({
text: 'ok',
});
}
}
module.exports = Test;
一般 helper 用于支持 view 渲染,上文中的例子用自定义的 utils 目录更合适
插入自定义中间件
首先自定义一个中间件
egg-plugin-demo/app/middleware/cost.js
module.exports = options => {
const header = options.header || 'X-Response-Time';
return async function cost(ctx, next) {
const now = Date.now();
await next();
ctx.set(header, `${Date.now() - now}ms`);
};
};
然后在插间中配置中间件
egg-plugin-demo/app.js
module.exports = app => {
// 把 cost 中间件放到 bodyParser 之前
const index = app.config.coreMiddleware.indexOf('bodyParser');
app.config.coreMiddleware.splice(index, 0, 'cost');
};
引用开启插件后 cost 中间件被启用,仍然可以在应用配置文件中修改 cost 中间件配置
egg—demo/config/config.default.js
config.cost = {
enable: true,
ignore: /^\/api/,
header: 'egg-cost',
}
应用启动初始化任务
egg-plugin-demo/app.js
const fs = require('fs');
const path = require('path');
module.exports = app => {
// 把 cost 中间件放到 bodyParser 之前
const index = app.config.coreMiddleware.indexOf('bodyParser');
app.config.coreMiddleware.splice(index, 0, 'cost');
// 启动时候读取 package.json
app.customData = fs.readFileSync(path.join(app.config.baseDir, 'package.json'));
app.coreLogger.info('read data ok');
};
最后
本文简单介绍了 egg.js 插件的开发 & 使用方式,通过几个示例展示了插件的作用:egg.js 使用插件来管理、编排相对独立的业务逻辑
示例代码:
https://github.com/Samaritan89/egg-demo/tree/v5
https://github.com/Samaritan89/egg-plugin-demo
更多细节可以参考 egg.js 官方文档