跳至主要內容

6 篇標記為「Electron 內部原理」的文章

「深入 Electron 原始碼的技術探討」

檢視所有標籤

WebView2 與 Electron

·6 分鐘閱讀

在過去幾週,我們收到了一些關於全新 WebView2 與 Electron 之間差異的問題。

雙方團隊皆表達了讓 Web 技術在桌面上達到最佳狀態的目標,並且正在討論共享的全面比較。

Electron 與 WebView2 都是快速發展且不斷演進的專案。我們彙編了 Electron 和 WebView2 截至今日的相似之處與相異之處的簡要快照。


架構概述

Electron 和 WebView2 皆從 Chromium 原始碼建置以用於渲染網頁內容。嚴格來說,WebView2 從 Edge 原始碼建置,但 Edge 是使用 Chromium 原始碼的分支建置的。Electron 不與 Chrome 共用任何 DLL。WebView2 二進位檔硬連結至 Edge (截至 Edge 90 為止的穩定通道),因此它們共用磁碟和部分工作集。請參閱 常青發佈模式 以取得更多資訊。

Electron 應用程式始終捆綁並發佈它們開發時所用的確切 Electron 版本。WebView2 在發佈中有兩個選項。您可以捆綁您的應用程式開發時所用的確切 WebView2 程式庫,或者您可以使用系統上可能已存在的共享執行階段版本。WebView2 為每種方法提供工具,包括在共享執行階段遺失時的啟動安裝程式。WebView2 從 Windows 11 開始內建出貨。

捆綁其框架的應用程式負責更新這些框架,包括次要安全性發佈。對於使用共享 WebView2 執行階段的應用程式,WebView2 有自己的更新程式,類似於 Chrome 或 Edge,獨立於您的應用程式執行。更新應用程式的程式碼或其任何其他相依性仍然是開發人員的責任,與 Electron 相同。Electron 和 WebView2 都不受 Windows Update 管理。

Electron 和 WebView2 都繼承了 Chromium 的多進程架構 - 即,單一主進程與一個或多個渲染器進程通訊。這些進程與系統上執行的其他應用程式完全隔離。每個 Electron 應用程式都是一個單獨的進程樹,包含一個根瀏覽器進程、一些公用程式進程和零個或多個渲染進程。使用相同使用者資料夾的 WebView2 應用程式 (例如應用程式套件會這樣做) 共用非渲染器進程。使用不同資料夾的 WebView2 應用程式不共用進程。

  • ElectronJS 進程模型

    ElectronJS Process Model Diagram

  • 基於 WebView2 的應用程式進程模型

    WebView2 Process Model Diagram

在此處閱讀更多關於 WebView2 的進程模型Electron 的進程模型 的資訊。

Electron 提供常用桌面應用程式需求的 API,例如選單、檔案系統存取、通知等等。WebView2 是一個旨在整合到應用程式框架 (例如 WinForms、WPF、WinUI 或 Win32) 中的元件。WebView2 不透過 JavaScript 提供 Web 標準之外的作業系統 API。

Node.js 已整合到 Electron 中。Electron 應用程式可以使用來自渲染器和主進程的任何 Node.js API、模組或 node-native-addon。WebView2 應用程式不假設您的應用程式的其餘部分是以哪種語言或框架編寫的。您的 JavaScript 程式碼必須透過應用程式主機進程代理任何作業系統存取。

Electron 致力於維持與 Web API 的相容性,包括從 Fugu Project 開發的 API。我們有一個 Electron 的 Fugu API 相容性快照。WebView2 維護了類似的 與 Edge 的 API 差異列表

Electron 具有可配置的 Web 內容安全性模型,從完全存取到完全沙箱。WebView2 內容始終處於沙箱中。Electron 具有關於選擇安全性模型的全面安全性文件。WebView2 也有安全性最佳實務

Electron 原始碼在 GitHub 上維護和提供。應用程式可以修改並建置它們自己的 Electron 品牌。WebView2 原始碼在 GitHub 上不可用。

快速摘要

