中间件是 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.jsonapp.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 官方文档
