Electron

原名Atom Shell.
自Electron 28開始, 支援ESM.
參考: https://github.com/Elanis/web-to-desktop-framework-comparison
NW.js(原名node-webkit)是Electron的主要競爭對手, 二者同樣是Chromium+Node.js架構的跨平台桌面應用程式解決方案.
NW.js比Electron創立早2年, 但Electron現已遠比NW.js受歡迎, 並且情況不太可能在未來改變.
  • Electron分離了Main程序和Renderer程序.
    Renderer程序內分為兩個上下文, 分別執行Node.js程式碼和瀏覽器端程式碼.
Electron的模型分離了關注點, 但這種強制分離關注點的設計是否有意義是非常值得商榷的.
異常繁瑣的IPC設計和源源不斷的安全作秀使開發體驗變得極差.
  • NW.js基於Chrome Apps架構, Node.js和Chromium渲染器共享同一程序, 可以直接在瀏覽器端使用Node.js模組.
    由於可以在瀏覽器端直接載入Node.js原生插件, NW.js的性能遠遠優於依賴IPC的Electron和基於WASM的方案.
有分離上下文和混合上下文兩種模式:
  • 分離上下文模式下, 分為Node上下文和Web上下文兩個環境, Node上下文是Node.js模組的運行環境, Web上下文是NW.js腳本的運行環境.
    由於不同上下文的全局變量是隔離的, 容易出現引用相等性問題.
  • 混合上下文模式下, 只使用一個上下文.
    不會出現引用相等性問題, 但存在被第三方庫污染上下文的風險.
  • Electron的入口點是Main程序的JavaScript腳本.
  • NW.js入口點主要是HTML檔案, 但也可以是JavaScript腳本.
Electron的文件顯著優於NW.js的文件, 後者的文件可讀性差且嚴重缺乏更新.
  • Electron沒有原生的原始碼保護, 需要使用bytenode或electron-vite這樣的V8字節碼方案.
  • NW.js有官方提供的程式碼編譯器nwjc, 原理跟bytenode相同, 但實現略有不同:
    因為NW.js可以直接修改V8, 從而避免了bytenode這樣的外部方案所存在的許多局限性.
  • Electron有很多商業公司用戶, 最著名的項目是VSCode.
  • 儘管其起步較早, NW.js現存的用戶量比較少.js.
    RPG Maker製作的遊戲可能是NW.js目前最大的用戶來源.
Neutralinojs不打包Chromium, 而是使用作業系統上的WebView.
WebView可想而知會帶來兼容性問題, 因此不會被考慮為生產級方案.
Neutralionjs不支援較舊的Windows系統: https://github.com/neutralinojs/neutralinojs/issues/486
Tauri使用Rust和作業系統的WebView, 不支援Node.js模組, 需要用Rust編寫API.
WebView可想而知會帶來兼容性問題, 因此不會被考慮為生產級方案.
除了桌面平台, Tauri還計劃支援iOS和Android.
  • 上下文隔離製造了不必要的複雜性.
  • API缺乏統一性, 光是移除視窗的菜單欄就有多種方法.
  • 過快的主版本迭代策略.
  • 缺乏有用的測試框架.
Electron應用程式的主程序, 是整個應用程式的入口, 一個實例裡只能有一個.
Electron應用程式裡負責用戶界面的程序, 由主程序啟動, 一個實例裡可以有多個.
渲染器程序在默認情況下不具備直接訪問本機Node.js模組的能力,
所有Node.js模組都需要通過由preload腳本創建的上下文隔離接口來間接調用.
可以在創建視窗時通過 nodeIntegration: true 選項啟用渲染器程序的Node.js集成.
默認情況下, preload腳本與渲染器程序的宿主環境是隔離的,
兩者只能通過 contextBridge 和 window.postMessage 進行通信.
可以通過 contextIsolation: false 取消這一行為.
contextBirdge.exposeInMainWorld 是在preload腳本裡向渲染器程序的宿主環境暴露API的唯一方法.
橋接時可用的數據類型受到限制:
除了基本的可結構化克隆的類型以外, 還允許發送函數, Promise, 錯誤等對象, 但仍然不能滿足常見需求.
橋接還具有以下特性:
  • 對preload來說, API暴露是一次性的, 不能重複暴露, API的變化也不能同步到宿主環境.
  • 對宿主環境來說, 遠程函數內部會顯示成native code.
  • 暴露的對象會被凍結, 因此對宿主環境來說, API不可寫.
  • 對不支援的類型, 可能會出現傳 undefined /對象屬性拷貝/報錯這三種不同的結果.
renderer程序可以使用ipcRenderer向main程序發出請求.
這種ipc支援兩種模式:
  • send: 僅向ipcMain發送消息
  • invoke: request-reply模式
