如何基于 Electron 開發(fā)跨終端的應用
本文首發(fā)于政采云前端團隊博客:如何基于 Electron 開發(fā)跨終端的應用
https://www.zoo.team/article/the-application-of-electron

自我介紹

首先我們分享的第一塊叫端的延展。不知道大家對這張圖熟不熟悉,前段時間的新聞大家應該都聽到過,硅谷鋼鐵俠艾隆馬斯克發(fā)布了第一款商業(yè)化的載人龍飛船,這張圖片中就是龍飛船的控制臺,知乎上有人對這張圖的評價叫 JS 上天了。為什么說叫 JS 上天了呢?因為有傳言說它是基于 Electron 開發(fā)的,不過這個消息并沒有得到證實。但是可以證實的一點是航天飛船的觸控界面 UI ,確實是基于 Chromium + JavaScript 這樣的架構(gòu)來實現(xiàn)的。這也從某種程度上說明了這種架構(gòu)的一個可用性和穩(wěn)定性的能力。

下面我們一起來回顧一下前端在整個端領域的發(fā)展歷程。在早期,前端工程師的定義可能是基于瀏覽器運行環(huán)境的 Web 開發(fā),但是隨著 09 年 Node.js 的出現(xiàn),讓前端工程師有了脫離瀏覽器運行環(huán)境的開發(fā)能力。我們擁有了可以面向服務端開發(fā)的能力,前端的能力延展到了服務端。

CLI -> GUI

xxx-cli create 這樣的命令去創(chuàng)建一個項目。創(chuàng)建項目完成之后,如果想進行開發(fā),我們需要去運行 npm install ,安裝所需的依賴包,最終將整個項目提交到 Git 倉庫上去。這是我們新項目的創(chuàng)建,基于 CLI 方式的一個操作流程。
GUI 賦予的價值

業(yè)務場景應用

基建場景應用

開發(fā)模式

Electron 架構(gòu)

能力點
我們來介紹一下它的一些核心的能力點。
首先是 Chromium,我們可以把它理解為是一個擁有最新版瀏覽器特性的一個 Chrome 瀏覽器,它帶給我們的好處就是在開發(fā)過程中無需考慮瀏覽器的兼容性,我們可以使用一些 ES6、ES7 最新的語法,可以放心的使用 Flex 布局,以及瀏覽器的最新特性,都可以嘗試,不需要考慮兼容性的問題。
Node.js 則是提供了一個文件讀寫、本地命令調(diào)用、以及第三方擴展的能力,并且基于 Node.js 整個強大的生態(tài),將近幾十萬的 Node.js 模塊都可以在整個客戶端內(nèi)使用。
Native APIs 提供了一個統(tǒng)一的原生界面的能力,還包括一些系統(tǒng)通知、快捷鍵,還可以通過它來獲取一些系統(tǒng)的硬件信息。還提供了桌面客戶端的基礎能力,像更新機制、崩潰報告這樣的能力。

其他桌面端選型對比
Electron 提供這些能力點大大的降低了桌面端開發(fā)的成本,以及上手的門檻。當然開發(fā)桌面端的話,除了 Electron 外,還會有一些其他的選型,我們看一下它跟其他的選型相比較的話有哪些差異點。
開發(fā)桌面端首先可以選擇 Native 開發(fā),但是,在開發(fā)不同的平臺的時候,需要使用不同的語言,但它的優(yōu)點是具有比較好的原生體驗,以及比較好的運行性能,但是它的門檻相對來說還是比較高的。
QT 是一個基于 C++ 的跨平臺桌面端開發(fā)框架,它所使用的語言是使用 C++,整體性能和體驗上來說,跟Native 開發(fā)的話是可以相媲美的,但由于技術棧原因,開發(fā)門檻相對來說也是比較高的。
另外兩個就是 Electron 和 NW.js。這兩個都是使用 Javascript 作為一個開發(fā)語言。相較于 Native 和 QT 來說,它們對前端工程師來說是相當友好的,并且它們兩個有著比較相似的一個架構(gòu),都是基于 Chromium + Node.js 實現(xiàn),同時它們也都有一個跨平臺的支持能力。但兩個的差異點是:Electron 相對來說有一個更好的一個社區(qū)的生態(tài)和社區(qū)的活躍度,我們平時如果遇到了一些問題,在社區(qū)內(nèi)可能會有比較多、比較完善的解決方案,同時它對 issue 的響應速度也是比較快的。