ElectronWebView2
建置相依性ChromiumEdge
原始碼在 GitHub 上可用
共用 Edge/Chrome DLL是 (截至 Edge 90)
應用程式之間的共享執行階段可選
應用程式 API
Node.js
沙箱可選總是
需要應用程式框架
支援的平台Mac、Win、LinuxWin (計畫支援 Mac/Linux)
應用程式之間的進程共享永不可選
框架更新管理方式應用程式WebView2

效能討論

在渲染您的網頁內容方面,我們預期 Electron、WebView2 和任何其他基於 Chromium 的渲染器之間的效能差異很小。我們建立了 使用 Electron、C++ + WebView2 和 C# + WebView2 建置的應用程式的骨架,供有興趣調查潛在效能差異的人使用。

在網頁內容渲染之外,還有一些差異會發揮作用,來自 Electron、WebView2、Edge 和其他方面的同仁已表示有興趣進行詳細比較,包括 PWA。

跨進程通訊 (IPC)

我們想要立即強調一個差異,因為我們認為它通常是 Electron 應用程式中的效能考量。

在 Chromium 中,瀏覽器進程充當沙箱渲染器與系統其餘部分之間的 IPC 代理。雖然 Electron 允許非沙箱渲染器進程,但許多應用程式選擇啟用沙箱以增加安全性。WebView2 始終啟用沙箱,因此對於大多數 Electron 和 WebView2 應用程式,IPC 可能會影響整體效能。

儘管 Electron 和 WebView2 具有相似的進程模型,但底層 IPC 不同。在 JavaScript 和 C++ 或 C# 之間通訊需要序列化,最常見的是 JSON 字串。JSON 序列化/解析是一項昂貴的操作,而 IPC 瓶頸可能會對效能產生負面影響。從 Edge 93 開始,WV2 將對網路事件使用 CBOR

Electron 透過 MessagePorts API 支援任何兩個進程之間的直接 IPC,該 API 利用 結構化複製演算法。利用此功能的應用程式可以在進程之間傳送物件時避免支付 JSON 序列化稅。

摘要

Electron 和 WebView2 有許多差異,但不預期它們在渲染網頁內容的效能方面會有太大差異。最終,應用程式的架構和 JavaScript 程式庫/框架對記憶體和效能的影響大於其他任何因素,因為無論 Chromium 在哪裡執行,它都是 Chromium

特別感謝 WebView2 團隊審閱這篇文章,並確保我們對 WebView2 架構有最新的看法。他們歡迎對該專案的任何 意見回饋

從原生到 JavaScript 在 Electron 中

·4 分鐘閱讀

以 C++ 或 Objective-C 編寫的 Electron 功能如何傳遞到 JavaScript,以便最終使用者可以使用它們?


背景

Electron 是一個 JavaScript 平台,其主要目的是降低開發人員建置穩健桌面應用程式的門檻,而無需擔心平台特定的實作。然而,在其核心,Electron 本身仍然需要以給定的系統語言編寫平台特定的功能。

實際上,Electron 為您處理原生程式碼,以便您可以專注於單一 JavaScript API。

但這是如何運作的呢?以 C++ 或 Objective-C 編寫的 Electron 功能如何傳遞到 JavaScript,以便最終使用者可以使用它們?

為了追蹤此路徑,讓我們從 app 模組 開始。

透過開啟我們 lib/ 目錄中的 app.ts 檔案,您會在頂端附近找到以下程式碼行

const binding = process.electronBinding('app');

這行直接指向 Electron 將其 C++/Objective-C 模組繫結到 JavaScript 以供開發人員使用的機制。此函式由標頭和 實作檔案ElectronBindings 類別建立。

process.electronBinding

這些檔案新增了 process.electronBinding 函式,其行為類似於 Node.js 的 process.bindingprocess.binding 是 Node.js require() 方法的較低階實作,但它允許使用者 require 原生程式碼而不是以 JS 編寫的其他程式碼。此自訂 process.electronBinding 函式賦予了從 Electron 載入原生程式碼的能力。

