中间件是 Koa 用户拓展、定制 Web 框架的机制,egg.js 还有一个独特的插件机制同样用于拓展框架,插件在前面示例中已经使用过 egg-mysqlegg-sequelize 都是插件

为什么需要插件

既然中间件已经可以解决框架的拓展了,为什么还需要插件

  1. 中间件的定位是拦截用户请求,并在它前后做一些事情,例如:鉴权、安全检查、访问日志等等。但实际情况是,有些功能是和请求无关的,例如:定时任务、消息订阅、后台逻辑等等
  2. 有些功能包含非常复杂的初始化逻辑,需要在应用启动的时候完成,这显然也不适合放到中间件中去实现
  3. 中间件加载其实是有先后顺序的,中间件自身无法管理这种顺序,只能交给使用者,但错误的使用顺序会导致功能大相径庭

egg.js 使用插件来管理、编排相对独立的业务逻辑,一个插件其实就是一个『迷你的应用』,是正常 App 的精简版

  • 包含 Service、中间件、配置、框架扩展等
  • 没有独立的 router 和 controller
  • 没有 plugin.js,只能声明跟其他插件的依赖,而不能决定其他插件的开启与否

插件初始化

脚手架

直接使用 egg-boilerplate-plugin 脚手架来快速上手

  1. $ mkdir egg-plugin-demo && cd egg-plugin-demo
  2. $ npm init egg --type=plugin

目录结构

  1. . egg-plugin-demo
  2. ├── package.json
  3. ├── app.js (可选)
  4. ├── agent.js (可选)
  5. ├── app
  6. ├── extend (可选)
  7. | ├── helper.js (可选)
  8. | ├── request.js (可选)
  9. | ├── response.js (可选)
  10. | ├── context.js (可选)
  11. | ├── application.js (可选)
  12. | └── agent.js (可选)
  13. ├── service (可选)
  14. └── middleware (可选)
  15. └── mw.js
  16. ├── config
  17. | ├── config.default.js
  18. ├── config.prod.js (可选)
  19. | ├── config.test.js (可选)
  20. | ├── config.local.js (可选)
  21. | └── config.unittest.js (可选)
  22. └── test
  23. └── plugin-demo.test.js

插件目录结构和应用几乎一样,主要有几个区别

  1. 插件没有独立的 controller 和 router,这样可以让插件更加通用,同时避免路由冲突
  2. 插件没有 plugin.js,形成插件的层层依赖就不好玩了
  3. 插件需要在 package.json 中 eggPlugin 节点指定插件信息

    • name:插件名称,配置依赖关系时会指定依赖插件的 name
    • env:只有在指定运行环境才能开启
    • dependencies:当前插件强依赖的插件列表,只用于声明依赖关系,不引入插件或开启插件
    • optionalDependencies:当前插件的可选依赖插件列表
      1. {
      2. "name": "egg-rpc",
      3. "eggPlugin": {
      4. "name": "rpc",
      5. "dependencies": [ "registry" ],
      6. "optionalDependencies": [ "vip" ],
      7. "env": [ "local", "test", "unittest", "prod" ]
      8. }
      9. }

      插件寻址

      在使用 egg-mysql 插件时,应用用用在 /config/plugin.js 对其进行了配置
      1. mysql: {
      2. enable: false,
      3. package: 'egg-mysql',
      4. },
      当配置了 package 的时候会按照以下顺序查找
  4. 应用根目录下的 node_modules

  5. 应用依赖框架路径下的 node_modules
  6. 当前路径下的 node_modules

也可以通过 path 字段配置插件的绝对路径

  1. mysql: {
  2. enable: false,
  3. path: '/dev/egg-mysql',
  4. },

插件实战

通过几个简单例子看看插件可以做什么

扩展内置对象的接口

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

  1. const assert = require('assert');
  2. function isObject(obj) {
  3. const objType = Object.prototype.toString.call(obj);
  4. return objType === '[object Object]' || objType === '[object Array]' || objType === '[object Null]';
  5. }
  6. class ResultDto {
  7. constructor(result, code = 200, errorMsg = '', errorStack = null) {
  8. assert(isObject(result), '[ResultDto:constructor]: arg[0] must be an object or null!');
  9. this.result = result;
  10. this.success = code === 200;
  11. this.code = code;
  12. if (code !== 200) {
  13. this.errorMsg = errorMsg;
  14. this.errorStack = errorStack;
  15. }
  16. }
  17. }
  18. module.exports = {
  19. ResultDto,
  20. };

在插件中拓展了 helper,添加了可以统一接口返回结果的 DTO 对象,在 egg-demo 应用中配置插件使用
egg-demo/config/plugin.js(egg-demo 与 egg-plugin-demo 在同级目录

  1. demoPlugin: {
  2. enable: true,
  3. path: path.join(__dirname, '../../egg-plugin-demo'), // path 模块需要引用
  4. },

配置完成后就可以在 controller 里面使用了
egg-demo/app/controller/test.js

  1. 'use strict';
  2. const Controller = require('egg').Controller;
  3. class Test extends Controller {
  4. async index() {
  5. this.ctx.body = new this.ctx.helper.ResultDto({
  6. text: 'ok',
  7. });
  8. }
  9. }
  10. module.exports = Test;

一般 helper 用于支持 view 渲染,上文中的例子用自定义的 utils 目录更合适

插入自定义中间件

首先自定义一个中间件
egg-plugin-demo/app/middleware/cost.js

  1. module.exports = options => {
  2. const header = options.header || 'X-Response-Time';
  3. return async function cost(ctx, next) {
  4. const now = Date.now();
  5. await next();
  6. ctx.set(header, `${Date.now() - now}ms`);
  7. };
  8. };

然后在插间中配置中间件
egg-plugin-demo/app.js

  1. module.exports = app => {
  2. // 把 cost 中间件放到 bodyParser 之前
  3. const index = app.config.coreMiddleware.indexOf('bodyParser');
  4. app.config.coreMiddleware.splice(index, 0, 'cost');
  5. };

引用开启插件后 cost 中间件被启用,仍然可以在应用配置文件中修改 cost 中间件配置
egg—demo/config/config.default.js

  1. config.cost = {
  2. enable: true,
  3. ignore: /^\/api/,
  4. header: 'egg-cost',
  5. }

应用启动初始化任务

egg-plugin-demo/app.js

  1. const fs = require('fs');
  2. const path = require('path');
  3. module.exports = app => {
  4. // 把 cost 中间件放到 bodyParser 之前
  5. const index = app.config.coreMiddleware.indexOf('bodyParser');
  6. app.config.coreMiddleware.splice(index, 0, 'cost');
  7. // 启动时候读取 package.json
  8. app.customData = fs.readFileSync(path.join(app.config.baseDir, 'package.json'));
  9. app.coreLogger.info('read data ok');
  10. };

最后

本文简单介绍了 egg.js 插件的开发 & 使用方式,通过几个示例展示了插件的作用:egg.js 使用插件来管理、编排相对独立的业务逻辑

示例代码:
https://github.com/Samaritan89/egg-demo/tree/v5
https://github.com/Samaritan89/egg-plugin-demo

更多细节可以参考 egg.js 官方文档