融云 IM 在 Electron 平台上的设计实践
Electron 凭借其相对更低的研发成本投入、强大的跨平台支持、拥有基数庞大的 Javascript 开发者受众等优势,在 PC 端桌面应用软件研发领域异军突起。
本文旨在分享融云 IM 在 Electron 平台上的桌面端 SDK 产品开发实践经验。关注【融云全球互联网通信云】了解更多
融云 IM 的 Electron 桌面解决方案目标
1. 提供与传统桌面通讯软件相匹配的能力支持
相较于 B/S 架构的 Web 页面应用,融云期望能够在 Electron 环境下向开发者提供更为丰富的本地化能力,以及比 Websocket(or Comet)更高效的双工通信通道。
借助这些在浏览器环境下不便实现的技术能力,来整体提高用户对于桌面端产品的使用体验,将 Electron 作为一个 C/S 架构软件运行平台的潜力发挥到最大。
2. Browser 与 Electron 不同运行时代码高度复用
由于 Electron 与标准 Web 应用拥有几乎相同的技术生态,因此多数产品会要求前端代码工程兼顾 Browser 与 Electron,也就是说,一套代码既要打包为传统桌面端应用,又可发布为浏览器中运行的 Web 应用。
基于此,作为 PaaS 能力提供的融云 IM SDK 需要在两种不同的 Runtime 下做到差异最小化,避免开发者编写冗余的平台兼容代码。
3. 便于开发者构建多窗口、多进程复杂桌面端应用
Electron 通过对 IPC 能力的封装为桌面端应用开发提供了较完善的跨进程通讯方案,借助此能力,开发者构建的桌面端应用也逐渐趋于复杂。
比较典型的如桌面端通讯产品,通常用一个独立窗口做基础的 IM 聊天业务,一个窗口做历史聊天记录查询业务;当有音视频会议业务场景时,还需要再开一个窗口做会议业务;甚至有开发者提出了与每个聊天对象都保持一个独立聊天窗口的需求(产品形态如 QQ)。
在这种场景下,长连接状态维持、消息同步变得异常复杂,原因在于:
若每个进程窗口都维持独立长连接,难免会出现某一进程连接与其他进程连接状态不同步;且开发者需在各进程同时维护连接状态,复杂度较高;同时还会造成服务的并发能力下降。
若仅有单一主窗口进行连接维持,其他窗口通过 IPC 能力将主窗口作为连接代理,则需要在主进程、各渲染进程中维护复杂的跨进程通讯业务代码,从而推高项目整体的复杂度。
目前的 Electron 开发者绝大多数来自于 Web 开发者,既有编程思维是建立在浏览器页面内单进程单线程的应用模型下构建起来的,对于处理此类多进程模型的产品开发缺乏相关的经验积累。
为降低类似场景的业务实现复杂度,融云需要在 PaaS 能力层面上解决多进程连接共享、多进程消息同步问题,让开发者在既有编程思维模式下将每个业务实现的更为顺畅。
4. 需同步适配融云 IM SDK 多个版本
融云的既有 Web IM SDK 存在多个不同版本,各版本都有不同数量的客户积累,且各版本 API 接口设计迥异,跨版本升级成本较高。
考虑到使用不同版本的客户未来将业务向 Electron 迁移的可能性,我们期望通过架构设计的改进来避免既有客户做过多的集成代码修改,在确保既有客户不因版本升级而流失的前提下降低 Web 研发团队自身的多版本 SDK 维护成本。
目标落地推进方案
1. 剥离各版本的共同业务与对外差异性 API 定义
融云的 IM SDK 各版本分别为不同的代码仓库独立维护,互无干系,这导致所有的功能(包括即将开发的 Electron 桌面解决方案)都可能要在各个版本仓库上单独实现,不仅开发成本高,还会导致实现质量无法保证、或代码实现不统一,同时也推高了产研后续流程的测试、上线等环节的成本。
(IM SDK 不同版本独立维护)
基于目标 4 的要求,在既有现状下继续开发,意味着需要在两个版本的基础上做不同实现,既不符合程序员的代码审美,也影响团队整体的研发效率。
为更好地达成目标 4,团队决定优先通过重构将既有业务分层,各个版本所必须的业务代码抽象下沉为 IM Engine 包,并为各个版本 IM SDK 分别实现不同的 APILayer 以便与既有线上版本接口对齐,这样既可以降低团队的研发成本,也可以满足既有线上客户后续的升级需求。
(重构代码,业务分层)
完成分层后,对于 IM SDK 有依赖的其他产品如 RTC SDK,也都可以摆脱对 IM SDK 接口的依赖而直接调用 Engine 层接口,业务层在拓展 RTC 业务时,也就无需再考虑 IM SDK 的版本问题。
(保证拓展性)
做分层的另一个考虑还为了达成目标 2,将与业务层的交互限制在 API 层,在 Engine 中处理 Electron 与 Browser 两种运行时下的代码差异,业务层只需关心 IM SDK 的接口调用而无需关心底层差异,确保业务层在两种运行时下只需要维护极少甚至无需维护兼容代码,便于业务层更专注于业务开发。
2. Electron 与 Browser 平台下 IM SDK 的区分
在将 Engine 与业务层隔离后,需要考虑 Engine 在不同的运行时下的关键能力差异,并依据能力差异落实 Engine 的底层设计。
Electron 环境下的连接、消息存储等能力由 c++ 模块编写提供,即后面提到的 CppProto.node
在 Browser 与 Electron 平台下,从连接管理、到消息收发等实现方式迥异,团队需要对 Engine 包继续分层,通过 AEngine 抽象类来定义 IM Engine 的能力接口,并抽象 APIContext 类用来管理 AEngine 的能力调用。
考虑到纯 Web 应用构建尺寸问题,Electron 的能力实现代码不应被打包到标准 Web 页面内,因此还需要将 Electron 平台下的实现代码单独抽离出来作为一个独立包(ElectronSolution),作为可选模块由开发者选择安装使用。
(代码抽离为可选模块)
如上图所示,CppEngine 在 ElectronSolution 包中定义,其需要由开发者在 Electron 应用创建 BrowserWindow 实例时通过 webPreferences.preload 配置属性向渲染进程窗口预挂载。
APIContext 在初始化 AEngine 实例时,优先检测 CppEngine 是否已定义。当发现有 CppEngine 定义时,则初始化 CppEngine 提供更丰富的本地化能力,否则初始化 JSEngine。
3. 解决多进程消息同步、多进程连接共享问题
ElectronSolution 包截止目前的设计中,所有代码都运行在渲染进程内,这意味着每个进程彼此独立,都在维护独立的进程状态,无法满足目标 3 中多进程状态同步、连接共享的需求。
为了解决该问题,需要将 CppProto.node 模块放到主进程,在主进程中实现连接管理、消息收发等能力,多个渲染进程通过 IPC 通信共享主进程状态。
(多个渲染进程通过 IPC 通信共享主进程状态)
为了达成目标 3 的要求,ElectronSolution 需要拆分为两个子包:Main 与 Renderer
Main 包运行在主进程内,负责维持 CppProto.node 模块的调用,实现底层连接管理、消息管理等功能,同时通过 Electron 提供的 ipcMain 与各渲染进程维持通信
Renderer 包中定义 CppEngine 类,继承自 Engine 包内的 AEngine 抽象类,依然通过 webPreferences.preload 用来作为主进程的代理,通过 ipcRenderer 与主进程维持通信
(Main 与 Renderer 两个子包)
修改完成后,ElectronSolution 包的整体结构基本确定,以下列出 ElectronSolution 包关键目录结构供参考。
基于上述架构变动,当业务层需要在多个渲染进程中实现 IM 能力时,仅需要关注在各个进程中的 IM SDK 接口调用,由 ElectronSolution 处理多进程之间的状态同步问题。
当开发者期望由既有 Web 业务向 Electron 平台迁移时,开发者也无需修改既有的 Web 业务代码,仅需要增量编写主进程代码相关功能实现,将 ElectronSolution 安装并集成到 Electron 桌面端应用中即可。
整体结构展示
融云 Electron 平台的未来规划
融云作为安全、可靠的全球互联网通信云服务商,除了在 IM 相关业务的持续深耕,后续还会在该平台下发力 RTC 场景能力。
目前 Electron 平台下由 Chromium 原始提供的 WebRTC 能力对于开发桌面级音视频应用软件来说相对薄弱,融云将探索如何借助 node.js 的拓展能力,提供更为底层的 WebRTC 能力拓展如音效、音质、视频特效等。
除了更深入的能力拓展,融云还将提供一个全方面的桌面级应用开发框架,为开发者提供更为全面的 Electron 能力封装,规范化开发者的代码组织、脚本构建过程,以便于开发者在处理复杂的大型桌面应用时,更专注于业务本身。
同时,融云一直坚持对技术的持续性投入,不断夯实底层技术,强化基石力量。对于 Electron 之外的前端技术不断探索,同时坚持自我迭代,坚持技术卓越,以期为广大开发者带来更多实用且易用的产品。
评论