Electron 內部原理:將 Chromium 建置為函式庫
Electron 基於 Google 的開源專案 Chromium,但 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 中匯出的符號數量非常多,即使是 Content Module 和 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_library
或 shared_library
表示,對於 Chromium 的正常建置,每個模組都產生一個靜態函式庫,並且它們在最終可執行檔中連結在一起。透過使用 gn
,每個模組現在只產生一堆物件檔案,最終可執行檔只是將所有物件檔案連結在一起,因此不再產生中間靜態函式庫檔案。
然而,這種改進給 libchromiumcontent 帶來了很大的麻煩,因為 libchromiumcontent 實際上需要中間靜態函式庫檔案。
解決這個問題的第一個嘗試是修補 gn
以產生靜態函式庫檔案,這解決了問題,但遠非一個像樣的解決方案。
第二個嘗試是由 @alespergl 進行的,目的是從物件檔案清單中產生自訂靜態函式庫。它使用了一個技巧,首先執行一個虛擬建置來收集產生的物件檔案清單,然後透過將清單饋送給 gn
來實際建置靜態函式庫。它僅對 Chromium 的原始碼進行了最小的變更,並保持了 Electron 的建置架構不變。
總結
正如您所看到的,與作為 Chromium 的一部分建置 Electron 相比,將 Chromium 建置為函式庫需要付出更大的努力,並且需要持續維護。然而,後者消除了建置 Electron 對強大硬體的需求,從而使更大範圍的開發人員能夠建置 Electron 並為其做出貢獻。這些努力是完全值得的。