當最上層 JavaScript 模組 (例如 app) 需要此原生程式碼時,該原生程式碼的狀態是如何判斷和設定的?方法在哪裡公開到 JavaScript?屬性呢?

native_mate

目前,此問題的答案可以在 native_mate 中找到:它是 Chromium gin 程式庫 的分支,它使在 C++ 和 JavaScript 之間序列化類型變得更容易。

native_mate/native_mate 內部,有一個 object_template_builder 的標頭和實作檔案。這就是允許我們在原生程式碼中形成模組的原因,這些模組的形狀符合 JavaScript 開發人員的期望。

mate::ObjectTemplateBuilder

如果我們將每個 Electron 模組視為 object,就更容易理解為什麼我們想要使用 object_template_builder 來建構它們。此類別建構於 V8 公開的類別之上,V8 是 Google 的開放原始碼高效能 JavaScript 和 WebAssembly 引擎,以 C++ 編寫。V8 實作 JavaScript (ECMAScript) 規範,因此其原生功能實作可以直接關聯到 JavaScript 中的實作。例如,v8::ObjectTemplate 為我們提供了沒有專用建構函式和原型的 JavaScript 物件。它使用 Object[.prototype],在 JavaScript 中相當於 Object.create()

若要查看實際運作情況,請查看 app 模組的實作檔案,atom_api_app.cc。底部是以下內容

mate::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate())
.SetMethod("getGPUInfo", &App::GetGPUInfo)

在上述程式碼行中,在 mate::ObjectTemplateBuilder 上呼叫了 .SetMethod。可以對 ObjectTemplateBuilder 類別的任何執行個體呼叫 .SetMethod,以在 JavaScript 中的 Object 原型 上設定方法,語法如下

.SetMethod("method_name", &function_to_bind)

這是 JavaScript 等效的

function App{}
App.prototype.getGPUInfo = function () {
// implementation here
}

此類別還包含在模組上設定屬性的函式

.SetProperty("property_name", &getter_function_to_bind)

.SetProperty("property_name", &getter_function_to_bind, &setter_function_to_bind)

這些反過來將是 Object.defineProperty 的 JavaScript 實作

function App {}
Object.defineProperty(App.prototype, 'myProperty', {
get() {
return _myProperty
}
})

function App {}
Object.defineProperty(App.prototype, 'myProperty', {
get() {
return _myProperty
}
set(newPropertyValue) {
_myProperty = newPropertyValue
}
})

可以建立以原型和屬性形成的 JavaScript 物件,如同開發人員所預期的那樣,並且更清楚地推斷在此較低系統層級實作的函式和屬性!

關於在何處實作任何給定模組方法的決策本身是一個複雜且通常不確定的決策,我們將在未來的文章中介紹。

Electron 內部原理:將 Chromium 建置為程式庫

·7 分鐘閱讀

Electron 基於 Google 的開放原始碼 Chromium,該專案不一定設計為供其他專案使用。這篇文章介紹了 Chromium 如何建置為程式庫供 Electron 使用,以及多年來建置系統如何演進。


使用 CEF

Chromium Embedded Framework (CEF) 是一個專案,它將 Chromium 轉換為程式庫,並提供基於 Chromium 程式碼庫的穩定 API。Atom 編輯器和 NW.js 的非常早期版本都使用 CEF。

為了維護穩定的 API,CEF 隱藏了 Chromium 的所有細節,並使用自己的介面包裝了 Chromium 的 API。因此,當我們需要存取底層 Chromium API 時,例如將 Node.js 整合到網頁中,CEF 的優勢就變成了阻礙。

因此,最終 Electron 和 NW.js 都切換為直接使用 Chromium 的 API。

作為 Chromium 的一部分建置

即使 Chromium 沒有正式支援外部專案,程式碼庫也是模組化的,並且可以輕鬆地建置基於 Chromium 的最小瀏覽器。提供瀏覽器介面的核心模組稱為內容模組。

為了開發使用內容模組的專案,最簡單的方法是將專案作為 Chromium 的一部分建置。這可以透過先檢查 Chromium 的原始碼,然後將專案新增到 Chromium 的 DEPS 檔案來完成。

