头图
本文由融云技术团队分享,有修订和改动。

1、引言

Electron 凭借其相对更低的研发成本投入、强大的跨平台支持、拥有基数庞大的 Javascript 开发者受众等优势,在 PC 端跨平台桌面开发领域异军突起,大受欢迎。
本文分享的是融云基于Electron的IM跨平台PC端SDK改造过程中所总结的一些实践经验,希望对你有用。
图片

  • 友情提示:如果您对Electron的基础概念还不太了解,建议您先从本系列文章的首篇《快速了解新一代跨平台桌面技术——Electron》和第2篇《Electron初体验(快速开始、跨进程通信、打包、踩坑等)》开始阅读,否则可能难以理解本文的有关内容。
    学习交流:

    -移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》
    -开源IM框架源码:https://github.com/JackJiang2...(备用地址点此)

(本文已同步发布于:http://www.52im.net/thread-40...

2、系列文章

本文是系列文章中的第5篇,本系列总目录如下:
《IM跨平台技术学习(一):快速了解新一代跨平台桌面技术——Electron》
《IM跨平台技术学习(二):Electron初体验(快速开始、跨进程通信、打包、踩坑等)》
《IM跨平台技术学习(三):vivo的Electron技术栈选型、全方位实践总结》
《IM跨平台技术学习(四):蘑菇街基于Electron开发IM客户端的技术实践》
《IM跨平台技术学习(五):融云基于Electron的IM跨平台SDK改造实践总结》(* 本文)
《IM跨平台技术学习(六):网易云信基于Electron的IM消息全文检索技术实践》(稍后发布.. )

3、本次改造的技术目标

针对本次改造,我们需要达到以下4个技术目标:
1)需提供与传统桌面通讯软件相匹配的能力支持;
2)需实现浏览器与Electron不同运行时代码的高度复用;
3)便于开发者构建多窗口、多进程的复杂桌面端应用;
4)需同步适配同一IM端SDK的多个版本。
以下,我们将逐条讨论这4个目标所有实现的具体技术内容。

4、技术目标1:需提供与传统桌面通讯软件相匹配的能力支持

相较于 B/S 架构的 Web 网页应用,我们期望能够在 Electron 环境下向开发者提供更为丰富的本地化能力,以及比 Websocket(或Comet)更高效的Socket实时双工通信通道。
图片
借助这些原本在浏览器环境下不便实现的技术能力,来整体提高用户对于桌面端产品的使用体验,将 Electron 作为一个 C/S 架构软件运行平台的潜力发挥到最大。(白话就是,我们希望借助Electron这个框架,将原本Web端的一些鸡肋能力,做到像原生富客户端一样)

5、技术目标2:浏览器与Electron不同运行时代码的高度复用

由于 Electron 与标准 Web 应用拥有几乎相同的技术生态,因此多数产品会要求前端代码工程兼顾浏览器与 Electron。也就是说,一套代码既要打包为传统桌面端应用(利用Electron),又可发布为浏览器中运行的 Web 网页应用。基于此,我们提供的 IM SDK 需要在两种不同的运行时环境下做到差异最小化,避免开发者编写冗余的平台兼容代码。(白话就是,尽可能在基于Electron的桌面端和纯Web网页端之间重用更多的代码,不然又得多撸一个全新的Electron端,这得多费劲)

6、技术目标3:便于开发者构建多窗口、多进程的复杂桌面端应用

Electron 通过对 IPC 能力的封装为桌面端应用开发提供了较完善的跨进程通讯方案,借助此能力,开发者构建的桌面端应用也逐渐趋于复杂。比较典型的如桌面端IM产品:通常用一个独立窗口做基础的 IM 聊天业务,一个窗口做历史聊天记录查询业务。当有音视频会议业务场景时,还需要再开一个窗口做会议业务。甚至有开发者提出了与每个聊天对象都保持一个独立聊天窗口的需求(产品形态如 QQ)。
在这类需求下,长连接状态维持、消息同步变得异常复杂,原因在于以下3个方面。
1)若每个进程窗口都维持独立长连接,难免会出现某一进程连接与其他进程连接状态不同步。且开发者需在各进程同时维护连接状态,复杂度较高。同时还会造成服务的并发能力下降。
2)若仅有单一主窗口进行连接维持,其他窗口通过 IPC 能力将主窗口作为连接代理,则需要在主进程、各渲染进程中维护复杂的跨进程通讯业务代码,从而推高项目整体的复杂度。
3)目前的 Electron 开发者绝大多数来自于 Web 开发者,既有编程思维是建立在浏览器页面内单进程单线程的应用模型下构建起来的,对于处理此类多进程模型的产品开发缺乏相关的经验积累。为降低类似需求场景的业务实现复杂度,我们需要在 PaaS 能力层面上解决多进程连接共享、多进程消息同步问题,让开发者在既有编程思维模式下将每个业务实现的更为顺畅。

