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

自我介紹

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

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

CLI -> GUI

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

業(yè)務(wù)場(chǎng)景應(yīng)用

基建場(chǎng)景應(yīng)用

開發(fā)模式

Electron 架構(gòu)

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

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

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

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

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

由點(diǎn)到線

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

流程的設(shè)計(jì)

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

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

更新

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

更新流程

敦煌工程化平臺(tái)技術(shù)架構(gòu)圖

更多場(chǎng)景




推薦一本書

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