NW.js 和非常早期的 Electron 版本都使用這種方式進行建置。

缺點是,Chromium 是一個非常龐大的程式碼庫,需要非常強大的機器才能建置。對於普通的筆記型電腦,這可能需要超過 5 個小時。因此,這極大地影響了可以為專案做出貢獻的開發人員數量,並且也使開發速度變慢。

將 Chromium 建置為單一共享程式庫

作為內容模組的使用者,Electron 在大多數情況下不需要修改 Chromium 的程式碼,因此改進 Electron 建置的顯而易見的方法是將 Chromium 建置為共享程式庫,然後在 Electron 中與其連結。這樣,開發人員在為 Electron 做出貢獻時不再需要建置所有 Chromium。

libchromiumcontent 專案由 @aroben 為此目的而建立。它將 Chromium 的內容模組建置為共享程式庫,然後提供 Chromium 的標頭和預先建置的二進位檔供下載。libchromiumcontent 初始版本的程式碼可以在 此連結 中找到。

brightray 專案也作為 libchromiumcontent 的一部分誕生,它在內容模組周圍提供了一個薄層。

透過一起使用 libchromiumcontent 和 brightray,開發人員可以快速建置瀏覽器,而無需深入了解 Chromium 的建置細節。並且它消除了建置專案對快速網路和強大機器的需求。

除了 Electron 之外,還有其他基於 Chromium 的專案以這種方式建置,例如 Breach 瀏覽器

過濾匯出的符號

在 Windows 上,一個共享程式庫可以匯出的符號數量有限制。隨著 Chromium 的程式碼庫不斷增長,libchromiumcontent 中匯出的符號數量很快就超過了限制。

解決方案是在產生 DLL 檔案時過濾掉不需要的符號。它的工作原理是向連結器提供 .def 檔案,然後使用腳本來判斷是否應該匯出命名空間下的符號

透過採用這種方法,儘管 Chromium 不斷新增匯出的符號,但 libchromiumcontent 仍然可以透過剝離更多符號來產生共享程式庫檔案。

元件建置

在討論 libchromiumcontent 中採取的後續步驟之前,重要的是首先介紹 Chromium 中元件建置的概念。

作為一個龐大的專案,Chromium 在建置時的連結步驟非常耗時。通常,當開發人員進行小幅變更時,可能需要 10 分鐘才能看到最終輸出。為了解決這個問題,Chromium 引入了元件建置,它將 Chromium 中的每個模組建置為單獨的共享程式庫,因此最終連結步驟中花費的時間變得微不足道。

發佈原始二進位檔

隨著 Chromium 的持續增長,Chromium 中匯出的符號太多了,即使內容模組和 Webkit 的符號也超過了限制。僅僅透過剝離符號就無法產生可用的共享程式庫。

最後,我們不得不發佈 Chromium 的原始二進位檔,而不是產生單一共享程式庫。

如前所述,Chromium 中有兩種建置模式。由於發佈了原始二進位檔,我們不得不在 libchromiumcontent 中發佈兩種不同的二進位檔發行版。一種稱為 static_library 建置,其中包括 Chromium 的正常建置產生的每個模組的所有靜態程式庫。另一種是 shared_library,其中包括元件建置產生的每個模組的所有共享程式庫。

在 Electron 中,Debug 版本與 libchromiumcontent 的 shared_library 版本連結,因為它下載量小,並且在連結最終可執行檔時花費的時間很少。Electron 的 Release 版本與 libchromiumcontent 的 static_library 版本連結,因此編譯器可以產生對偵錯很重要的完整符號,並且連結器可以進行更好的最佳化,因為它知道需要哪些物件檔案,而哪些不需要。

因此,對於正常開發,開發人員只需要建置 Debug 版本,這不需要良好的網路或強大的機器。雖然 Release 版本然後需要更好的硬體才能建置,但它可以產生最佳化的二進位檔。

gn 更新

作為世界上最大的專案之一,大多數普通系統都不適合建置 Chromium,Chromium 團隊開發了自己的建置工具。