7、技术目标4:需同步适配同一IM端SDK的多个版本

我们的既有Web端 IM SDK 存在一个端多个不同版本的情况(主要是为了兼容老用户,旧版本很难一刀切直接扔掉,只能新老版末同时并存)。各版本都有不同数量的客户积累,且各版本 API 接口设计迥异,跨版本升级成本较高。考虑到使用不同版本的客户未来将业务向 Electron 迁移的可能性,我们期望通过架构设计的改进来避免既有客户做过多的集成代码修改,在确保既有客户不因版本升级而流失的前提下降低 Web 研发团队自身的多版本 SDK 维护成本。

8、本次改造的落地实践

针对上面章节中确定的技术目标,我们将从以下3个方向着手落地实践:
1)剥离各版本的共同业务与对外差异性 API 定义;
2)Electron 与浏览器平台下 IM SDK 的区分;
3)解决多进程消息同步、多进程连接共享问题。以下,我们将逐条分享这3个方面的具体实践内容。

9、落地实践1:剥离各版本的共同业务与对外差异性API定义

我们的 IM SDK 各版本分别为不同的代码仓库独立维护,互无干系。(白话就是,所有端的IM SDK都是独立开发,从头造轮子)这导致所有的功能(包括即将开发的 Electron 桌面解决方案)都可能要在各个版本仓库上单独实现,不仅开发成本高,还会导致实现质量无法保证、或代码实现不统一,同时也推高了产研后续流程的测试、上线等环节的成本。
图片
▲ IM SDK 不同版本独立维护
基于前述技术目标4的要求,在既有现状下继续开发,就意味着需要在两个版本的基础上做不同实现,既不符合程序员的代码审美,也影响团队整体的研发效率。(白话就是,如果又要从头造轮子实在太难受)为更好地达成技术目标4,团队决定优先通过重构将既有业务分层,即各个版本所必须的业务代码抽象下沉为 IM Engine 包,并为各个版本 IM SDK 分别实现不同的API Layer以便与既有线上版本接口对齐,这样既可以降低团队的研发成本,也可以满足既有线上客户后续的升级需求。
图片
▲ 重构代码实现业务分层
完成业务分层后,对于 IM SDK 有依赖的其他产品如 RTC SDK,也都可以摆脱对 IM SDK 接口的依赖而直接调用 Engine 层接口,业务层在拓展 RTC 业务时,也就无需再考虑 IM SDK 的版本问题。
图片
▲ 业务分层后的结构将保证拓展性
做分层的另一个考虑还为了达成技术目标2,将与业务层的交互限制在 API 层,在 Engine 中处理 Electron 与浏览器两种运行时下的代码差异,业务层只需关心 IM SDK 的接口调用而无需关心底层差异,确保业务层在两种运行时下只需要维护极少甚至无需维护兼容代码,便于业务层更专注于业务开发。

10、落地实践2:Electron 与浏览器平台下 IM SDK 的区分

