一、被发版卡住的需求
2024 年,我们遇到了一个让业务方和客户端团队都很痛苦的场景:工作台中的轻应用(OA 审批、报表、公告等)有频繁的变更需求,但每次变更都必须跟客户端版本一起发布。
App 的发版周期是 2-4 周,iOS 还需要审核。业务方催:「审批流程加一个字段,为什么需要等三周?」客户端也很无奈:「改的是 H5 页面、不是原生代码,但它在 App 的 WebView 容器里,容器改了也得跟版本。」
这个矛盾催生了插件化架构的需求:让业务模块可以独立于 App 版本进行更新。本文复盘我们从零搭建插件化架构的过程,包括插件协议设计、动态加载机制、版本兼容策略和在鸿蒙平台上的适配经验。
二、插件化的核心问题
在动手之前,我们先定义了三个核心问题:
- 隔离性:一个插件的崩溃不能影响宿主 App 和其他插件的稳定性。
- 动态加载:插件代码和资源如何下载、加载、替换,而不需要重新编译 App。
- 版本兼容:宿主 App 和插件分别独立迭代,如何保证接口的向前兼容。
对于 Flutter 项目来说,「动态化」还有一层额外的挑战:Dart 代码在 Release 模式下是 AOT 编译的,没有字节码虚拟机。这意味着我们无法像 React Native 的 CodePush 那样直接下发 JavaScript 代码。只能在 Flutter 已有的机制上寻找方案。
三、整体架构设计
3.1 三层架构
1 | ┌────────────────────────────────────────┐ |
3.2 插件协议(Plugin Protocol)
插件与宿主 App 之间的通信通过标准协议完成:
1 | // 插件必须实现的接口 |
这个设计的核心思路是:插件不直接访问网络、数据库、文件系统,所有系统能力通过 MPluginContext 代理。这保证了宿主 App 对插件的完全控制权——如果某个插件滥用网络请求,宿主可以在 Context 层面限流。
3.3 插件管理器的实现
1 | class MPluginManager { |
四、动态加载的三种方式
在 Flutter 中实现动态化,业界主要有三种方案。我们逐一评估后做了组合使用。
4.1 方案一:Dart 代码下发 — CodePush(不适用)
原理:将 Dart 代码编译为 kernel 文件(.dill),运行时动态加载并执行。类似于 React Native 的 CodePush。
评估结论:不适用于生产环境。原因:
- Flutter 官方明确表示不支持 Release 模式下的动态代码加载。AOT 模式下 Dart VM 不可用,无法执行 dart kernel 文件。
- 即使强行在 Debug 模式下使用,也面临严重的安全风险——下发的代码可以执行任意 Dart 代码,等于开了一个后门。
- iOS 的 App Store 审核明确禁止动态下载可执行代码。
在安全性和合规性要求极高的企业 IM 项目中,这条路走不通。
4.2 方案二:WebView 轻应用(部分适用)
原理:插件以 H5 页面的形式存在,通过 WebView 容器加载。页面更新只需刷新 WebView 缓存。
这是我们最核心的动态化方案,适用于工作台中的 OA 审批、报表、公告等非实时交互型业务。
在企业 IM 中的实践:
1 | class WebViewPlugin extends MPlugin { |
WebView 方案的优势:
- 真正的动态更新:H5 页面部署到 CDN,客户端无需发版。
- Web 团队直接参与:前端开发者不需要学 Flutter 就能开发业务插件。
- 成熟生态:Vue/React/Angular 都可以作为 H5 开发框架。
WebView 方案的局限:
- 性能上限:WebView 的渲染性能无法与原生自建 UI 相比。对于聊天页面、消息列表这种高交互性场景不适用。
- 离线能力弱:没有网络时,缓存策略需要精心设计。
- 原生能力受限:虽然可以通过 JSBridge 调用原生能力,但每次调用都是异步的,复杂交互的体验不如原生。
4.3 方案三:原生混合方案(渐进式)
第三类方案是「不追求纯 Flutter 动态化,而是利用平台原生能力」。在企业 IM 中,我们将部分高交互性但需要动态更新的场景(如自定义表情面板)用原生实现,通过 Platform Channel 嵌入 Flutter 页面。
在鸿蒙平台上,这个方案发挥了意想不到的作用:鸿蒙的系统 WebView 组件与 Android/iOS 有差异,某些 H5 插件在鸿蒙 WebView 中渲染异常。我们改为用鸿蒙原生的 ArkUI 实现这些页面,通过 Flutter Platform Channel 通信,反而获得了更好的体验。
五、版本兼容策略
插件和宿主 App 独立演进,版本兼容是最头疼的问题。
5.1 接口版本化
MPluginContext 中的每个方法都带版本号:
1 | abstract class MPluginContext { |
核心原则:只增加新接口,不修改已有接口的语义,不删除已发布的接口。旧接口标记 deprecated 后至少保留两个宿主大版本。
5.2 最低版本协商
插件可以在配置中声明对宿主 App 的最低版本要求:
1 | { |
插件管理器在加载前检查宿主版本,不满足要求时提示用户升级 App。这个机制避免了「插件用了新 API 但在旧宿主上崩溃」的问题。
六、鸿蒙平台的适配经验
在将插件化架构扩展到鸿蒙平台时,遇到了两个特有的问题:
问题一:鸿蒙的 WebView 组件不支持某些 JSBridge 的注入机制。解决方案是为鸿蒙单独实现了一套基于原生 ArkUI Channel 的通信桥,在 WebView 插件加载时检测平台、注入对应的 JSBridge 代码。
问题二:鸿蒙的文件系统路径规则与 Android 不同。插件包下载后的解压路径需要做平台适配。解决方案是在 Plugin SDK 中抽象 MStorage 接口,各平台提供自己的实现。
七、总结
插件化架构在企业 IM 项目中的核心价值不是「技术炫酷」,而是解耦发布节奏。让审批、报表、公告这种高频变化的轻应用可以按天迭代,而不受客户端两周发版周期的约束。
三个关键设计决策:
- WebView 作为主要动态化手段。不是最优美的方案,但是最可靠、最合规的方案。
- MPluginContext 作为能力代理。插件不能直接访问任何系统能力,全部通过宿主可控的 Context 代理。这是安全性和稳定性的底线。
- 接口只增不改。版本兼容的黄金法则:新增容易,修改和删除需要巨大的代价。在插件架构设计的初期,宁可接口方法多一点,也不要后续破坏兼容性。
插件化不是终点。随着我们对模块化和微前端的探索(后续文章会展开),插件的边界会从「独立业务模块」逐步扩展到「可组合的业务单元」。
扫描二维码,分享此文章