早期版本的 Chromium 使用 gyp 作為建置系統,但它的缺點是速度慢,並且其組態檔案對於複雜專案變得難以理解。經過多年的開發,Chromium 切換到 gn 作為建置系統,它速度更快並且具有清晰的架構。

gn 的改進之一是引入 source_set,它代表一組物件檔案。在 gyp 中,每個模組都由 static_libraryshared_library 表示,對於 Chromium 的正常建置,每個模組都會產生一個靜態程式庫,並且它們在最終可執行檔中連結在一起。透過使用 gn,每個模組現在僅產生一堆物件檔案,並且最終可執行檔只是將所有物件檔案連結在一起,因此不再產生中間靜態程式庫檔案。

然而,此改進給 libchromiumcontent 帶來了很大的麻煩,因為 libchromiumcontent 實際上需要中間靜態程式庫檔案。

解決此問題的第一個嘗試是修補 gn 以產生靜態程式庫檔案,這解決了問題,但遠非一個體面的解決方案。

第二個嘗試是由 @alespergl 進行的,從物件檔案列表產生自訂靜態程式庫。它使用了一個技巧,首先執行虛擬建置以收集產生的物件檔案列表,然後透過將列表饋送到 gn 來實際建置靜態程式庫。它僅對 Chromium 的原始碼進行了最小的變更,並保持了 Electron 的建置架構不變。

摘要

正如您所看到的,與作為 Chromium 一部分建置 Electron 相比,將 Chromium 建置為程式庫需要付出更大的努力並需要持續維護。然而,後者消除了建置 Electron 對強大硬體的需求,從而使更廣泛的開發人員能夠建置 Electron 並為其做出貢獻。這種努力是完全值得的。

Electron 內部原理:弱參考

·6 分鐘閱讀

作為一種具有垃圾回收的語言,JavaScript 使用戶無需手動管理資源。但是由於 Electron 託管此環境,因此它必須非常小心地避免記憶體和資源洩漏。

這篇文章介紹了弱參考的概念以及它們如何在 Electron 中用於管理資源。


弱參考

在 JavaScript 中,每當您將物件指派給變數時,您都會新增對該物件的參考。只要存在對該物件的參考,它就會始終保留在記憶體中。一旦對物件的所有參考都消失了,即不再有變數儲存該物件,JavaScript 引擎將在下次垃圾回收時回收記憶體。

弱參考是一種對物件的參照,它允許您取得該物件,而不會影響它是否會被垃圾回收。當物件被垃圾回收時,您也會收到通知。這樣一來,就可以使用 JavaScript 管理資源了。

以 Electron 中的 NativeImage 類別為例,每次您呼叫 nativeImage.create() API 時,都會傳回一個 NativeImage 實例,並且它會在 C++ 中儲存影像資料。一旦您完成了該實例,且 JavaScript 引擎 (V8) 已經對該物件進行垃圾回收,就會呼叫 C++ 中的程式碼來釋放記憶體中的影像資料,因此使用者無需手動管理。

另一個例子是視窗消失問題,它以視覺化的方式展示了當所有對視窗的參照都消失時,視窗是如何被垃圾回收的。

在 Electron 中測試弱參考

在原始 JavaScript 中沒有直接測試弱參考的方法,因為該語言沒有指派弱參考的方式。JavaScript 中唯一與弱參考相關的 API 是 WeakMap,但由於它僅建立弱參考鍵,因此無法知道物件何時被垃圾回收。

在 v0.37.8 之前的 Electron 版本中,您可以使用內部 v8Util.setDestructor API 來測試弱參考,它會將弱參考新增到傳遞的物件,並在物件被垃圾回收時呼叫回呼函式

// Code below can only run on Electron < v0.37.8.
var v8Util = process.atomBinding('v8_util');

var object = {};
v8Util.setDestructor(object, function () {
console.log('The object is garbage collected');
});

// Remove all references to the object.
object = undefined;
// Manually starts a GC.
gc();
// Console prints "The object is garbage collected".

請注意,您必須使用 --js-flags="--expose_gc" 命令開關啟動 Electron,才能公開內部的 gc 函式。

