zoukankan      html  css  js  c++  java
  • 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 开发之路也算是入门了吧。

  • 相关阅读:
    npm依赖版本变动引发的惨案
    Flutter ListTile
    操作系统的发展史(科普章节)
    操作系统的发展史(科普章节)
    如何在电脑上保存微信公众号文章封面图片?
    如何在电脑上保存微信公众号文章封面图片?
    操作系统(科普章节)
    操作系统(科普章节)
    前端面试之前要准备的那些事
    前端面试之前要准备的那些事
  • 原文地址:https://www.cnblogs.com/seesawgame/p/14592534.html
Copyright © 2011-2022 走看看