簡單 Electron 應用的結(jié)構(gòu)
main 字段,通過 main 字段來定義應用的一個啟動入口。我們將入口文件定義為 main.js ,在 mian.js 里我們做了哪些事情呢?首先 app 代表著整個應用,監(jiān)聽 app 的狀態(tài),當整個應用達到一個 ready 的狀態(tài)之后,通過 Electron 提供的 BrowserWindow ,去新創(chuàng)建一個瀏覽器窗口。創(chuàng)建瀏覽器窗口之后,去加載 index.html 文件,這樣的話我們就完成了一個最基礎版桌面端應用的實現(xiàn)。基于 Electron 開發(fā)桌面端應用,和平時的開發(fā) web 端應用有哪些不一樣的,我們需要了解的兩個核心概念就是:主進程和渲染進程,以及兩個進程間的通信如何實現(xiàn)。在剛才的示例中,其中 main.js 是運行在主進程中, index.html 則是運行在渲染進程之中。下面我們通過一個簡單的 Demo,來看一下如何實現(xiàn)兩個進程之間的通信,并且如何通過主進程來進行一些 Node.js 能力調(diào)用的。
進程間的通信
我們想要實現(xiàn)這樣的效果,頁面上有一個按鈕,當點擊按鈕之后,向主進程發(fā)送了一個 say-hello 的消息,當主進程接收到消息之后,它會在系統(tǒng)桌面上創(chuàng)建一個文件叫 hello.txt。并寫入內(nèi)容 Hello Mac!。具體的我們是怎么做的?
ipcRenderer ?API 向主進程發(fā)送一個叫 say-hello 這樣的一個消息。當我們的主進程接收到這樣一個消息之后,則可以在主進程中直接調(diào)用 Node.js 的 fs 模塊,一個文件讀寫的模塊。首先先創(chuàng)建一個文件,并且對這個文件寫入我們所傳輸?shù)膬?nèi)容。當文件寫入成功之后,對渲染進程進行回復,通過調(diào)用 Electron 提供的 Notification模塊,顯示系統(tǒng)通知去告知用戶,這是一個簡單的 Demo 的實現(xiàn),其核心的點就是需要關注主進程和渲染進程的概念,以及兩個進程之間是如何通過 IPC 機制進行通信的,這邊是一個簡單的實現(xiàn)。還有一些更多的應用的場景,這塊就不再對 API 進行過多的介紹。
工程化發(fā)展 CLI -> GUI

以我們的前端工程化平臺敦煌為例,介紹一下我們是如何通過 Electron 將工程化能力由 CLI 式 變?yōu)?GUI 式的使用。首先大家先看一個視頻,這個視頻就是我們在最開始所提到的項目創(chuàng)建的整個流程的運行的演示。大家可以看到我們整個流程完成了 Git 倉庫的創(chuàng)建、項目模板的創(chuàng)建、項目模板到倉庫的推送,并且對 Git 項目進行本地克隆,克隆完成之后,會進行依賴的安裝,并且在客戶端進行重新載入和管理這樣一個流程。將之前分散的單點命令操作,通過 GUI 的方式進行一個串聯(lián)。這個流程只是工程化平臺中的一塊,我們在整個工程化平臺中,實現(xiàn)了很多的單點命令到工作流的串聯(lián)。

I2P(Install To Publish)
這邊是我們整個前端應用管理平臺的系統(tǒng)架構(gòu),大概看一下。核心流程就是上面所寫到的一個 I2P 的概念,就是 install to publish 。它完成了組件、模板和項目這三個級別,從創(chuàng)建到發(fā)布的全流程托管。
創(chuàng)建階段,主要提供了包括本地創(chuàng)建、Git 創(chuàng)建、統(tǒng)一的創(chuàng)建模板管理、創(chuàng)建的流程審批和創(chuàng)建完成的反饋。
開發(fā)階段,提供了一個 Dev Server 的運行能力,對項目級的頁面管理、依賴管理、分支管理,還有一鍵式的升級能力。同時還打通了 CI/CD 持續(xù)集成能力。
發(fā)布階段,則提供了一個發(fā)布前的權(quán)限校驗和合規(guī)檢測、資源推送以及發(fā)布的審批機制。
數(shù)據(jù)分析,是我們整個流程中比較核心的一塊,是對我們整個流程進行一些數(shù)據(jù)沉淀,并且將這些數(shù)據(jù)以可視化報表的形式進行成輸出,基于這些數(shù)據(jù)將整個 I2P 的流程與其他的能力進行一個串聯(lián)。

由點到線

單點命令 -> 任務流
下面我們就具體來看一下如何實現(xiàn)由一個單點命令到任務流這樣的一個串聯(lián)。將單點命令的操作變?yōu)槿蝿樟鞯拇?lián)模式,我們要從以下 4 個切入點來實現(xiàn)。
? 首先我們要將常規(guī)的一些命令調(diào)用變?yōu)楹瘮?shù)式的調(diào)用。
? 基于這些函數(shù)式的調(diào)用,進行一個任務流的編排和組裝,根據(jù)實際的開發(fā)場景,去定制一個任務流。
? 第三塊我們所需要的是整個任務流的任務進度反饋機制,如何將任務執(zhí)行,通過 GUI 的能力,讓用戶可以直觀感受到整個任務的執(zhí)行鏈路和進度。

流程的設計

npm install 變?yōu)?npm.install()
npm install 這樣一個命令行的調(diào)用方式變成變?yōu)橐粋€函數(shù)式的調(diào)用,會變?yōu)?npm.install() 這樣一個調(diào)用方式。
git init 變?yōu)?git.init()

將命令式執(zhí)行 Promise 化
git init 這樣的操作,在執(zhí)行整個命令的時候,我們更多關心的是整個命令執(zhí)行的結(jié)果,可能不太會關心命令執(zhí)行過程中的一些輸出的內(nèi)容。這樣的話我們就可以通過 Node.js 中的 spawn ,啟動子進程來執(zhí)行命令。通過監(jiān)聽子進程輸出來判斷我們整個命令的執(zhí)行狀態(tài),然后對整個命令進行 Promise 封裝,我們就完成了 git init 這樣一個命令行調(diào)用變?yōu)?git.init() 這樣一個異步的函數(shù)調(diào)用。
實時輸出命令執(zhí)行日志
npm install ,依賴安裝,或者說啟動本地開發(fā)服務,整個命令的執(zhí)行過程可能會比較長,我們更關注的是過程中實時的日志輸出。我們怎么來做呢?首先我們這邊是先創(chuàng)建一個 EventEmitter 實例,作為我們的日志的分發(fā)管理,同樣的我們也是通過 spwan 來啟動一個子進程來執(zhí)行命令,并且實時的監(jiān)聽子進程的輸出,將輸出的日志通過 emitter 實例將它分發(fā)出去。當我們在主進程中拿到這樣的實時日志輸出之后,可以通過 Electron 主進程跟渲染進程間的 IPC 的通信,將日志實時的輸出到渲染進程當中。
模擬終端:反饋任務進度
上面我們提到的是主進程中對整個命令執(zhí)行方式的一些改變。那么在我們的渲染進程當中,我們要怎樣去實現(xiàn)類似于剛才視頻中的終端日志反饋呢?反饋的方式有很多,我們可以通過設計一些任務的步驟條,或者進度條這樣的方式來給予整個任務進度的反饋。但是更好的方式是我們可以把任務的進度,包括整個任務輸出日志進行一個及時的反饋。這邊我們使用的是 xterm.js。它是一個基于 ts 所編寫的一個前端終端組件,可以在瀏覽器內(nèi)實現(xiàn)終端應用,VsCode 也是基于 xterm.js 來實現(xiàn)的終端的。要如何將主進程的日志來輸出到渲染進程當中,就是我們上面所提到的,在拿到一個 EventEmitter 所廣播的的輸出之后,要通過主進程與渲染進程之間的通信,將數(shù)據(jù)推送到渲染進程,在渲染進程所需要做的一個處理,把接受到的命令輸出,實時的渲染到 xterm 實現(xiàn)的終端組件上面來。

更新

autoUpdater 模塊,它是 Electron 內(nèi)置的更新管理模塊。首先需要設置 feedUrl,就是最新的更新包在更新服務端地址。當收到一個渲染進程的版本檢測請求之后,調(diào)用 checkForUpdates 方法,之后,它會觸發(fā)下面一系列的一些事件,我們可以通過對整個更新事件的各個生命周期的監(jiān)聽,來完成整個更新流程的把控。
通過 Electron 內(nèi)置的一個更新機制要面臨的問題是更新包體積比較大。因為我們通過 Electron 所構(gòu)建的桌面端的應用,它將整個 Chromium 進行了集成,就會導致即使我們寫了一個很小的 Hello world 這樣一個應用,它的體積壓縮后也會有 40MB 左右,常規(guī)的一個應用來說可能占用 100MB 左右。這樣的問題就是有一些比較小的改動的時候,就需要全量的更新,對于用戶的一個體驗來說并不是很好,對于這些我們有哪些解決方案?首先我們是可以對整個更新的交互設計上做一個優(yōu)化。我們需要提供的是對整個更新流程的一個進度反饋,另外一點就是我們可以通過 autoUpdater,實現(xiàn)后臺的下載。當我們完成了整個更新包的下載之后,然后再通知用戶對整個應用進行一個重啟,然后更新整個應用,這樣的話就才從交互層面上,一定程度的避免了增量更新對用戶所體驗上的一些影響。當然全量更新還會存在的一個問題,如果用戶量比較大的話,就會比較浪費網(wǎng)絡資源。
增量發(fā)布

更新流程

敦煌工程化平臺技術架構(gòu)圖

更多場景




推薦一本書

QA
“請問子洋:如何進行熱更新呢?據(jù)我了解 Electron 打包出來的頁面是放在包內(nèi)的,如何進行在線更新?
我理解問題應該是 UI 層界面的更新。其實剛才我有提到過,我們對頁面的一些靜態(tài)資源是做了一個 cdn 上的托管,在更新的時候,會有一個檢測更新的機制,它可以通過輪詢或者服務端推送來實現(xiàn),當收到靜態(tài)資源版本更新的通知,通過主進程對渲染進程進行一個忽略緩存的強制刷新,或者說可以通過在主進程有相應的交互,包括升級提醒和更新日志,讓用戶觸發(fā)頁面重載,去更新 UI 層面的靜態(tài)資源。
“請問子洋:Electron 和 NW.js 的區(qū)別能請您對比一下嗎?
它們兩個最大的區(qū)別是在于對 Node.js 和 Chromium 事件循環(huán)機制的整合的處理方式是不一樣的。首先 NW.js 是通過修改源碼的方式,讓 Chromium 與 Node.js 的事件循環(huán)機制進行打通;Electron 實現(xiàn)的機制是通過啟用一個新的安全線程,在 Node.js 和 Chromium 之間做事件轉(zhuǎn)發(fā),這樣來實現(xiàn)兩者的打通。這樣的一個好處就是 Chromium 和 Node.js 的事件循環(huán)機制不會有這么強的耦合。另外的區(qū)別則是 NW.js 支持 xp 系統(tǒng),Electron 是不支持的。相比較而言 Electron 有著更活躍的社區(qū),以及更多的大型應用如 VS code、Atom 的實踐案例,更多的區(qū)別可以參考 Electron 官方的一篇介紹:www.electronjs.org/docs/develo…
“請問子洋:更新包的文件是放在私有文件服務器還是 Gitlab 或者 Github 上面?
有比較多方式,我們的實現(xiàn)是通過 CDN 的托管,也可以通過 Github 或者私有文件服務器的搭建來實現(xiàn)。根據(jù)自己實際的業(yè)務場景和技術棧來選擇。
推薦閱讀


