Electron App 的初步優化


最近用 React + Electron + Ant Design 開發了一個 app. 經過一番折騰,雖能 build 出來能正常運行,但碰到兩個問題影響到了 app 的性能:

  1. 功能簡單的程序竟有三百多兆容量——需要瘦身。
  2. React 程序如何正確調用 Electron 的 NodeJS 模塊功能。

下面記錄一下解決過程。

瘦身

搜了一下,貌似沒有辦法通過剪裁 Electron 中 Chromium 各種無用功能來減少 app 體積。要不就用別人做的魔改版——並不是很想。終於,在 stackoverflow 站找到了良法:

I managed to reduce the final size of my mac app from 250MB to 128MB by moving 'electron' and my reactJs dependencies to devDependencies in package.json ... since all I need is going to be in the final bundle.js

確實,很多按常理放在 dependencies 的東西,其實都可以放到 devDependencies 中去,因為 react-scripts 會通過 Webpack 把在 React 項目中用到的各種庫都打包壓縮到 build 文件夾里,之后讓 Electron 根據 dependencies 所列再打包一遍,毫無意義,build 根本不會用到。照帖中所說,騰挪之后,用 electron-builder 打包,速度快了不少,打包后,程序大小從 368MB 銳減至 185MB,安裝包更是只有 57.4MB。絕了,這數字讓減肥廚狂喜。

瘦身后,運行程序,一切正常。瘦身成功。

渲染進程與主進程的通信

對於 Electron desktop app 而言,如果不能讓“網頁”調用到 node 模塊的功能,那 Electron 的包裝就失去了意義。在 app 中,React 網頁在渲染進程,Electron 和 node 模塊在主進程,如何連接兩者是關鍵。

Bad

起初,我用了一種比較笨的辦法,讓 React 直接調用 electron 主進程的對象。方法是這樣的,首先在配置窗口屬性的時候加入:

webPreferences: {
  nodeIntegration: true,
  contextIsolation: false,
  preload: path.join(__dirname, 'preload.js')
}

nodeIntergration 開啟后,使 Electron 集成了 node 的功能,可以使用 require 等方法了。 [contextIsolation](https://www.electronjs.org/docs/tutorial/context-isolation) 關閉后,渲染進程和主進程就有了共同的上下文對象。

簡而言之,預加載腳本 (preload.js) 和渲染進程可以共用 window 對象。比如,我在 preload.js 中可以這樣寫:

const fs = require('fs-extra');
window.fs = fs;

然后在 React 代碼中就可以通過 window 調用到 fs 了:

// React scripts
// const fs = require('fs-extra'); // ✖️
const fs = window.fs; // ✔️

這樣確實很方便,於是我把很多要用到的 node 模塊都放在 preload.js 去加載,一路開發下去,暢通無阻。但是,我忽然發現,React 如果用到讀取文件的方法時,只能在第一次渲染出來時起作用,用 Electron app 的 reload 重新加載頁面,就不會再讀取。這導致我為了調試,只能關掉 electron 腳本重開,才能正確讀取到本地自定義的配置文件。而且,如果 preload 過多地通過 require() 加載模塊,也會影響程序的啟動速度。最重要地,官方也不推薦這種做法:Do not enable Node.js Integration for Remote Content, Enable Context Isolation for Remote Content.

出於安全的考慮,Electron 官方希望我們關閉 node 的集成並使用獨立的上下文:

webPreferences: {
  nodeIntegration: false, // default
  contextIsolation: true, // default
  preload: path.join(__dirname, 'preload.js')
}

取而代之的,是讓我們通過 API 接口去進行渲染與底層的通信——也是前后端分離的思想。

Good

我在 stackoverflow 找到一個不錯的實踐方式,簡述如下。

首先,在入口腳本處 (main.js) 定義調用 node 模塊的各種方法:

// main.js
const { app, BrowserWindow, ipcMain };
const fs = require('fs');

let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({
    //...
  });
}

app.on('ready', createWindow);

ipcMain.on('toMain', (event, args) => {
  fs.readFile('path/to/file', (error, data) => {
    // 用 node 的 fs 模塊進行文件處理。
    // response = ...
    // 將結果發送給 webContents,等待渲染進程去獲取。
    mainWindow.webContents.send('fromMain', response);
  });
});

其中,'toMain' 是渲染進程向主進程發送請求的頻道名,'fromMain' 是渲染進程從主進程取回響應的頻道名。如何在渲染進程和主進程之間架起溝通的橋梁,是在預加載腳本中要做的事:

const { contextBridge, ipcRenderer } = require('electron');

// 只暴露 API 方法,不暴漏完整對象。
contextBridge.exposeInMainWorld(
  'api', {
    send: (channel, data) => {
      // 注冊請求名。
      let validChannels = ['toMain'];
      if (validChannels.includes(channel)) {
        // ipcRenderer 向主進程發送請求。
        ipcRenderer.send(channel, data);
      }
    },
    receive: (channel, func) => {
      // 注冊響應名。
      let validChannels = ['formMain']; 
      if (validChannels.includes(channel)) {
        // 渲染進程通過 channel 名調用接收方法,從主進程拿到響應內容,再用自己的方法進行處理。
        ipcRenderer.on(channel, (event, ...args) => func(...args));
      }
    }
  }
);

預加載腳本只需加載“發送”和“接收”兩種方法即可,這樣啟動時的加載時間就減少了。

最后在 React 渲染進程的腳本中是這樣調用的:

const loadConfig = (setFunction) => {
  // 向主進程發送請求。主進程通過 .webContents.send() 將響應結果發送給 fromMain 處理。
  window.api.send('inMain');

  // 通過 fromMain 接收響應結果,然后對其進行處理。
  window.api.receive('fromMain', result => {
    if (typeof setFunction !== 'function') {
      return;
    }
    setFunction(result);
  });
};

整個通信過程有點像:組織上 (preload) 規定了我獲取情報的方式,然后我 (renderer) 通過暗號 ("toMain") 向 (send) 某部門 (main) 要情報,某部門搞定后,將情報放在某個地方,讓我用口令 ("fromMain") 去取 (receive),取回來之后我再另作處理……

雖然看上去有點復雜,但多實操幾遍,就能領悟其中道理,進而逐漸體會到此法之妙。改了之后,之前遇到的 reload 無法再次加載配置文件的問題就自然解決了,運行 app 也感覺流暢了許多。

我想我的 Electron 開發之路也算是入門了吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM