Electron 在 Taro IDE 的開發實踐
- 2020 年 4 月 9 日
- 筆記
背景—
Taro IDE 是一款我們正在精心打造的一站式移動端研發工作台。除了需要實現 Taro 從創建項目到預覽、編譯的全部能力,還需要打通用戶測試、調試、監控等一系列流程。為了提升開發體驗,僅僅一個命令行工具是遠遠不夠的,我們需要開發一款桌面客戶端,並同時提供 Windows、MacOS 等不同系統的版本。
Electron[1] 最初是 Github 為 Atom 編輯器開發的桌面應用框架。Electron 將 Chromium 與 Node 合併到同個運行時環境中,賦予了 Web 程式碼與底層作業系統進行交互的能力,並在打包時生成 Windows、MacOS、Linux 等平台的桌面應用。比起原生的桌面應用開發框架,Electron 在性能、應用體積方面會稍遜一籌,但 Electron 支援打包多個平台的桌面應用,在業界已經有 VSCode、Atom、Slack 等綜合體驗拔群的成功案例,我們認為 Electron 完全滿足我們的需求。
介紹 Electron—
如果只想體驗一下 Electron,最快的方式是使用 Electron Fiddle[2],或者直接使用社區中提供的 腳手架[3]。
最初接觸 Electron,一般是被「使用前端技術棧生成多平台桌面應用」的特性吸引。但在後續的開發中,才會留意到 Electron 相比 NW.js[4] 更為複雜的進程模型:
Electron 的架構可以用下圖來表示:

