桌面客户端插件可扩展架构
插件式可扩展架构是一种设计应用程序的方法,可以在运行时动态加载和卸载插件。该架构的目的是使应用程序更加灵活和可扩展。使用插件式可扩展架构,应用程序的各个部分可以独立开发和更新,而不需要对整个应用程序进行修改或重新编译。
架构设计优势
对于我们团队开发的桌面应用来说,它有如下优势:
- 减少安装及启动时间,动态扩展功能
利用插件管理器在运行时动态加载和扩展的机制,让程序的功能在不修改原有代码的情况下进行扩展。
(首次安装插件会耗费些时间,之后启动都较快) - 开发更加灵活和高效
开发人员可以专注于插件的具体功能,而不必担心影响应用程序的总体结构。
(主程序和插件逻辑不会深度耦合,开发更专注,减少沟通成本) - 升级和维护更加容易和快速
开发人员只需要发布新插件即可,不必重新编译或修改应用程序的主体部分。
(更重要的是,用户不需要频繁安装客户端) - 降低应用程序的复杂性,便于移植
开发人员可以根据需要选择和使用插件,而不必将所有功能都集成到应用程序中。
(融合出版和幼师云客户端,我们只需要实现相同插件管理器,就可以集成所有插件,便于功能移植)
架构基本组成
可以分为三个主要部分:应用程序、插件和插件管理器。如下图所示:
- 应用程序:应用程序是该架构的主体。它提供一个框架,使得插件可以被加载、管理和运行。
- 插件:插件是应用程序的一部分,可以在运行时被加载和卸载。每个插件可以实现一些特定的功能或功能组合。插件可以被其他插件调用,可以使用框架提供的API进行通信和交互。
- 插件管理器:插件管理器是用于管理插件的一种组件。它负责加载和卸载插件,管理插件之间的依赖关系以及提供插件之间通信的接口。
架构设计的几大要素
当开始设计插件架构时,我们所要思考的问题往往离不开以下几点。整个设计过程其实就是为每一点选择合适的方案,最后形成一套插件体系。这几点分别是:
- 如何注入、配置、初始化插件
- 插件如何为主程序提供扩展能力
- 主程序赋予的能力与插件输入输出的含义
- 多个插件之间的关系
注入、配置、初始化插件
注入本质上是让程序感知到插件的存在。注入的方式一般可以分为 声明式 和 编程式。两者可以单独也可以结合使用。
声明式就是通过配置信息,告诉程序应该去哪里去取什么插件,程序运行时无需关心插件是如何实现,按照约定与配置去加载对应的插件。
编程式的就是程序提供某种注册 API,开发者通过将插件传入 API 中来完成注册。
初始化的时机分为三种,按实际需求去选择:注入即初始化、统一初始化、运行时初始化(有的插件随时安装随时使用,有的需要重启服务,在注入时即初始化)
插件如何为程序提供扩展能力
这个过程的前提是插件与程序间建立一份契约,约定好对接的方式。这份契约可以包含文件结构、配置格式、API接口。
以VSCode插件为例:
- 文件结构:沿用了 NPM 的传统,约定了目录下 package.json 承载元信息。
- 配置格式:约定了 main 的配置路径作为代码入口,activationEvents定义激活方式,私有字段 contributes 声明命令与配置(该字段可配置菜单和视图容器)
- API 接口:约定了扩展必须提供 activate 和 deactivate 两个接口。并提供了 vscode 下各项 API 来完成注册。
下面以两个开源项目插件配置为例,简单介绍插件的配置格式
VSCode 插件配置格式
我们只需要关心package.json和extension.ts,结构如下:
package.json
1 |
|
有两个关键字,engines指VSCode兼容版本,activationEvents表示触发事件。onLanguage为语言为java时,输入命令或者工作区中包含pom.xml文件,这些都会加载插件。插件的加载机制是懒加载,只有触发了指定事件才会加载。
extension.ts导出一个activate函数,表示当插件被激活时执行函数内的内容。弹出一个Hello World的提示框。
Rubick 取色器、系统剪贴板插件配置格式
Rubick是一款提升工作效率的开源工具,它的特色就是集成了各种工作场景下的插件,例如取色器、系统剪贴板、文件拖拽中转站等等。插件的配置格式如下:
左边是UI插件,右边是系统插件
1 |
|
1 |
|
主程序提供的能力与插件的输入输出
插件与主程序间最重要的契约就是 API 接口,这涉及了可以使用哪些 API,以及这些 API 的输入输出是什么。
同时,不管是主程序对插件的调用还是插件调用主程序的能力,我们都是需要一个确定的输入输出信息。
主程序提供的能力
主程序提供给插件使用的公共工具,或者可以通过一些方式获取或影响主程序本身的状态。能力的注入我们常使用的方式是参数(环境变量)、上下文对象(global、mainWindow)或者工厂函数闭包。
提供的能力类型主要有下面四种:
- 纯工具:不影响主程序状态
- 获取当前主程序状态
- 修改当前主程序状态
- API 形式注入功能:例如注册 UI,注册事件等
对于需要提供哪些能力,一般的建议是根据插件需要完成的工作,提供最小够用范围内的能力,尽量减少插件破坏主程序的可能性。
输入输出
输入输出的结构需要与插件的职责强关联,保证易读性及防止过度膨胀
输入 { inPath: 原始图片 } —插件处理中—> 输出 { outPath: 转码后图片 }
如果插件输入输出过于复杂,可能要反思一下抽象是否过于粗粒度。
多个插件之间的关系
通常是一个扩展点对应一个插件,例如Rubick。
复杂程序,在同一个扩展点上注入的插件,例如VSCode。
并且有多种组合方式。常见的形式如下:
管道式
输入输出相互衔接,一般输入输出是同一个数据类型。
覆盖式
只执行最新注册的逻辑,跳过原始逻辑
洋葱圈式
在管道式的基础上,如果主程序核心逻辑处于中间,插件同时关注进与出的逻辑,则可以使用洋葱圈模型。
这里也可以参考 koa 中的中间件调度模式 https://github.com/koajs/compos
集散式
集散式就是每一个插件都会执行,如果有输出则最终将结果进行合并。这里的前提是存在方案,可以对执行结果进行 merge。
我们只需要做简单了解,毕竟我们不是做复杂系统和IDE。本次分享只关注一个扩展点对应一个插件。
有了以上理论基础,在我们的脑海中一定已经有了一个插件架构的雏形。剩下的可能是结合具体问题,再通过一些设计模式去优化开发和使用体验。
融合出版客户端插件架构设计
插件模式的设计,可以简单也可以复杂,我们不能指望一套插件模式适合所有的场景,在设计之前我们一定要先定义清楚问题。具体选择什么方式实现,一定是根据具体解决的问题权衡得出的。
回顾一下架构组成和四个要素,就可以开始进行设计
架构组成:应用程序、插件管理器、插件
四个要素:
- 如何注入、配置、初始化插件
- 插件如何为主程序提供扩展能力
- 主程序赋予的能力与插件输入输出的含义
- 多个插件之间的关系
主程序启动流程
插件管理器
VSCode和Rubick的插件管理器是带UI界面的插件中心。插件的安装、卸载、更新等操作都在插件中心进行。融合出版目前无UI管理界面,从CMS拉取插件清单。
在CMS中动态配置插件列表,客户端启动时统一下载、更新插件。
当然对于使用场景少、体积较大的插件,会采用不同策略,在运行时去下载并初始化。
插件配置、注入、初始化
配置
约定插件配置
1 |
|
注入
1 |
|
初始化
运行时初始化(部分插件运行时下载再初始化)
调用插件命令 plugin.courseware-packer.createView
1 |
|
主程序提供能力
要使用主程序提供能力,首先约定插件包文件结构
文件结构
electron-main.js 脚本入口、node_modules 脚本依赖、package.json 脚本包信息、index.html 页面入口、assets 页面资源
…
API 接口
供插件脚本使用:参数、上下文对象或者工厂函数闭包。
1 |
|
供插件UI渲染进程使用:
preload.js中IPC通信方法
- 常规通信方法:窗口放大、缩小、弹窗、下载插件、Loading等
- 插件专用通信方法:插件初始化时注册的方法
目前已有的插件
有三种类型的插件:plugin插件、bin插件、shell插件
bin只是扩展使用二进制文件,主逻辑还是在主程序中。
shell插件是一个资源包,供课件打包合并使用。
plugin插件就是按照上诉结构进行设计。
插件 | 插件名称 | 类型 | 依赖项 | 备注 |
---|---|---|---|---|
coreutils | 计算hash | bin | 无 | 可使用npm-yt-ouid替代(评估中) |
ImageMagick | 图片转码 | bin | 无 | |
ffprobe | 多媒体分析 | bin | 无 | ffmpeg命令替代(评估中) |
ffmpeg | 音视频编解码 | bin | 无 | |
object-storage | 文件存储 | plugin | ImageMagick、ffprobe、ffmpeg | |
game-player | 游戏播放 | plugin | 无 | |
game-editor | 游戏编辑 | plugin | game-player | |
main-html | 融合出版内容 | plugin | all | |
courseware-packer | 课件打包 | plugin | electron-shell | |
electron-shell | 壳程序 | shell | 无 |
插件间的关系及输入输出
大部分是一个扩展点对应一个插件,例如游戏编辑器、游戏播放器等
少量是一个扩展点上对应多个插件,类似管道式,输入输出相互衔接,一般输入输出是同一个数据类型。
以 object-storage 上传插件为例:
选择视频地址 —(ffmpeg插件)—> 转码后地址 —(object-storage插件)—> 上传后远端地址
选择图片地址 —(ImageMagick插件)—> 转码后地址 —(object-storage插件)—> 上传后远端地址
如上,一个初级版本的插件式可扩展架构就已经设计完了。
总结及思考
插件式可扩展架构,把扩展功能从应用程序中剥离出来,降低了应用程序的复杂度,让应用程序功能更容易实现。扩展功能与应用程序以一种很松的方式耦合,两者在保持接口不变的情况下,可以独立变化和发布。基于插件架构设计并不复杂,相反它比之前幼师云客户端的设计更简单,更容易理解。
当然,融合出版客户端作为一个初级版本的插件架构,目前还存在一些已知的问题:
- 目前插件配置项过于简单,并没有考虑插件和程序版本关系、插件间的依赖关系、配置入口页面等。
- 部分二进制插件(音视图转码插件)逻辑还写在主程序中,后期会规范统一,移植到插件中。
- 插件管理器过于简单,后续会考虑增加带UI的安装、卸载、更新等操作。
- 部分插件未实现异常捕捉,可能影响主程序稳定。同时也会通过API控制插件影响范围,可以考虑为插件创造沙箱环境,比如插件内可能会调用 global 的接口等
…
除此之外,后续也会根据实际业务需求去进行优化,向着VSCode、NTQQ等业界优秀产品看齐。