由於上下文隔離, renderer程序裡調用 ipcRenderer.send/invoke 是很不方便的.
main程序可以向renderer程序發出請求, 但前提是先得到renderer程序的WebContents對象.
WebContents對象有以下兩種取得方式:
  • 由renderer首先向main發出請求, main接收到的 event.sender 就是WebContents對象.
  • main保留創建視窗時的BrowserWindow, 然後使用 BrowserWindow.webContents.
由於上下文隔離, 在renderer程序裡註冊事件監聽器是很不方便的.
標準MesssagePort接口.
https://www.electronjs.org/docs/latest/tutorial/message-ports
適用於main和renderer, renderer和renderer之間的通信.
由於main和renderer之間可以通過ipcMain和ipcRenderer通信,
因此這種通信方式主要是用於renderer和renderer之間的通信.
建立通信前, 首先要通過ipcMain和ipcRenderer傳遞MessagePort對象.
藉助網絡協議可以實現IPC, 例如HTTP和WebSocket.
考慮到在Electron裡建立Main和Renderer(Web上下文)的通信是如此繁瑣,
使用基於網絡協議的作為IPC降低複雜性也是一種方法.
  • IPC接口會暴露給同一網絡的其他程序
  • 性能比其他方法差.
由於ABI不同, Electron不能直接使用Node.js的native模組, 模組必須專門為Electron編譯.
https://www.electronjs.org/docs/latest/tutorial/devtools-extension
Electron有限度地支援Chrome擴展程序API, 可以載入一些開發工具.
在Windows和macOS上, electron自帶的API可以用於設置開機啟動:
https://www.electronjs.org/docs/latest/api/app#appsetloginitemsettingssettings-macos-windows
然而, 相關功能的實現充滿了可悲的bug:
  • https://github.com/electron/electron/issues/32657
  • https://github.com/electron/electron/issues/33308
該實現不支援自定義args.
專門用於將配置項持久化為JSON檔案的方案.
底層使用了同作者的conf模組, 支援JSON Schema和基於 package.json 的版本遷移.
一個在內存中運行的文檔資料庫, 間歇性持久化到檔案系統.
開發不積極.
純JavaScript模組, 用JSON作為資料庫.
這玩意說白了就是一個用於讀取和寫入JSON的類, 沒有任何亮眼之處.
作者停止維護了.
將RxJS和PouchDB結合起來的一個高級資料庫.
由於使用PouchDB的生態系統, 官方支援Electron.
下載量與Github Stars數不成比例的典型代表, 實際的用戶量並不多.
儘管作者對這個項目付出很多努力, 但該項目對於新用戶來說仍然具有陡峭的學習曲線.
這個項目還依賴了太多作者無力控制的軟體包, 導致該項目變得非常脆弱:
  • RxJS
  • PouchDB
  • pouchdb-find(RxCollection的find方法直接使用了此包, 因此共享相同的缺陷)
  • mquery(複製了程式碼)
從v10版本起, RxDB開始為脫離PouchDB而做準備.
無論從項目的流行程度, 可維護性, 還是其依賴項的可靠性考慮, RxDB都不值得使用.
在Node.js裡, PouchDB通過levelup模組利用LevelDB作為儲存底層,
levelup模組的底層是native模組, 但它原生支援Electron, 因此不需要用electron-rebuild處理就能使用.
PouchDB並非為多程序場景而設計, 有很多基於內存的優化, 並且其Node.js底層LevelDB也不支援多程序.
因此在Electron使用時, 應該只在Main程序裡創建它的實例, 然後由IPC暴露給Renderer訪問.
pouchdb-find是用於實現Mongo式查詢語法的官方擴展.
這個包存在很多問題, 包括但不限於:
  • 在有索引的情況下反而無法得到正確結果:
    https://github.com/pouchdb/pouchdb/issues/7810
    儘管相關issue被關閉, 但我仍然在v7.2.2的包裡遇到了相同的問題.
  • 丟棄選擇器:
    https://github.com/pouchdb/pouchdb/issues/6572
    https://github.com/pouchdb/pouchdb/issues/6371
