跳至主要內容

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

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

檢視所有標籤

WebView2 和 Electron

·6 分鐘閱讀

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

兩個團隊都表達了讓網路技術在桌面上達到最佳狀態的目標,並且正在討論共享的全面比較。

Electron 和 WebView2 都是快速發展且不斷演進的專案。我們整理了 Electron 和 WebView2 之間目前存在的相似之處和差異的簡要快照。


架構概述

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

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 的網路標準以外的作業系統 API。

Node.js 已整合到 Electron 中。Electron 應用程式可以使用渲染程序和主程序中的任何 Node.js API、模組或 Node 原生附加元件。WebView2 應用程式不假設您應用程式的其餘部分是用哪種語言或架構編寫的。您的 JavaScript 程式碼必須透過應用程式主機程序代理任何作業系統存取。

Electron 致力於維護與網路 API 的相容性,包括從 Fugu 專案開發的 API。我們有 Electron Fugu API 相容性的快照。WebView2 維護類似的 與 Edge 的 API 差異清單

Electron 為網頁內容提供可設定的安全性模型,從完整存取到完整沙盒。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# 之間進行通訊需要進行封送處理 (marshalling),最常見的是轉換成 JSON 字串。JSON 的序列化/解析是一個耗費資源的操作,而 IPC 瓶頸可能會對效能產生負面影響。從 Edge 93 開始,WV2 將針對網路事件使用 CBOR

Electron 透過 MessagePorts API,在任意兩個進程之間支援直接 IPC,該 API 利用結構化複製演算法 (structured clone algorithm)。應用程式若能善用此機制,在進程之間傳送物件時,便可避免 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)

在上面的程式碼行中,.SetMethod 是在 mate::ObjectTemplateBuilder 上呼叫的。.SetMethod 可以在 ObjectTemplateBuilder 類別的任何實例上呼叫,以在 JavaScript 中的 Object prototype 上設定方法,語法如下

.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)

這些依序會是 JavaScript Object.defineProperty 的實作

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 建置最小化的瀏覽器。提供瀏覽器介面的核心模組稱為 Content Module。

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

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

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

將 Chromium 建置為單個共享函式庫

作為 Content Module 的使用者,Electron 在大多數情況下不需要修改 Chromium 的程式碼,因此改進 Electron 建置的一個明顯方法是將 Chromium 建置為共享函式庫,然後在 Electron 中與其連結。透過這種方式,開發人員在為 Electron 做出貢獻時,不再需要建置整個 Chromium。

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

brightray 專案也是作為 libchromiumcontent 的一部分而誕生,它在 Content Module 周圍提供了一個薄層。

透過一起使用 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 中,除錯版本會連結到 libchromiumcontent 的 shared_library 版本,因為它下載檔案較小,而且在連結最終可執行檔時只需很少的時間。而 Electron 的發佈版本會連結到 libchromiumcontent 的 static_library 版本,以便編譯器可以產生對於除錯很重要的完整符號,並且連結器可以進行更好的最佳化,因為它知道需要哪些物件檔案,以及哪些不需要。

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

gn 更新

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

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

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

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

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

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

總結

正如您所見,與將 Electron 作為 Chromium 的一部分來建置相比,將 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 會將物件儲存在地圖中並為其指派一個 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];
});

具有弱值的地圖

透過先前的簡單實作,remote 模組中的每次呼叫都會從主要程序傳回一個新的遠端物件,而每個遠端物件都代表主要程序中物件的參考。

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

例如,以下程式碼

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

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

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

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

使用 JavaScript 核心中的 API 是不可能的。使用一般地圖快取物件會阻止 V8 對物件進行垃圾回收,而 WeakMap 類別只能使用物件作為弱鍵。

為了解決這個問題,新增了以值作為弱參考的地圖類型,這非常適合快取具有 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 如何被建置,以及許多控制 Node 哪些部分被啟用以及是否開啟特定配置的 GYP 變數。

要變更建置標誌,你需要在你專案的 .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 內部原理的系列文章的第一篇。這篇文章介紹了如何在 Electron 中將 Node 的事件迴圈與 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 的成熟,現在可以使用另一種方法。

backend fd 的概念被引入到 libuv 中,這是一個 libuv 為其事件迴圈輪詢的檔案描述元(或控制代碼)。因此,透過輪詢 backend fd,可以在 libuv 中有新事件時收到通知。

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

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

程式碼

你可以在 electron/atom/common/ 下的 node_bindings 檔案中找到訊息迴圈整合的實作。它可以輕鬆地重複用於想要整合 Node 的專案。

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