本文由蘑菇街前端技術(shù)團隊分享,原題“Electron 從零到一”,有修訂和改動。
1、引言
在上篇《快速了解新一代跨平臺桌面技術(shù)——Electron》,我們已經(jīng)對Electron跨端框架有了基本的認(rèn)識。
本篇將帶你簡單上手Electron框架開發(fā)跨平臺桌面端,內(nèi)容包括一個快速開始例子、跨進(jìn)程通信原理、打包和分發(fā)、以及一些典型的技術(shù)踩坑等。希望能帶給你啟發(fā)。

學(xué)習(xí)交流:
(本文已同步發(fā)布于:http://www.52im.net/thread-4039-1-1.html)
2、系列文章
本文是系列文章中的第2篇,本系列總目錄如下:
3、Electron簡介
Electron 是一個賦力前端進(jìn)行跨平臺開發(fā)的框架,讓開發(fā)人員使用 JavaScript、HTML 和 CSS 等前端技術(shù)構(gòu)建跨平臺的桌面應(yīng)用。
Electron 通過將 Chromium(所有類Chrome的瀏覽器都是基于這個開源工程而來) 和 Node.js 合并到同一個運行時環(huán)境中(見下圖),并將其打包為 Mac,Windows 和 Linux 系統(tǒng)下的應(yīng)用,而開發(fā)人員只需關(guān)注前端代碼的開發(fā)。

▲ 上圖引用自《快速了解新一代跨平臺桌面技術(shù)——Electron》
Chromium、Node.js、Native API這三者的作用分別是:
- 1)Chromium :為Electron提供了強大的UI能力,可以不考慮傳統(tǒng)瀏覽器兼容性的情況下,利用強大的Web生態(tài)來開發(fā)界面;
- 2)Node.js:讓Electron有了底層的操作能力(比如文件的讀寫,甚至是集成C++等等操作),并可以使用大量開源的npm包來完成開發(fā)需求。
- 3)Native API:Native API讓Electron有了跨平臺和桌面端的原生能力(比如說它有統(tǒng)一的原生界面,窗口、托盤、消息通知這些)。
Electron就是通過這三者的巧妙組合,讓我們開發(fā)跨平臺應(yīng)用變的十分高效。
本質(zhì)上就是chromium(chrome開源版本)瀏覽器,有最新的東西都會在chromium測試,所以electron可以體驗最新的api,這也是好處之一。
有關(guān)Electron的基本介紹等,這里就不再贅述,如果您還不曾了解,可以先閱讀本文的上篇。
4、快速開始
4.1 資料準(zhǔn)備
Electron 官方提供了一個名為electron-quick-start 的項目,可以 clone 下來當(dāng)成模版使用,本文使用 create-react-app 來一步一步學(xué)習(xí)。
其它重要的Electron開發(fā)資源:
4.2 創(chuàng)建一個 react 項目
# 安裝 create-react-app 命令,如果已將安裝請忽略
npm install -g create-react-app
# 創(chuàng)建 electron-react 項目
create-react-app electron-react
# 啟動項目
cd electron-react && npm start
4.3 配置 Electron 環(huán)境
1)在 public 文件夾下新建 index.html,隨便寫點內(nèi)容:
...
<div>hello world</div>
...
2)接下來創(chuàng)建 electron 主線程文件(public/main.js),建議寫在 public 路徑下面:
const{app, BrowserWindow} = require('electron')
// 創(chuàng)建全局變量并在下面引用,避免被GC
let win
function createWindow () {
// 創(chuàng)建瀏覽器窗口并設(shè)置寬高
win = newBrowserWindow({ width: 800, height: 600 })
// 加載頁面
win.loadFile('./index.html')
// 打開開發(fā)者工具
win.webContents.openDevTools()
// 添加window關(guān)閉觸發(fā)事件
win.on('closed', () => {
win = null// 取消引用
})
}
// 初始化后 調(diào)用函數(shù)
app.on('ready', createWindow)
// 當(dāng)全部窗口關(guān)閉時退出。
app.on('window-all-closed', () => {
// 在 macOS 上,除非用戶用 Cmd + Q 確定地退出,
// 否則絕大部分應(yīng)用及其菜單欄會保持激活。
if(process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
// 在macOS上,當(dāng)單擊dock圖標(biāo)并且沒有其他窗口打開時,
// 通常在應(yīng)用程序中重新創(chuàng)建一個窗口。
if(win === null) {
createWindow()
}
})
3)接著再修改 package.json 中的 main 字段對應(yīng)的路徑, 并添加 start 命令:
{
...
"main": "main.js",
"scripts": "electron ."
}
4)執(zhí)行 npm start,就會彈出如下運行界面:

以上就是我簡單寫的一個頁面,大家也可以寫一寫自己感興趣的東西。
真如上面演示的這樣,一個簡單的Electron跨平臺桌面應(yīng)用就開發(fā)好了,真的 so easy!
5、進(jìn)程詳解
5.1 基本認(rèn)知
Electron 架構(gòu)和 Chromium 架構(gòu)類似,也是具有1個主進(jìn)程和多個渲染進(jìn)程(如下圖所示)。

但是也有區(qū)別:
- 1)在各個進(jìn)程中暴露了 Native API ,提供了 Native 能力;
- 2)引入了 Node.js,所以可以使用 Node 的能力;
- 3)但是渲染進(jìn)程使用node 需要配置。
可以簡單的理解為:Electron為web項目套上了Node.js環(huán)境的殼,使得我們可以調(diào)用Node.js的豐富的API。這樣我們可以用JavaScript來寫桌面應(yīng)用,拓展很多我們在web端不能做的事情。
下面這張圖,技術(shù)原理更容易理解一點:

5.2 主進(jìn)程的主要特點
Electron 運行 package.json 的 main 腳本的進(jìn)程被稱為主進(jìn)程 (主進(jìn)程只有一個)。涉及到具體代碼的講解,將在下一節(jié)中展開,本節(jié)就不作過多闡述了。
Electron主進(jìn)程的具體職責(zé):
- 1)主進(jìn)程連接著操作系統(tǒng)和渲染進(jìn)程,可以把她看做頁面和計算機溝通的橋梁;
- 2)進(jìn)程間通信、窗口管理
- 3)全局通用服務(wù);
- 4)一些只能或適合在主進(jìn)程做的事情(例如瀏覽器下載、全局快捷鍵處理、托盤、session);
- 5)維護(hù)一些必要的全局狀態(tài)。
5.3 渲染進(jìn)程的主要特點
渲染進(jìn)程就是我們所熟悉前端環(huán)境了,只是載體改變了,從瀏覽器變成了window.
注意:出于安全考慮,渲染進(jìn)程是不能直接訪問本地資源的,因此都需要在主進(jìn)程完成。
Electron渲染進(jìn)程主要特點:
- 1)Electron 使用了 Chromium 來展示 web 頁面,所以 Chromium 的多進(jìn)程架構(gòu)也被使用到;
- 2)每個web頁面運行在它自己的渲染進(jìn)程中(每個渲染進(jìn)程都是相互獨立的,并且只關(guān)心他們自己的網(wǎng)頁);
- 3)使用BrowserWindow類開啟一個渲染進(jìn)程并將這個實例運行在該進(jìn)程中,當(dāng)一個BrowserWindow實例被銷毀后,相應(yīng)的渲染進(jìn)程也會被終止;
- 4)渲染進(jìn)程中不能調(diào)用原生資源,但是渲染進(jìn)程中同樣包含Node.js環(huán)境,所以可以引入Node.js。
5.4 主進(jìn)程與渲染進(jìn)程的關(guān)系
主進(jìn)程與渲染進(jìn)程的關(guān)系主要是這樣:
- 1)主進(jìn)程使用 BrowserWindow 實例創(chuàng)建網(wǎng)頁;
- 2)每個 BrowserWindow 實例都在自己的渲染進(jìn)程里運行著一個網(wǎng)頁(當(dāng)一個 BrowserWindow 實例被銷毀后,相應(yīng)的渲染進(jìn)程也會被終止);
- 3)主進(jìn)程管理所有頁面和與之對應(yīng)的渲染進(jìn)程;
- 4)由于在網(wǎng)頁里管理原生 GUI 資源是非常危險而且容易造成資源泄露,所以在網(wǎng)頁面調(diào)用 GUI 相關(guān)的 APIs 是不被允許的(如果你想在網(wǎng)頁里使用 GUI 操作,其對應(yīng)的渲染進(jìn)程必須與主進(jìn)程進(jìn)行通訊,請求主進(jìn)程進(jìn)行相關(guān)的 GUI 操作)。
具體關(guān)系如下圖所示:

把它們想象成這樣:
即Chrome(或其他瀏覽器)的每個標(biāo)簽頁(tab)及其頁面,就好比 Electron 中的一個單獨渲染進(jìn)程。即使關(guān)閉所有標(biāo)簽頁,Chrome 依然存在。這好比 Electron 的主進(jìn)程,能打開新的窗口或關(guān)閉這個應(yīng)用。就像下圖這樣。