該 API 在後續版本中被移除,因為 V8 實際上不允許在解構函式中執行 JavaScript 程式碼,而在後續版本中這樣做會導致隨機崩潰。

remote 模組中的弱參考

除了使用 C++ 管理原生資源外,Electron 也需要弱參考來管理 JavaScript 資源。Electron 的 remote 模組就是一個例子,它是一個遠端程序呼叫 (RPC) 模組,允許從渲染程序中使用主程序中的物件。

remote 模組的一個主要挑戰是避免記憶體洩漏。當使用者在渲染程序中取得遠端物件時,remote 模組必須保證該物件在主程序中持續存在,直到渲染程序中的參照消失為止。此外,它還必須確保當渲染程序中不再有對該物件的任何參照時,該物件可以被垃圾回收。

例如,若沒有適當的實作,以下程式碼會快速導致記憶體洩漏

const { remote } = require('electron');

for (let i = 0; i < 10000; ++i) {
remote.nativeImage.createEmpty();
}

remote 模組中的資源管理很簡單。每當請求一個物件時,就會向主程序發送一個訊息,Electron 會將該物件儲存在一個 map 中並為其指派一個 ID,然後將該 ID 發送回渲染程序。在渲染程序中,remote 模組將接收到 ID 並用一個代理物件包裝它,當代理物件被垃圾回收時,將向主程序發送一個訊息以釋放該物件。

remote.require API 為例,一個簡化的實作看起來像這樣

remote.require = function (name) {
// Tell the main process to return the metadata of the module.
const meta = ipcRenderer.sendSync('REQUIRE', name);
// Create a proxy object.
const object = metaToValue(meta);
// Tell the main process to free the object when the proxy object is garbage
// collected.
v8Util.setDestructor(object, function () {
ipcRenderer.send('FREE', meta.id);
});
return object;
};

在主程序中

const map = {};
const id = 0;

ipcMain.on('REQUIRE', function (event, name) {
const object = require(name);
// Add a reference to the object.
map[++id] = object;
// Convert the object to metadata.
event.returnValue = valueToMeta(id, object);
});

ipcMain.on('FREE', function (event, id) {
delete map[id];
});

具有弱值的 Map

使用先前的簡單實作,remote 模組中的每次呼叫都會從主程序傳回一個新的遠端物件,並且每個遠端物件都代表對主程序中物件的參照。

設計本身沒問題,但問題是當多次呼叫接收同一個物件時,會建立多個代理物件,而對於複雜的物件,這可能會對記憶體使用量和垃圾回收造成巨大的壓力。

例如,以下程式碼

const { remote } = require('electron');

for (let i = 0; i < 10000; ++i) {
remote.getCurrentWindow();
}

它首先使用大量記憶體建立代理物件,然後佔用 CPU(中央處理器)來進行垃圾回收並發送 IPC 訊息。

一個顯而易見的優化是快取遠端物件:當已經存在具有相同 ID 的遠端物件時,將傳回先前的遠端物件,而不是建立新的物件。

使用 JavaScript 核心中的 API 無法做到這一點。使用普通的 map 來快取物件會阻止 V8 對物件進行垃圾回收,而 WeakMap 類別只能使用物件作為弱鍵。

為了解決這個問題,新增了一種以值作為弱參考的 map 類型,這非常適合使用 ID 快取物件。現在 remote.require 看起來像這樣

const remoteObjectCache = v8Util.createIDWeakMap()

remote.require = function (name) {
// Tell the main process to return the meta data of the module.
...
if (remoteObjectCache.has(meta.id))
return remoteObjectCache.get(meta.id)
// Create a proxy object.
...
remoteObjectCache.set(meta.id, object)
return object
}

請注意,remoteObjectCache 將物件儲存為弱參考,因此當物件被垃圾回收時,無需刪除鍵。

原生程式碼

對於對 Electron 中弱參考的 C++ 程式碼感興趣的人,可以在以下檔案中找到它

setDestructor API

createIDWeakMap API

Electron 內部原理:使用 Node 作為程式庫

·4 分鐘閱讀

