Web应用开发框架-egg(三)06-基础功能——插件之定义插件、编写插件 & 定时任务 & 自定义启动app
插件
插件机制是我们框架的一大特色。它不但可以保证框架核心的足够精简、稳定、高效,还可以促进业务逻辑的复用,生态圈的形成。
- Koa 已经有了中间件的机制,为啥还要插件呢?
- 中间件、插件、应用它们之间是什么关系,有什么区别?
为什么要插件
使用 Koa 中间件过程中发现了下面一些问题:
- 中间件加载其实是有先后顺序的,但是中间件自身却无法管理这种顺序,只能交给使用者。这样其实非常不友好,一旦顺序不对,结果可能有天壤之别。
- 中间件的定位是拦截用户请求,并在它前后做一些事情,例如:鉴权、安全检查、访问日志等等。但实际情况是,有些功能是和请求无关的,例如:定时任务、消息订阅、后台逻辑等等。
- 有些功能包含非常复杂的初始化逻辑,需要在应用启动的时候完成。这显然也不适合放到中间件中去实现。
中间件、插件、应用的关系
一个插件其实就是一个『迷你的应用』,和应用(app)几乎一样:
- 它包含了 Service、中间件、配置、框架扩展等等。
- 它没有独立的 Router 和 Controller。(插件一般不写业务逻辑)
- 它没有 plugin.js,只能声明跟其他插件的依赖,而不能决定其他插件的开启与否。
使用插件
插件一般通过 npm 模块的方式进行复用:
npm i egg-mysql --save
然后需要在应用或框架的 config/plugin.js
中声明:
// config/plugin.js // 使用 mysql 插件 exports.mysql = {
enable: true, package: 'egg-mysql', };
根据环境配置
同时,我们还支持 plugin.{env}.js
这种模式,会根据运行环境加载插件配置。
// config/plugin.local.js exports.dev = {
enable: true, package: 'egg-dev', };
引入
package
是npm
方式引入,也是最常见的引入方式path
是绝对路径引入,如应用内部抽了一个插件,但还没达到开源发布独立npm
的阶段,或者是应用自己覆盖了框架的一些插件
// config/plugin.js const path = require('path'); exports.mysql = {
enable: true, path: path.join(__dirname, '../lib/plugin/egg-mysql'), };
如何写一个插件
你可以直接使用 egg-boilerplate-plugin 脚手架来快速上手。
$ mkdir egg-hello && cd egg-hello $ npm init egg --type=plugin $ npm i
一个插件其实就是一个『迷你的应用』,下面展示的是一个插件的目录结构,和应用(app)几乎一样。
. egg-hello ├── 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 └── middleware └── mw.test.js
- 插件没有独立的 router 和 controller。这主要出于几点考虑:
- 路由一般和应用强绑定的,不具备通用性。
- 一个应用可能依赖很多个插件,如果插件支持路由可能导致路由冲突。
- 如果确实有统一路由的需求,可以考虑在插件里通过中间件来实现。
- 插件需要在
package.json
中的eggPlugin
节点指定插件特有的信息:
{String} name
- 插件名(必须配置),具有唯一性,配置依赖关系时会指定依赖插件的 name。{Array} dependencies
- 当前插件强依赖的插件列表(如果依赖的插件没找到,应用启动失败)。{Array} optionalDependencies
- 当前插件的可选依赖插件列表(如果依赖的插件未开启,只会 warning,不会影响应用启动)。{Array} env
- 只有在指定运行环境才能开启,具体有哪些环境可以参考运行环境。此配置是可选的,一般情况下都不需要配置。
{
"name": "egg-rpc", "eggPlugin": {
"name": "rpc", "dependencies": [ "registry" ], "optionalDependencies": [ "vip" ], "env": [ "local", "test", "unittest", "prod" ] } }
插件能做什么?
- 扩展内置对象的接口
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 类
- 插入自定义中间件
- 在应用启动时做一些初始化工作
- 设置定时任务
定时任务
会有许多场景需要执行一些定时任务,例如:
- 定时上报应用状态。
- 定时从远程接口更新本地缓存。
- 定时进行文件切割、临时文件删除。
框架提供了一套机制来让定时任务的编写和维护更加优雅。
编写定时任务
所有的定时任务都统一存放在 app/schedule
目录下,每一个文件都是一个独立的定时任务,可以配置定时任务的属性和要执行的方法。
const Subscription = require('egg').Subscription; class LogSubscription extends Subscription {
// 通过 schedule 属性来设置定时任务的执行间隔等配置 static get schedule() {
return {
interval: '1s', // 1 分钟间隔 type: 'worker', // 指定所有的 worker 都需要执行 }; } // subscribe 是真正定时任务执行时被运行的函数 async subscribe() {
console.log('我是定时任务') } } module.exports = LogSubscription;
另一种写法:
module.exports = {
schedule: {
interval: '1s', // 1 分钟间隔 type: 'all', // 指定所有的 worker 都需要执行 }, async task(ctx) {
console.log('我是定时任务') }, };
参数
- interval
- 数字类型,单位为毫秒数,例如
5000
。 - 字符类型,会通过 ms 转换成毫秒数,例如
5s
。
- 数字类型,单位为毫秒数,例如
- type
worker
类型:每台机器上只有一个 worker 会执行这个定时任务,每次执行定时任务的 worker 的选择是随机的。all
类型:每台机器上的每个 worker 都会执行这个定时任务。
自定义启动
我们常常需要在应用启动期间进行一些初始化工作,等初始化完成后应用才可以启动成功,并开始对外提供服务。
框架提供了统一的入口文件(app.js
)进行启动过程自定义,这个文件返回一个 Boot 类,我们可以通过定义 Boot 类中的生命周期方法来执行启动应用过程中的初始化工作。
框架提供了这些 生命周期函数供开发人员处理:
- 配置文件即将加载,这是最后动态修改配置的时机(
configWillLoad
) - 配置文件加载完成(
configDidLoad
) - 文件加载完成(
didLoad
) - 插件启动完毕(
willReady
) - worker 准备就绪(
didReady
) - 应用启动完成(
serverDidReady
) - 应用即将关闭(
beforeClose
)
// app.js class AppBootHook {
constructor(app) {
this.app = app; } configWillLoad() {
// 此时 config 文件已经被读取并合并,但是还并未生效 // 这是应用层修改配置的最后时机 // 注意:此函数只支持同步调用 console.log('configWillLoad') // 例如:参数中的密码是加密的,在此处进行解密 // this.app.config.mysql.password = decrypt(this.app.config.mysql.password); // // 例如:插入一个中间件到框架的 coreMiddleware 之间 // const statusIdx = this.app.config.coreMiddleware.indexOf('status'); // this.app.config.coreMiddleware.splice(statusIdx + 1, 0, 'limit'); } async didLoad() {
console.log('didLoad') // 所有的配置已经加载完毕 // 可以用来加载应用自定义的文件,启动自定义的服务 // 例如:创建自定义应用的示例 // this.app.queue = new Queue(this.app.config.queue); // await this.app.queue.init(); // // 例如:加载自定义的目录 // this.app.loader.loadToContext(path.join(__dirname, 'app/tasks'), 'tasks', {
// fieldClass: 'tasksClasses', // }); } async willReady() {
console.log('willReady') // 所有的插件都已启动完毕,但是应用整体还未 ready // 可以做一些数据初始化等操作,这些操作成功才会启动应用 // 例如:从数据库加载数据到内存缓存 // this.app.cacheData = await this.app.model.query(QUERY_CACHE_SQL); } async didReady() {
console.log('didReady') // 应用已经启动完毕 // const ctx = await this.app.createAnonymousContext(); // await ctx.service.Biz.request(); } async serverDidReady() {
console.log('serverDidReady') // http / https server 已启动,开始接受外部请求 // 此时可以从 app.server 拿到 server 的实例 // this.app.server.on('timeout', socket => {
// // handle socket timeout // }); } } module.exports = AppBootHook;
注意:在自定义生命周期函数中不建议做太耗时的操作,框架会有启动的超时检测。
到此这篇Web应用开发框架-egg(三)06-基础功能——插件之定义插件、编写插件 & 定时任务 & 自定义启动app的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权、违法违规、事实不符,请将相关资料发送至xkadmin@xkablog.com进行投诉反馈,一经查实,立即处理!
转载请注明出处,原文链接:https://www.xkablog.com/yd-ios/10736.html