6、從代碼角度理解進(jìn)程
6.1 主進(jìn)程和渲染進(jìn)程
先來看看 electron 項目基本目錄結(jié)構(gòu):
app
└─public
└─index.html---------------入口文件
├─main.js----------------------程序啟動入口,主進(jìn)程
├─ipc--------------------------進(jìn)程間模塊
├─appNetwork-------------------應(yīng)用通信模塊
└─src--------------------------窗口管理,渲染進(jìn)程
├─components---------------通用組件模塊
├─store--------------------數(shù)據(jù)共享模塊
├─statics------------------靜態(tài)資源模塊
└─pages----------------------窗口業(yè)務(wù)模塊
├─窗口A----------------窗口
└─窗口B----------------窗口
如上所示:package.json 中的 main 字段對應(yīng)的文件的進(jìn)程是主進(jìn)程。Electron集成了Chromium來展示窗口界面,窗口中所看到的內(nèi)容使用的都是HTML渲染出來的。
Chromium本身是多進(jìn)程渲染頁面的架構(gòu)(在默認(rèn)情況下,Chromium的默認(rèn)策略是對每一個tab新開一個進(jìn)程,以確保每個頁面是獨立且互不影響的,避免一個頁面的崩潰導(dǎo)致全部頁面無法使用),所以Electron在展示窗口時,也會使用到Chromium的多進(jìn)程架構(gòu)。而這種多進(jìn)程渲染架構(gòu)在Electron中,也就被是渲染進(jìn)程(render process)啦。
6.2 進(jìn)程間通信
在 Electron 中,GUI 相關(guān)的模塊(如 dialog,menu 等)僅在主進(jìn)程可用,在渲染進(jìn)程中不可用。
為了在渲染進(jìn)程中使用它們,需要使用 ipc 模塊向主進(jìn)程發(fā)送消息,下面是幾種進(jìn)程間通訊的方法。
1)ipcMain & ipcRenderer:
從主進(jìn)程到渲染進(jìn)程的異步通信,也可以將消息從主進(jìn)程發(fā)送到渲染進(jìn)程(參考文檔)。
發(fā)送消息時,事件名稱為 channel?;貜?fù)同步消息時,需要設(shè)置 event.returnValue。
將異步消息發(fā)送回發(fā)送方,可以使用 event.reply(...),這個輔助方法將自動處理來自渲染進(jìn)程的消息,然而 event.sender.send(...) 這個方法則始終將消息發(fā)送給主進(jìn)程。
下面是在渲染和主進(jìn)程之間發(fā)送和處理消息的一個例子:
// 在主進(jìn)程中
const { ipcMain } = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg); // 輸出 'ping'
event.reply('asynchronous-reply', 'pong');
})
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // 輸出 ‘ping’
event.returnValue = 'pong'
})
// 在渲染進(jìn)程(網(wǎng)頁)中
const { ipcRenderer } = require('electron')
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // 輸出 'pong'
ipcRenderer.on('asynchronous-reply', (event, arg) => {
console.log(arg); // 輸出 'pong'
})
ipcRenderer.send('asynchronous-message', 'ping')
2)remote 模塊:
remote 為渲染進(jìn)程和主進(jìn)程通信提供了一種簡單的方法。你可以調(diào)用 main 進(jìn)程對象的方法,而不必顯式發(fā)送進(jìn)程間消息。
例如:從渲染進(jìn)程創(chuàng)建瀏覽器窗口
const { BrowserWindow } = require('electron').remote
let win = newBrowserWindow({ width: 800, height: 600 })
win.loadUrl('https://www.mogu.com')
注意: 反過來,如果需要從主進(jìn)程訪問渲染進(jìn)程,可以使用 webContents.executeJavascript。
3)webContents:
即通過 channel 向渲染進(jìn)程發(fā)送異步消息,可以發(fā)送任意參數(shù)。在內(nèi)部,參數(shù)會被序列化為 JSON,因此參數(shù)對象傷的函數(shù)和原型鏈不會被發(fā)送。
除了以上這些方法,也可以使用 localStorage、sessionStorage 等。
7、打包發(fā)布
開發(fā)完成后,還需要將應(yīng)用打包成可執(zhí)行文件,這一環(huán)節(jié)的坑還是學(xué)習(xí) electron 到現(xiàn)在踩的最多的。
目前主流的打包工具有 electron-packager和 electron-builder
7.1 electron-packager
1)安裝依賴:
npm i electron-packager --save-dev
2)打包:
electron-packager --platform= --arch= [optional flags...]
也可以直接運行 npm run electron-packager 打包。
7.2 electron-builder
官方解釋:
A complete solution to package and build a ready for distribution Electron, Proton Native or Muon app for macOS, Windows and Linux with “auto update” support out of the box.
簡單的說:electron-builder 有比 electron-packager 更豐富的功能,支持更多的平臺,同時也支持了自動更新。除了這幾點外,electron-builder 打出的包更為輕量,并且可以打包出不暴露源碼的 setup 安裝程序。
另外使用下來感覺比 electron-packager 的坑要少一點。
1)安裝依賴:
npm i electron-builder --save-dev
2)打包(在項目的 package.json 文件中定義 build 字段):
{
"build": {
"appId": "com.xxx.app",
"extends": null,
"files": [
"build/**/*"
],
"mac": {
"icon": "icons/icon.icns"
},
"win": {
"target": "nsis",
"icon": "icons/icon.png"
}
}
}
這是最基礎(chǔ)的配置,當(dāng)然打包過程中可能會碰到其他的問題需要修改配置。通常 files 配置只寫一個 build 文件夾是不夠的,要根據(jù)項目結(jié)構(gòu)和打包情況添加其他路徑。
添加 scripts 命令
{
"scripts": {
"pack": "electron-builder"
}
}
運行 npm run pack 打包。
打包完成后在 dist 目錄下有可執(zhí)行文件,打開后如果沒有報錯,則說明打包成功。
8、踩坑總結(jié)
我所遇到的大部分都是打包遇到的坑,以下列舉幾個典型的坑。
8.1 使用 electron-packager 打包報錯
Generated checksum for"electron-v6.0.2-darwin-x64.zip"did not match expected checksum。
解決方法:node 版本升級到 8.x 以上就好。
8.2 打開打包生成的可執(zhí)行文件報錯