依賴該包進行查詢顯然是不可靠的.
在沒有Mongo支援的情況下, PouchDB沒有使用價值.
上一個版本是2020年發布的, 期間的bug沒有被修復, 有很多關鍵bug的issue被維護者忽視.
其類型定義 @types/pouchdb 也是舊的(適配的是PouchDB v6.4).
RxDB的作者在RxDB v10的發行註記中也認為PouchDB沒有得到良好的維護.
https://github.com/pouchdb/pouchdb/issues/3224
作為一個不支援Node-API(N-API)的native模組,
better-sqlite3需要用electron-rebuild處理後才能使用, 這可能帶來依賴鎖定問題.
這是Ghost團隊維護的SQLite3模組.
該模組自v5版本開始基於Node-API重寫.
callback接口還設置了this, 導致無法使用箭頭函數.
為了獲得Promise包裝, 需要安裝第三方包裝器node-sqlite.
該庫的每個Database實例只是一個連接, 不存在任何競爭條件.
當開始事務期間, 在實例上運行的所有的語句都會成為該事務的一部分.
因此, 總是需要創建一個新的實例來開始事務, 為了複用實例, 意味著需要創建池, 這使解決方案變得複雜.
這是VSCode使用的SQLite3庫, 它是sqlite3的fork.
https://www.electronjs.org/docs/latest/tutorial/application-distribution
本質上, 應用程式分發是通過分發滿足特定目錄結構的預構建Electron二進制包來實現的.
例如, 在Windows上, 人工分發的方法是下載對應版本, 對應平台的預構建好的Electron二進制包,
在Electron目錄下創建 resources/app 目錄, 然後將整個項目檔案拷貝到此目錄下.
之後, 通常還需要對Electron可執行檔案改名和更改圖標.
官方的electron-packager包可用來簡化以上步驟.
Electron專用的一種類似tar的檔案格式, 旨在將Electron工程打包成單個檔案.
resources/app.asar 檔案可以替代 resources/app 資料夾.
一些本機模組在打包成asar檔案後會彈出此錯誤.
在不使用asar打包的情況下, Electron可以正確找到並載入本機模組.
使用 --asar.unpack=*.{node,dll} 替換electron-packager的 --asar 可以解決此問題.
參考:
  • https://github.com/electron/packager/issues/424
  • https://github.com/electron/asar/issues/75
app.isPackaged 被廣泛用於判斷Electron應用程式是處於開發環境還是生產環境.
然而, 它的判斷方式比較反直覺, 它是通過檢查Electron可執行檔案的檔案名是否仍為electron來實現的.
該包用來自動化創建各個平台的應用程式.
  • 支援設置可執行檔案圖標.
  • 支援刪除devDependencies以縮小打包體積.
electron-packager缺少按ignore檔案忽略打包路徑的特性, 雖然曾經有過 .electronignore 檔案的PR, 但是被拒絕
(拒絕的理由缺乏邏輯和事實支撐).
缺乏此特性導致electron-packager變成一個無用的包, 因為除非在命令行裡大量添加ignore參數, 否則會將開發環境一同打包.
該項目在啟用 --all 選項時對I/O和CPU資源的使用明顯高於它所執行任務所需的程度.
檢查原始碼之後發現, 該程序只是將所有任務并行, 而這在electron v28意味著同時下載10個electron.
考慮到伺服器的頻寬和下載方的頻寬, 這種毫無節制的并行只是在增加遇到網絡錯誤的概率.
Windows安裝包的開源解決方案.
由electron-builder支援.
由社區包electron-wix-msi支援.
Windows 8開始支援的UWP格式, 可發布到Microsoft Store.
electron-builder可使用MakeAppx工具將程序打包為appx.
微軟於2018年開始支援的主要格式, 意在替代MSI, 用於Windows 10以上系統, 可發布到Microsoft Store.
  • Electron社區還不存在MSIX的打包方案.
  • 打包解決方案提供商Advanced Installer支援將Electron項目打包為MSIX.
  • 微軟官方提供的MSIX Packaging Tool可用來將EXE或MSI安裝包重新打包為MSIX:
    https://docs.microsoft.com/en-us/windows/msix/packaging-tool/create-app-package
Electron有一個官方維護的基於ChromeDriver的測試庫Spectron, 但最終由於缺乏維護者於2022年初被棄用.
Spectron的潛在替代方案是Playwright.
然而, 截止Playwright v1.16, 基於Playwright的測試仍然是實驗性功能, 並且在很大程度上不可用:
  • 無法調試錯誤
  • evaluate不支援導入Node.js模組
一個用於創建視窗覆蓋層的庫.
一站式開發和打包Electron程序.
一站式打包和分發Electron程序.
文件不一致到了讓人覺得可悲的地步.
Electron使用與Node.js不同版本的ABI, 因此無法直接使用npm和yarn以當前Node.js版本為目標下載的native模組,
electron-rebuild是解決這一問題的工具.
出於可維護性方面的考慮, 不建議使用需要electron-rebuild的native模組.
  • electron-rebuild無法選擇正確的ABI版本, 這使得依賴electron-rebuild的模組可能發展為一個難以升級的依賴項.
    https://github.com/electron/electron-rebuild/issues/886
  • 儘管屬於官方倉庫, electron-rebuild卻經常不能跟上Electron的最新版本.