在将 Engine 与业务层隔离后(见上一节),需要考虑 Engine 在不同的运行时下的关键能力差异,并依据能力差异落实 Engine 的底层设计。
Electron 环境下的连接、消息存储等能力由 c++ 模块编写提供(即后面提到的 CppProto.node):
图片
在浏览器与 Electron 平台下,从连接管理、到消息收发等实现方式迥异,团队需要对 Engine 包继续分层,通过 AEngine 抽象类来定义 IM Engine 的能力接口,并抽象 APIContext 类用来管理 AEngine 的能力调用。
考虑到纯 Web 应用构建尺寸问题,Electron 的能力实现代码不应被打包到标准 Web 页面内,因此还需要将 Electron 平台下的实现代码单独抽离出来作为一个独立包(即ElectronSolution),作为可选模块由开发者选择安装使用。
图片
▲ Electron相关的代码抽离为可选模块如上图所示,CppEngine 在 ElectronSolution 包中定义,其需要由开发者在 Electron 应用创建 BrowserWindow 实例时通过 webPreferences.preload 配置属性向渲染进程窗口预挂载。APIContext 在初始化 AEngine 实例时,优先检测 CppEngine 是否已定义。当发现有 CppEngine 定义时,则初始化 CppEngine 提供更丰富的本地化能力,否则初始化 JSEngine。
就像下面的代码的展现的逻辑:

const engine: AEngine = typeofCppEngine !== 'undefined'  
? newCppEngine()  
: newJSEngine()

11、落地实践3:解决多进程消息同步、多进程连接共享问题

ElectronSolution 包截止目前的设计中,所有代码都运行在渲染进程内。这意味着每个进程彼此独立,都在维护独立的进程状态,无法满足目标 3 中多进程状态同步、连接共享的需求。为了解决该问题,需要将 CppProto.node 模块放到主进程,在主进程中实现连接管理、消息收发等能力,多个渲染进程通过 IPC 通信共享主进程状态。
图片
▲ 多个渲染进程
通过 IPC 通信共享主进程状态为了达成技术目标3的要求,ElectronSolution 需要拆分为两个子包,即Main 与 Renderer。
具体就是:
1)Main 包运行在主进程内,负责维持 CppProto.node 模块的调用,实现底层连接管理、消息管理等功能,同时通过 Electron 提供的 ipcMain 与各渲染进程维持通信;
2)Renderer 包中定义 CppEngine 类,继承自 Engine 包内的 AEngine 抽象类,依然通过 webPreferences.preload 用来作为主进程的代理,通过 ipcRenderer 与主进程维持通信。
图片
▲ 拆分为Main与Renderer两个子包
修改完成后,ElectronSolution 包的整体结构基本确定。
以下列出 ElectronSolution 包关键目录结构供参考:

node_modules/@rongcloud/electron-solution
├── index.js
├── main
│   ├── addon
│   │   ├── binding
│   │   │   └── electron-v{electron-version}-{platform}-{arch}.node
│   │   └── index.js
│   ├── dist
│   │   └── index.js
│   ├── index.js
│   └── package.json
└── renderer
│   ├── dist
│   │   └── index.js
│   ├── index.js
│   └── package.json
└── package.json
基于上述架构变动,当业务层需要在多个渲染进程中实现 IM 能力时,仅需要关注在各个进程中的 IM SDK 接口调用,由 ElectronSolution 处理多进程之间的状态同步问题。当开发者期望由既有 Web 业务向 Electron 平台迁移时,开发者也无需修改既有的 Web 业务代码,仅需要增量编写主进程代码相关功能实现,将 ElectronSolution 安装并集成到 Electron 桌面端应用中即可。
最终,我们形成了以下这样的IM SDK整体结构:
图片

12、未来的规划

除了上述IM相关业务,后续我们还打算在Electron平台下提升RTC的场景能力。目前,Electron 平台下由 Chromium 原始提供的 WebRTC 能力对于开发桌面级音视频应用软件来说相对薄弱,我们有计划探索借助 node.js 的拓展能力,提供更为底层的 WebRTC 能力拓展如音效、音质、视频特效等。

13、参考资料

[1] 快速了解新一代跨平台桌面技术——Electron
[2] Electron初体验(快速开始、跨进程通信、打包、踩坑等)
[3] WebSocket从入门到精通,半小时就够!
[4] Comet技术详解:基于HTTP长连接的Web端实时通信技术
[5] 一套海量在线用户的移动端IM架构设计实践分享(含详细图文)
[6] Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE
[7] 搞懂现代Web端即时通讯技术一文就够:WebSocket、socket.io、SSE
(本文已同步发布于:http://www.52im.net/thread-40...


JackJiang
1.6k 声望808 粉丝

专注即时通讯(IM/推送)技术学习和研究。