出現(xiàn)這種問題可能有以下幾個原因。
1)項目中可能直接訪問了本地路徑, 瀏覽器為了安全考慮不允許訪問。
2)package.json 中的 build 配置問題,假如 main.js 在一個很深的路徑中,需要在下面單獨添加 main.js 的路徑:
"build": {
...
+ "public/main.js"
...
}
3)webpack 配置中的路徑直接使用了 __dirname, 可以使用 remote 模塊的 getAppPath 方法取得路徑:
const remote = require('remote')
const app = remote.require('app')
console.log(app.getAppPath());
參考資料:https://github.com/electron/electron/issues/5107
8.3 dependencies & devDependencies
在 Electron 打包時,一定要分清哪些是生產(chǎn)環(huán)境依賴,哪些是開發(fā)環(huán)境依賴,避免出現(xiàn)此類錯誤:

8.4 關(guān)于打包慢的問題(npm & cnpm)
cnpm 裝的各種 node_modules,這種方式下所有的包都是扁平化的安裝,一下子 node_modules 展開就有非常多的文件,導(dǎo)致打包的過程非常慢。
但是如果該用 npm 來安裝 node_modules 的話,所有的包都是樹狀結(jié)構(gòu),層級變深。但是打包速度會快很多(具體資料參見:electron打包過了2小時都沒好?)。
9、Electron的優(yōu)缺點
文章的最后,基于實踐體會,總結(jié)一下Electron的優(yōu)缺點。
Electron優(yōu)點很明顯:
- 1)上手較簡單:HTML、CSS、JS、Node 、npm包、UI框架 ,方便高效,能很輕松的實現(xiàn)很好看的UI;
- 2)可多端運行:可以快速構(gòu)建“跨平臺”(Windows、MacOs、Linux)的桌面級應(yīng)用;
- 3)開發(fā)時間短:相對其他跨平臺方案(如:QT、GTK+ 等),更穩(wěn)定、bug少,畢竟只要瀏覽器外殼跑起來了就可以了(當(dāng)然坑是少不了的);
- 4)兼容性問題:再也不用兼容多瀏覽器(只針對谷歌,但要兼容mac、Linux)。
Electron缺點也同樣顯而易見:
- 1)安裝包體積大:安裝包體積略大(打包了Chromium),至少包含了一個瀏覽器的體積 ,每裝一個app就相當(dāng)于裝一個chrome;
- 2)運行性能稍差:性能不如原生應(yīng)用,Mac系統(tǒng)下絲滑一些,Window系統(tǒng)就有點丟幀;
- 3)內(nèi)存占用較大:卡、啟動慢,新開一個進(jìn)程,起步價就是一個NodeJS的內(nèi)存開銷;
- 4)網(wǎng)頁加載稍慢:loadURL加載遠(yuǎn)程頁面白屏?xí)r間長(優(yōu)化可采用 VSCode 骨架屏)。
10、參考資料
[1] Electron官方開發(fā)者手冊
[2] Electron初體驗(快速開始、跨進(jìn)程通信、打包、踩坑等)
[3] Electron 基礎(chǔ)入門 簡單明了,看完啥都懂了
[4] 網(wǎng)易云信Web端IM的聊天消息全文檢索技術(shù)實踐
(本文已同步發(fā)布于:http://www.52im.net/thread-4039-1-1.html)