本文由融云技術團隊分享,有修訂和改動。
1、引言
Electron 憑借其相對更低的研發成本投入、強大的跨平臺支持、擁有基數龐大的 Javascript 開發者受眾等優勢,在 PC 端跨平臺桌面開發領域異軍突起,大受歡迎。
本文分享的是融云基于Electron的IM跨平臺PC端SDK改造過程中所總結的一些實踐經驗,希望對你有用。
* 友情提示:如果您對Electron的基礎概念還不太了解,建議您先從本系列文章的首篇《快速了解新一代跨平臺桌面技術——Electron》和第2篇《Electron初體驗(快速開始、跨進程通信、打包、踩坑等)》開始閱讀,否則可能難以理解本文的有關內容。
學習交流:
(本文已同步發布于:http://www.52im.net/thread-4060-1-1.html)
2、系列文章
本文是系列文章中的第5篇,本系列總目錄如下:
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-4060-1-1.html)