介绍
有了中间件为啥还要插件?
- 中间件是定位拦截用户请求, 并在它的前后做一件事情, 例如:鉴权、访问日志、安全检查
- 中间件加载是有先后顺序的, 但是中间件自身缺无法管理这种顺序, 只能交给使用者
- 一些非常复杂的初始化逻辑, 需要在应用启动的时候完成, 如果放在中间件就不合适
中间件、插件、应用的关系
一个插件就是一个迷你版的应用, 包含:
- 包含了:Service、中间件、配置、扩展等
- 没有 Controller 和 Router
- 没有plugin.js, 只能声明跟其他插件的依赖, 不能决定插件是否开启
- 插件本身可以包含中间件
使用
1. plugin.js参数的配置
- {Boolean} enable - 是否开启此插件,默认为 true
- {String} package - npm 模块名称,通过 npm 模块形式引入插件
- {String} path - 插件绝对路径,跟 上一个参数package 配置互斥
- {Array} env - 只有在指定运行环境才能开启,会覆盖插件自身 package.json 中的配置
2. egg内置插件
关闭内置插件
1 2 3 4 5 6 7
| exports.cors = { enable: false; };
exports.cors = false;
|
- package 和 path
- package 是 npm 方式引入,也是最常见的引入方式
- path 是绝对路径引入,如应用内部抽了一个插件,但还没达到开源发布独立 npm 的阶段,或者是应用自己覆盖了框架的一些插件
1 2 3 4 5 6
| const path = require('path'); exports.mysql = { enable: true, path: path.join(__dirname, '../lib/plugin/bg-mysql'), };
|
3. 内置插件列表
- 异常处理 onerror
- session实现 session
- 安全 security
- 日志分割 logrotator
- 定时任务 schedule
- 模板引擎 view
- 文件流式上传 multpart
- 等等…
开发
1. 脚手架快速开发
1 2 3 4
| mkdir egg-test && cd egg-test npm init egg --type=plugin npm i npm test
|
插件使用场景
1. 扩展内置对象接口
- 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 类
2. 插入自定义中间件
1 2 3 4 5 6 7 8 9 10
|
const fs = require('fs'); const path = require('path');
module.exports = app => { app.customData = fs.readFileSync(path.join(app.config.baseDir, 'data.bin'));
app.coreLogger.info('read data ok'); };
|
3. 设置定时任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| { "name": "your-plugin", "eggPlugin": { "name": "your-plugin", "dependencies": [ "schedule" ] } }
exports.schedule = { type: 'worker', cron: '0 0 3 * * *', };
exports.task = async ctx => { };
|
插件开发实践
1. 之前插件存在问题
- 在一个应用中同时使用同一个服务的不同实例(连接到两个不同的 MySQL 数据库)。
- 从其他服务获取配置后动态初始化连接(从配置中心获取到 MySQL 服务地址后再建立连接)。
2. 实现
如果让插件各自实现,可能会出现各种奇怪的配置方式和初始化方式,所以框架提供了 app.addSingleton(name, creator) 方法来统一这一类服务的创建。需要注意的是在使用 app.addSingleton(name, creator) 方法时,配置文件中一定要有 client 或者 clients 为 key 的配置作为传入 creator 函数 的 config。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| module.exports = app => { app.addSingleton('mysql', createMysql); }
async function createMysql(config, app) { const mysqlConfig = await app.configManager.getMysqlConfig(config.mysql); assert(mysqlConfig.host && mysqlConfig.port && mysqlConfig.user && mysqlConfig.database); const client = new Mysql(mysqlConfig);
const rows = await client.query('select now() as currentTime;'); app.coreLogger.info(`[egg-mysql] init instance success, rds currentTime: ${rows[0].currentTime}`);
return client; }
|
3. 应用层面使用案例
单实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| module.exports = { mysql: { client: { host: 'mysql.com', port: '3306', user: 'test_user', password: 'test_password', database: 'test', }, }, };
class PostController extends Controller { async list() { const posts = await this.app.mysql.query(sql, values); }, }
|
多实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| exports.mysql = { clients: { db1: { user: 'user1', password: 'upassword1', database: 'db1', }, db2: { user: 'user2', password: 'upassword2', database: 'db2', }, }, default: { host: 'mysql.com', port: '3306', }, }
class PostController extends Controller { async list() { const posts = await this.app.mysql.get('db1').query(sql, values); }, }
|
动态创建实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| module.exports = app => { app.beforeStart(async () => { const mysqlConfig = await app.configCenter.fetch('mysql'); app.database = await app.mysql.createInstanceAsync(mysqlConfig); }); };
class PostController extends Controller { async list() { const posts = await this.app.database.query(sql, values); }, }
|
插件寻址规则
- 优先级A: 如果配置了 path,直接按照 path 加载
- 优先级B: 没有 path 根据 package 名去查找,查找的顺序依次是:
- 应用根目录下的 node_modules
- 应用依赖框架路径下的 node_modules
- 当前路径下的 node_modules (主要是兼容单元测试场景)
文章来源1
文章来源2