Electron 項目中,運行 package.json 的 main 腳本的進程被稱為主進程。主進程通過創建 web 頁面來展示用戶介面。這些用戶介面都運行在彼此隔離的渲染進程中。
Electron 主進程支援 Node API,並且可直接與作業系統進行底層交互,彈出系統通知、文件系統讀寫、調用硬體設備等。
Electron 渲染進程默認只能與自身的 Web 內容進行交互。在打開 nodeIntegration
功能後,渲染進程也可以具備操作 Node 的能力。渲染進程也無法直接操作彈窗(Dialog)、系統通知(Notification)等,這些功能都需要通過 Electron 提供的 IPC/remote 機制在主進程中調用。
並且在後續 Electron 的升級中,這些約束也可能因為安全、性能的原因進行調整。可以說,Electron 的開發體驗並不太美好,但正是這種開發體驗與用戶體驗之間的博弈,保證了 Electron 應用在性能、安全方面的表現。
開發工作流—
我們使用社區提供的 electron-react-typescript[5] 作為項目的初始腳手架。閱讀 package.json 文件,我們可以了解到,這個項目使用 webpack 進行主進程和渲染進程的打包,src/main/main.ts[6] 文件就是主進程的入口。
Electron 的 BrowserWindow 類負責創建和控制瀏覽器窗口,app 對象則可以控制應用程式的各個事件與生命周期。主進程的程式碼大致如下:
import { app, BrowserWindow } from'electron' let win app.on('ready', () => { win = new BrowserWindow({ width: 800, height: 600 }); win.loadURL(`http://localhost:2003`); // xxx }); app.on('activate', () => {}) app.on('window-all-closed', () => {})
渲染進程 src/renderer/app.tsx[7] 就一個普通的頁面,這裡不再贅述。安裝依賴後,使用 yarn start-dev
,即可啟動項目的預覽服務。
這個項目使用 webpack 來打包項目程式碼,這樣處理有兩個好處。一是通過 webpack 處理,我們可以減少運行時的 require 調用,對 Electron 應用載入性能有一定幫助;二是藉助 webpack 的 tree shaking 能力,未使用的程式碼也會被輕鬆移除,可以有效減少安裝包體積。
為了打包 electron 項目,我們需要至少兩份 webpack 配置文件,一份打包主進程文件,指定 target 為 electron-main
,另一份打包渲染進程,target 設置為 electron-renderer
。
為了輔助 Electron 項目的調試工作,我們可以安裝 Devtron[8]。Devtron 是 Electron 提供的開發調試插件。在開發者工具中加入 Devtron 後,項目中的 IPC 通訊、查看項目依賴、事件等資訊,都可以在開發者工具中直接查看。
如有需要,我們還可以安裝其他的開發者工具擴展,例如 Redux、React 等,只需要在主進程中運行:
// main.js const { default: installExtension, REACT_DEVELOPER_TOOLS, REACT_PERF, REDUX_DEVTOOLS } = require('electron-devtools-installer') const extensions = [REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS, REACT_PERF] extensions.forEach(extension => { try { installExtension(extension) } catch (e) { console.error(e) } })
至此,我們的開發環境搭建完畢,可以開始進行業務程式碼的開發了。
優化—
業務程式碼開發完畢後,就到了優化的環節了。這裡主要從 Electron 應用的性能與體積兩方面來講。
性能
Electron 在性能方面一直受到廣大開發者的詬病。窗口打開慢,載入時間長都是老生常談的話題。這些問題該如何解決呢?
答案是預載入。在展示登錄窗口時,我們可以提前將主窗口開啟並設置隱藏,預載入主窗口的靜態資源。用戶登錄後,再通過 IPC 消息通知主窗口展示,達到秒開的效果。這個過程可以用下圖表示:

除了窗口載入,在 Electron 中,require Node 模組也是相當昂貴的操作。如果在渲染進程中直接使用大量的原生模組,會嚴重拖慢頁面的打開時間,造成窗口可交互時間的延後,這對於桌面應用來說是災難性的體驗。Electron@5 之後的版本已經默認關閉了 BrowserWindow 的 nodeIntegration
功能,可以看出 Electron 團隊也並不建議在渲染進程中直接使用原生模組。
在桌面應用中,等待是非常難以忍受的,性能上的些許欠缺都會讓用戶覺得這是個套殼的網頁。如需使用原生模組,我們更建議使用非同步的方式載入模組,或是使用非同步 IPC 在主進程中調用。另外,為了優化用戶的體驗,我們還需要在小動畫等方面下功夫,例如骨架屏等等。
Atom 團隊通過使用 V8 snapshot 能力,在生產環境中去掉了低性能的 require 調用,將 Electron 應用的載入性能提升了 30%,同時還提升了應用的安全性能,這篇文章 How Atom Uses Chromium Snapshots[9] 對他們的做法做了詳細介紹。
啟用骨架屏前後對比:

性能優化的方式並不局限於上面的方式。例如開啟 electron-builder 的 asar 功能,在打包時將源碼生成二進位的 asar 文件,降低 require 操作的代價的同時,也能稍許減少空間佔用,代價是無法對 asar 內的文件使用 child_process.spawn
;需要密集計算的功能,可以開多一個渲染進程來跑,或是使用 require('child_process').spawn
開子進程來跑,避免阻塞主進程,造成應用卡死。
體積
同樣受到開發者詬病的,還有 Electron 應用的體積 。一個空 Electron 項目,在打包後就會佔據近上百兆空間。Electron 的應用體積之所以大,除了自帶的 Chromium 內核,還有大部分體積是來自用戶安裝的 node_modules。
使用 electron-builder 打包 Electron 應用時,如果不加處理,會將 node_modules 內的內容全數打包,導致應用體積偏大。針對這種情況,我們可以進行一系列優化:
- 使用
yarn autoclean
命令進行清理。node_modules 目錄中,包含著大量的 README 文件、文檔等內容,這部分文件在生產環境中並非必要。如果項目中使用yarn
進行依賴管理,則可以使用yarn autoclean
命令。這個命令會初始化一份默認的配置文件.yarnclean
。yarn
在安裝依賴後,將會自動根據.yarnclean
進行依賴清理。# 默認的 .yarnclean 文件大致如下:
# test directories
__tests__
test
tests
powered-test
# asset directories
docs
doc
website
assets
# examples
example
examples
...
- 使用雙 package.json 架構。node_modules 目錄中,除了生產環境需要用到的依賴,還存在著很多 devDependencies,這部分依賴是不應該被打包的。為了解決這個問題,electron-builder 提供了雙 package.json 架構。具體來說,electron-builder 推薦用戶將 Electron 應用依賴劃分為兩部分:開發依賴以及生產依賴。用戶使用項目根目錄的 package.json 來管理開發依賴,而使用項目的應用文件夾下的 package.json 管理生產依賴。electron-builder 僅會打包應用文件夾下的依賴。 在這個改動後,安裝依賴時還需要通過
electron-builder install-app-deps
命令安裝應用依賴。這個操作推薦放在package.json
內的post-install
腳本中。 electron-builder@8 後,並不會打包devDependencies
內的依賴。這意味著我們可以通過這個途徑來避免開發依賴被打包的問題。如果項目使用了 webpack 之類的工具進行打包,則需要注意將 webpack 已經打包過的資源從 dependencies 中排除,避免重複打包。
未來—
能力 Web 化
目前,項目的大部分能力依然是基於 Electron 提供的能力實現的。這相當於與 Electron 嚴重耦合,不利於項目中個別能力的復用。未來,我們希望對項目的架構進行調整,對核心能力進行插件化改造,方便能力的移植與復用,甚至未來的研發上雲,這有賴於項目核心能力的 Web 化。當然,Web 化也會帶來額外的性能損耗,這會對我們項目的性能提出新的要求。

崩潰處理
項目的穩定性也是未來需要努力的方向。我們有時會收到用戶關於應用閃退、卡死等現象的回饋,卻苦於無法復現,很多時候難以解決用戶回饋的問題。未來,我們需要在項目中加入異常監控上報的機制,收集作業系統資訊、記憶體使用量等關鍵資訊,在 Crash 時進行上報,甚至推送告警消息。這有利於開發人員進一步了解用戶的使用過程,方便問題的復現。
小結—
在開發桌面應用時,Electron 在效率上有很大的優勢。幾行 JS 程式碼就可以啟動桌面客戶端,大大降低了開發門檻。但 Electron 在性能、體積等方面也存在著軟肋。如果在前期開發時沒有經過充分思考,很有可能會在後期優化時付出慘痛的代價。在這個項目中,我們的優化工作還遠遠不夠,後續有更多突破會分享給大家。
參考資料
[1]
Electron: https://www.electronjs.org/
[2]
Electron Fiddle: https://www.electronjs.org/fiddle
[3]
腳手架: https://github.com/search?q=electron+boilerplate&ref=opensearch
[4]
NW.js: https://nwjs.io/
[5]
electron-react-typescript: https://github.com/Robinfr/electron-react-typescript
[6]
src/main/main.ts: https://github.com/Robinfr/electron-react-typescript/blob/b50263f06ecd518bfd43421a3c0bc3c3be308b64/src/main/main.ts
[7]
src/renderer/app.tsx: https://github.com/Robinfr/electron-react-typescript/blob/b50263f06ecd518bfd43421a3c0bc3c3be308b64/src/renderer/app.tsx#L1
[8]
Devtron: https://www.electronjs.org/devtron
[9]
How Atom Uses Chromium Snapshots: https://flight-manual.atom.io/behind-atom/sections/how-atom-uses-chromium-snapshots/