這是正在進行的系列文章中的第二篇,解釋 Electron 的內部原理。如果您還沒看過關於事件迴圈整合的第一篇文章,請查看。

大多數人使用 Node 用於伺服器端應用程式,但由於 Node 豐富的 API 集合和蓬勃發展的社群,它也非常適合作為嵌入式函式庫。這篇文章解釋了 Node 如何在 Electron 中作為函式庫使用。


建置系統

Node 和 Electron 都使用 GYP 作為其建置系統。如果您想將 Node 嵌入到您的應用程式中,您也必須將其用作您的建置系統。

GYP 新手?在繼續閱讀本文之前,請先閱讀本指南

Node 的標誌

Node 原始碼目錄中的 node.gyp 檔案描述了 Node 的建置方式,以及許多 GYP 變數,這些變數控制著啟用了 Node 的哪些部分以及是否開啟某些配置。

要變更建置標誌,您需要在專案的 .gypi 檔案中設定變數。Node 中的 configure 腳本可以為您產生一些常見的配置,例如執行 ./configure --shared 將產生一個 config.gypi,其中包含指示 Node 建置為共享函式庫的變數。

Electron 不使用 configure 腳本,因為它有自己的建置腳本。Node 的配置定義在 Electron 根原始碼目錄中的 common.gypi 檔案中。

在 Electron 中,Node 通過將 GYP 變數 node_shared 設定為 true 來作為共享函式庫連結,因此 Node 的建置類型將從 executable 變更為 shared_library,並且包含 Node 的 main 進入點的原始碼將不會被編譯。

由於 Electron 使用 Chromium 附帶的 V8 函式庫,因此不使用 Node 原始碼中包含的 V8 函式庫。這是通過將 node_use_v8_platformnode_use_bundled_v8 都設定為 false 來完成的。

共享函式庫或靜態函式庫

在與 Node 連結時,有兩種選擇:您可以將 Node 建置為靜態函式庫並將其包含在最終可執行檔中,或者您可以將其建置為共享函式庫並將其與最終可執行檔一起發布。

在 Electron 中,Node 長期以來都是作為靜態函式庫建置的。這使得建置變得簡單,啟用了最佳的編譯器最佳化,並允許 Electron 在沒有額外的 node.dll 檔案的情況下發布。

然而,在 Chrome 切換到使用 BoringSSL 後,這種情況發生了變化。BoringSSL 是 OpenSSL 的一個分支,它移除了幾個未使用的 API 並變更了許多現有的介面。由於 Node 仍然使用 OpenSSL,如果它們連結在一起,編譯器會由於符號衝突而產生大量的連結錯誤。

Electron 無法在 Node 中使用 BoringSSL,也無法在 Chromium 中使用 OpenSSL,因此唯一的選擇是切換到將 Node 建置為共享函式庫,並隱藏每個元件中的 BoringSSL 和 OpenSSL 符號

這個變更為 Electron 帶來了一些正面的副作用。在此變更之前,如果您使用了原生模組,您將無法在 Windows 上重新命名 Electron 的可執行檔,因為可執行檔的名稱被硬編碼在導入函式庫中。在 Node 作為共享函式庫建置之後,這個限制消失了,因為所有原生模組都連結到 node.dll,而 node.dll 的名稱不需要變更。

支援原生模組

Node 中的原生模組通過定義一個供 Node 載入的進入函式,然後從 Node 中搜尋 V8 和 libuv 的符號來工作。對於嵌入者來說,這有點麻煩,因為預設情況下,當將 Node 建置為函式庫時,V8 和 libuv 的符號會被隱藏,並且原生模組將因為找不到符號而載入失敗。

因此,為了使原生模組能夠工作,V8 和 libuv 符號在 Electron 中被公開。對於 V8,這是通過強制公開 Chromium 配置檔案中的所有符號來完成的。對於 libuv,這是通過設定 BUILDING_UV_SHARED=1 定義來實現的。

在您的應用程式中啟動 Node

在完成所有建置和與 Node 連結的工作之後,最後一步是在您的應用程式中執行 Node。

