導讀
使用Electron
開發客戶端程序已經有一段時間了,整體感覺還是非常不錯的,其中也遇到了一些坑點,本文旨在從【運行原理】到【實際應用】對Electron
進行一次系統性的總結。【多圖,長文預警~】
另外electron-react
還可作為使用Electron + React + Mobx + Webpack
技術棧的腳手架工程。
一、桌面應用程序
桌面應用程序,又稱為 GUI 程序(Graphical User Interface),但是和 GUI 程序也有一些區別。桌面應用程序 將 GUI 程序從 GUI 具體為“桌面”,使冷冰冰的像塊木頭一樣的電腦概念更具有 人性化,更生動和富有活力。
我們電腦上使用的各種客戶端程序都屬于桌面應用程序,近年來WEB
和移動端的興起讓桌面程序漸漸暗淡,但是在某些日常功能或者行業應用中桌面應用程序仍然是必不可少的。
傳統的桌面應用開發方式,一般是下面兩種:
1.1 原生開發
直接將語言編譯成可執行文件,直接調用系統API
,完成 UI 繪制等。這類開發技術,有著較高的運行效率,但一般來說,開發速度較慢,技術要求較高,例如:
- 使用
C++ / MFC
開發Windows
應用 - 使用
Objective-C
開發MAC
應用
1.2 托管平臺
一開始就有本地開發和 UI 開發。一次編譯后,得到中間文件,通過平臺或虛機完成二次加載編譯或解釋運行。運行效率低于原生編譯,但平臺優化后,其效率也是比較可觀的。就開發速度方面,比原生編譯技術要快一些。例如:
- 使用
C# / .NET Framework
(只能開發Windows應用
) Java / Swing
不過,上面兩種對前端開發人員太不友好了,基本是前端人員不會設計的領域,但是在這個【大前端 ??】的時代,前端開發者正在想方設法涉足各個領域,使用WEB
技術開發客戶端的方式橫空出世。
1.3 WEB 開發
使用WEB
技術進行開發,利用瀏覽器引擎完成UI
渲染,利用Node.js
實現服務器端JS
編程并可以調用系統API
,可以把它想像成一個套了一個客戶端外殼的WEB
應用。
在界面上,WEB
的強大生態為UI
帶來了無限可能,并且開發、維護成本相對較低,有WEB
開發經驗的前端開發者很容易上手進行開發。
本文就來著重介紹使用WEB
技術開發客戶端程序的技術之一【electron
】
二、Electron
Electron
是由Github
開發,用HTML,CSS
和JavaScript
來構建跨平臺桌面應用程序的一個開源庫。 Electron
通過將Chromium
和Node.js
合并到同一個運行時環境中,并將其打包為Mac,Windows
和Linux
系統下的應用來實現這一目的。
https://juejin.im/post/5c67619351882562276c3162#heading-5
2.1 使用 Electron 開發的理由:
- 1.使用具有強大生態的
Web
技術進行開發,開發成本低,可擴展性強,更炫酷的UI
- 2.跨平臺,一套代碼可打包為
Windows、Linux、Mac
三套軟件,且編譯快速 - 3.可直接在現有
Web
應用上進行擴展,提供瀏覽器不具備的能力 - 4.你是一個前端 ???
當然,我們也要認清它的缺點:性能比原生桌面應用要低,最終打包后的安裝包和其他文件都比較大。
2.2 開發體驗
兼容性
雖然你還在用WEB
技術進行開發,但是你不用再考慮兼容性問題了,你只需要關心你當前使用Electron
的版本對應Chrome
的版本,一般情況下它已經足夠新來讓你使用最新的API
和語法了,你還可以手動升級Chrome
版本。同樣的,你也不用考慮不同瀏覽器帶了的樣式和代碼兼容問題。
Node 環境
這可能是很多前端開發者曾經夢想過的功能,在WEB
界面中使用Node.js
提供的強大API
,這意味著你在WEB
頁面直接可以操作文件,調用系統API
,甚至操作數據庫。當然,除了完整的Node API
,你還可以使用額外的幾十萬個npm
模塊。
跨域
你可以直接使用Node
提供的request
模塊進行網絡請求,這意味著你無需再被跨域所困擾。
強大的擴展性
借助node-ffi
,為應用程序提供強大的擴展性(后面的章節會詳細介紹)。
2.3 誰在用 Electron
現在市面上已經有非常多的應用在使用electron
進行開發了,包括我們熟悉的VS Code
客戶端、GitHub
客戶端、Atom
客戶端等等。印象很深的,去年迅雷在發布迅雷 X10.1
時的文案:
從迅雷 X 10.1 版本開始,我們采用 Electron 軟件框架完全重寫了迅雷主界面。使用新框架的迅雷 X 可以完美支持 2K、4K 等高清顯示屏,界面中的文字渲染也更加清晰銳利。從技術層面來說,新框架的界面繪制、事件處理等方面比老框架更加靈活高效,因此界面的流暢度也顯著優于老框架的迅雷。至于具體提升有多大?您一試便知。
你可以打開VS Code
,點擊【幫助】【切換開發人員工具】來VS Code
客戶端的界面。
三、Electron 運行原理
Electron
結合了 Chromium
、Node.js
和用于調用操作系統本地功能的API
。
3.1 Chromium
Chromium
是Google
為發展Chrome
瀏覽器而啟動的開源項目,Chromium
相當于Chrome
的工程版或稱實驗版,新功能會率先在Chromium
上實現,待驗證后才會應用在Chrome
上,故Chrome
的功能會相對落后但較穩定。
Chromium
為Electron
提供強大的UI
能力,可以在不考慮兼容性的情況下開發界面。
3.2 Node.js
Node.js
是一個讓JavaScript
運行在服務端的開發平臺,Node
使用事件驅動,非阻塞I/O
模型而得以輕量和高效。
單單靠Chromium
是不能具備直接操作原生GUI
能力的,Electron
內集成了Nodejs
,這讓其在開發界面的同時也有了操作系統底層API
的能力,Nodejs
中常用的 Path、fs、Crypto
等模塊在 Electron
可以直接使用。
3.3 系統 API
為了提供原生系統的GUI
支持,Electron
內置了原生應用程序接口,對調用一些系統功能,如調用系統通知、打開系統文件夾提供支持。
在開發模式上,Electron
在調用系統API
和繪制界面上是分離開發的,下面我們來看看Electron
關于進程如何劃分。
3.4 主進程
Electron
區分了兩種進程:主進程和渲染進程,兩者各自負責自己的職能。
Electron
運行package.json
的 main
腳本的進程被稱為主進程。一個 Electron
應用總是有且只有一個主進程。
職責:
- 創建渲染進程(可多個)
- 控制了應用生命周期(啟動、退出
APP
以及對APP
做一些事件監聽) - 調用系統底層功能、調用原生資源
可調用的 API:
Node.js API
-
Electron
提供的主進程API
(包括一些系統功能和Electron
附加功能)
3.5 渲染進程
由于 Electron
使用了 Chromium
來展示 web
頁面,所以 Chromium
的多進程架構也被使用到。 每個Electron
中的 web
頁面運行在它自己的渲染進程中。
主進程使用 BrowserWindow 實例創建頁面。 每個 BrowserWindow 實例都在自己的渲染進程里運行頁面。 當一個 BrowserWindow 實例被銷毀后,相應的渲染進程也會被終止。
你可以把渲染進程想像成一個瀏覽器窗口,它能存在多個并且相互獨立,不過和瀏覽器不同的是,它能調用Node API
。
職責:
- 用
HTML
和CSS
渲染界面 - 用
JavaScript
做一些界面交互
可調用的 API:
DOM API
Node.js API
-
Electron
提供的渲染進程API
四、Electron 基礎
4.1 Electron API
在上面的章節我們提到,渲染進和主進程分別可調用的Electron API
。所有Electron
的API
都被指派給一種進程類型。 許多API
只能被用于主進程中,有些API
又只能被用于渲染進程,又有一些主進程和渲染進程中都可以使用。
你可以通過如下方式獲取Electron API
const { BrowserWindow, ... } = require('electron')
下面是一些常用的Electron API
:
在后面的章節我們會選擇其中常用的模塊進行詳細介紹。
4.2 使用 Node.js 的 API
你可以同時在Electron
的主進程和渲染進程使用Node.js API
,)所有在Node.js
可以使用的API
,在Electron
中同樣可以使用。
import { shell } from "electron";
import os from "os";
document.getElementById("btn").addEventListener("click", () => {
shell.showItemInFolder(os.homedir());
});
有一個非常重要的提示: 原生 Node.js 模塊 (即指,需要編譯源碼過后才能被使用的模塊) 需要在編譯后才能和 Electron 一起使用。
4.3 進程通信
主進程和渲染進程雖然擁有不同的職責,然是他們也需要相互協作,互相通訊。
例如:在
web
頁面管理原生GUI
資源是很危險的,會很容易泄露資源。所以在web
頁面,不允許直接調用原生GUI
相關的API
。渲染進程如果想要進行原生的GUI
操作,就必須和主進程通訊,請求主進程來完成這些操作。
4.4 渲染進程向主進程通信
ipcRenderer
是一個 EventEmitter
的實例。 你可以使用它提供的一些方法從渲染進程發送同步或異步的消息到主進程。 也可以接收主進程回復的消息。
在渲染進程引入ipcRenderer
:
import { ipcRenderer } from "electron";
異步發送:
通過 channel
發送同步消息到主進程,可以攜帶任意參數。
在內部,參數會被序列化為
JSON
,因此參數對象上的函數和原型鏈不會被發送。
ipcRenderer.send("sync-render", "我是來自渲染進程的異步消息");
同步發送:
const msg = ipcRenderer.sendSync("async-render", "我是來自渲染進程的同步消息");
注意: 發送同步消息將會阻塞整個渲染進程,直到收到主進程的響應。
主進程監聽消息:
ipcMain
模塊是EventEmitter
類的一個實例。 當在主進程中使用時,它處理從渲染器進程(網頁)發送出來的異步和同步信息。 從渲染器進程發送的消息將被發送到該模塊。
ipcMain.on
:監聽 channel
,當接收到新的消息時 listener
會以 listener(event, args...)
的形式被調用。
ipcMain.on("sync-render", (event, data) => {
console.log(data);
});
4.5 主進程向渲染進程通信
https://imweb.io/topic/5b13a663d4c96b9b1b4c4e9c
在主進程中可以通過BrowserWindow
的webContents
向渲染進程發送消息,所以,在發送消息前你必須先找到對應渲染進程的BrowserWindow
對象。:
const mainWindow = BrowserWindow.fromId(global.mainId);
mainWindow.webContents.send("main-msg", `ConardLi]`);
根據消息來源發送:
在ipcMain
接受消息的回調函數中,通過第一個參數event
的屬性sender
可以拿到消息來源渲染進程的webContents
對象,我們可以直接用此對象回應消息。
ipcMain.on("sync-render", (event, data) => {
console.log(data);
event.sender.send("main-msg", "主進程收到了渲染進程的【異步】消息!");
});
渲染進程監聽:
ipcRenderer.on
:監聽 channel
, 當新消息到達,將通過listener(event, args...)
調用 listener
。
ipcRenderer.on("main-msg", (event, msg) => {
console.log(msg);
});
4.6 通信原理
ipcMain
和 ipcRenderer
都是 EventEmitter
類的一個實例。EventEmitter
類是 NodeJS
事件的基礎,它由 NodeJS
中的 events
模塊導出。
EventEmitter
的核心就是事件觸發與事件監聽器功能的封裝。它實現了事件模型需要的接口, 包括 addListener,removeListener
, emit
及其它工具方法. 同原生 JavaScript
事件類似, 采用了發布/訂閱(觀察者)的方式, 使用內部 _events
列表來記錄注冊的事件處理器。
我們通過 ipcMain
和ipcRenderer
的 on、send
進行監聽和發送消息都是 EventEmitter
定義的相關接口。
4.7 remote
remote
模塊為渲染進程(web 頁面)和主進程通信(IPC
)提供了一種簡單方法。 使用 remote
模塊, 你可以調用 main
進程對象的方法, 而不必顯式發送進程間消息, 類似于 Java
的 RMI
。
import { remote } from "electron";
remote.dialog.showErrorBox("主進程才有的dialog模塊", "我是使用remote調用的");
但實際上,我們在調用遠程對象的方法、函數或者通過遠程構造函數創建一個新的對象,實際上都是在發送一個同步的進程間消息。
在上面通過 remote
模塊調用 dialog
的例子里。我們在渲染進程中創建的 dialog
對象其實并不在我們的渲染進程中,它只是讓主進程創建了一個 dialog
對象,并返回了這個相對應的遠程對象給了渲染進程。
4.8 渲染進程間通信
Electron
并沒有提供渲染進程之間相互通信的方式,我們可以在主進程中建立一個消息中轉站。
渲染進程之間通信首先發送消息到主進程,主進程的中轉站接受到消息后根據條件進行分發。
4.9 渲染進程數據共享
在兩個渲染進程間共享數據最簡單的方法是使用瀏覽器中已經實現的HTML5 API
。 其中比較好的方案是用Storage API
, localStorage,sessionStorage
或者 IndexedDB。
就像在瀏覽器中使用一樣,這種存儲相當于在應用程序中永久存儲了一部分數據。有時你并不需要這樣的存儲,只需要在當前應用程序的生命周期內進行一些數據的共享。這時你可以用 Electron
內的 IPC
機制實現。
將數據存在主進程的某個全局變量中,然后在多個渲染進程中使用 remote
模塊來訪問它。
在主進程中初始化全局變量:
global.mainId = ...;
global.device = {...};
global.__dirname = __dirname;
global.myField = { name: 'ConardLi' };
在渲染進程中讀?。?/p>
import { ipcRenderer, remote } from "electron";
const { getGlobal } = remote;
const mainId = getGlobal("mainId");
const dirname = getGlobal("__dirname");
const deviecMac = getGlobal("device").mac;
在渲染進程中改變:
getGlobal("myField").name = "code秘密花園";
多個渲染進程共享同一個主進程的全局變量,這樣即可達到渲染進程數據共享和傳遞的效果。
文中如有錯誤,歡迎在評論區指正,如果這篇文章幫助到了你,歡迎點贊和關注。
歡迎大家到公眾號: you的日常
閱讀,體驗更好哦。