Node 沒有提供許多公共 API 來將自身嵌入到其他應用程式中。通常,您只需呼叫 node::Startnode::Init 來啟動一個新的 Node 實例。但是,如果您要建置一個基於 Node 的複雜應用程式,您必須使用像 node::CreateEnvironment 這樣的 API 來精確控制每個步驟。

在 Electron 中,Node 以兩種模式啟動:獨立模式,在主程序中執行,類似於官方 Node 二進位檔;以及嵌入式模式,將 Node API 插入到網頁中。這方面的詳細資訊將在未來的文章中解釋。

Electron 內部原理:訊息迴圈整合

·3 分鐘閱讀時間

這是解釋 Electron 內部原理系列文章的第一篇。這篇文章介紹了 Node 的事件迴圈如何在 Electron 中與 Chromium 整合。


曾經有很多嘗試使用 Node 進行 GUI 程式設計,例如用於 GTK+ 綁定的 node-gui 和用於 QT 綁定的 node-qt。但它們都無法在生產環境中工作,因為 GUI 工具組有自己的消息迴圈,而 Node 使用 libuv 作為自己的事件迴圈,主執行緒一次只能執行一個迴圈。因此,在 Node 中執行 GUI 消息迴圈的常用技巧是在一個間隔非常小的計時器中泵送消息迴圈,這使得 GUI 介面回應緩慢並佔用大量 CPU 資源。

在 Electron 的開發過程中,我們遇到了同樣的問題,儘管方式相反:我們必須將 Node 的事件迴圈整合到 Chromium 的消息迴圈中。

主程序和渲染程序

在深入探討消息迴圈整合的細節之前,我將首先解釋 Chromium 的多程序架構。

在 Electron 中,有兩種程序類型:主程序和渲染程序(實際上這被極度簡化了,要獲得完整的視圖,請參閱 多程序架構)。主程序負責 GUI 工作,例如建立視窗,而渲染程序僅處理執行和渲染網頁。

Electron 允許使用 JavaScript 來控制主程序和渲染程序,這意味著我們必須將 Node 整合到這兩個程序中。

用 libuv 替換 Chromium 的消息迴圈

我的第一次嘗試是用 libuv 重新實作 Chromium 的消息迴圈。

對於渲染程序來說,這很容易,因為它的消息迴圈只監聽檔案描述器和計時器,而我只需要用 libuv 實作介面。

然而,對於主程序來說,這要困難得多。每個平台都有自己種類的 GUI 消息迴圈。macOS Chromium 使用 NSRunLoop,而 Linux 使用 glib。我嘗試了許多駭客技巧來從原生 GUI 消息迴圈中提取底層檔案描述器,然後將它們饋送到 libuv 進行迭代,但我仍然遇到了不起作用的邊緣情況。

所以最後我新增了一個計時器,以小間隔輪詢 GUI 消息迴圈。結果,該程序佔用了恆定的 CPU 使用率,並且某些操作有很長的延遲。

在單獨的執行緒中輪詢 Node 的事件迴圈

隨著 libuv 的成熟,隨後有可能採取另一種方法。

後端 fd 的概念被引入到 libuv 中,它是一個檔案描述器(或控制代碼),libuv 輪詢它以獲取其事件迴圈。因此,通過輪詢後端 fd,可以獲得 libuv 中何時有新事件的通知。

因此,在 Electron 中,我建立了一個單獨的執行緒來輪詢後端 fd,並且由於我使用的是系統呼叫進行輪詢而不是 libuv API,因此它是執行緒安全的。並且每當 libuv 的事件迴圈中有新事件時,都會向 Chromium 的消息迴圈發布一個訊息,然後 libuv 的事件將在主執行緒中處理。

通過這種方式,我避免了修補 Chromium 和 Node,並且相同的程式碼用於主程序和渲染程序。

程式碼

您可以在 electron/atom/common/ 下的 node_bindings 檔案中找到消息迴圈整合的實作。它可以很容易地重複用於想要整合 Node 的專案。

更新:實作已移動到 electron/shell/common/node_bindings.cc