likes
comments
collection
share

Electron入门及原理浅析

作者站长头像
站长
· 阅读数 20

原理

Electron通过将Chromium和Node.js合并到同⼀个运⾏时环境中,并将其打包为Mac, Windows和Linux系统下的应⽤来实现这⼀⽬的。

Electron本质就是提供了一个浏览器的壳子,用于运行我们的web应用,但是我们的代码具有更强大的功能。 JavaScript 可以访问文件系统,用户 shell 等。 这允许您构建更高质量的本机应用程序,但是内在的安全风险会随着授予您的代码的额外权力而增加。同时也内置了Nodejs环境,因此我们的页面也可以调用node的api。

Electron入门及原理浅析

进程

两种进程:主进程和渲染进程(类似于浏览器,一个tab窗口就是一个渲染进程控制,然后会有一个主进程管理所有的这些渲染进程)

Electron入门及原理浅析

主进程

每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。

窗口管理

主进程的主要目的是使用 BrowserWindow 模块创建和管理应用程序窗口。

BrowserWindow 类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。 您可从主进程用 window 的 webContent 对象与网页内容进行交互。

const { BrowserWindow } = require('electron')

const win = new BrowserWindow({ width: 800, height: 1500 })
win.loadURL('https://github.com')

const contents = win.webContents
console.log(contents)

由于 BrowserWindow 模块是一个 EventEmitter, 所以您也可以为各种用户事件 ( 例如,最小化 或 最大化您的窗口 ) 添加处理程序。

当一个 BrowserWindow 实例被销毁时,与其相应的渲染器进程也会被终止。

生命周期

Electron 生命周期介绍

原生api

为了使 Electron 的功能不仅仅限于对网页内容的封装,主进程也添加了自定义的 API 来与用户的作业系统进行交互。 Electron 有着多种控制原生桌面功能的模块,例如菜单、对话框以及托盘图标。

关于 Electron 主进程模块的完整列表,请参阅我们的 API 文档。

职责

  • 创建渲染进程(可多个)
  • 控制了应⽤⽣命周期(启动、退出APP以及对APP做⼀些事件监听)
  • 调⽤系统底层功能、调⽤原⽣资源
  • 可调⽤的API:
    • Node.js API
    • Electron提供的主进程API(包括⼀些系统功能和Electron附加功能) electron主进程api

渲染器进程

每个 Electron 应用都会为每个打开的 BrowserWindow ( 与每个网页嵌入 ) 生成一个单独的渲染器进程。 洽如其名,渲染器负责 渲染 网页内容。 所以实际上,运行于渲染器进程中的代码是须遵照网页标准的 (至少就目前使用的 Chromium 而言是如此) 。

此外,这也意味着渲染器无权直接访问 require 或其他 Node.js API。 为了在渲染器中直接包含 NPM 模块,您必须使用与在 web 开发時相同的打包工具 (例如 webpack 或 parcel)。(渲染进程理解为就是一个浏览器环境,所以我们的web代码是要经过编译打包才能正常运行)

职责:

  • ⽤HTML和CSS渲染界⾯
  • ⽤JavaScript做⼀些界⾯交互
  • 可调⽤的API:

预加载脚本

预加载(preload)脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 。 这些脚本虽运行于渲染器的环境中,却因能访问 Node.js API 而拥有了更多的权限。

主进程:

const { BrowserWindow } = require('electron')
//...
const win = new BrowserWindow({
  webPreferences: {
    preload: 'path/to/preload.js'
  }
})

由于预加载脚本与渲染器共享同一个全局 Window 接口,并且可以访问 Node.js API,因此它通过在 window 全局中暴露任意您的网络内容可以随后使用的 API 来增强渲染器。

上下文隔离

上下文隔离是一项功能,可确保您的 preload 脚本和Electron的内部逻辑都在与您在 webContents 中加载的网站不同的上下文中运行。这对确保安全性很重要,因为它有助于防止网站访问electron内部或您的预加载脚本可以访问的强大API。

虽然预加载脚本与其所附加的渲染器在全局共享着一个 window 变量,但您并不能从中直接附加任何变数到 window 之中,因为 上下文隔离 是默认的。

上下文隔离 意味着预加载脚本与渲染器的主要运行环境是隔离开来的(虽然都是在渲染器环境中运行),以避免泄漏任何具特权的 API 到您的网页内容代码中,同理,也避免在网页修改了全局变量会影响预加载脚本。(目的就是为了避免网页和预加载脚本的相互影响,限制网页的权限)

取而代之,我们可以通过 contextBridge 模块来安全地实现交互

// 预加载脚本
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld(
  'electron',
  {
    doThing: () => ipcRenderer.send('do-a-thing')
  }
)

// 渲染进程
window.electron.doThing()

进程通信

  1. LocalStorage, window.postMessage

在前端开发中,鉴于浏览器对本地数据有严格的访问限制,所以一般通过该两种方式进行窗口间的数据通讯,该方式同样适用于 Electron 开发中。然而因为 API 设计目的仅仅是为了前端窗口间简单的数据传输,大量以及频繁的数据通讯会导致应用结构松散,同时传输效率也值得怀疑。

  1. IPC (Inner-Process Communication)

Electron 中提供了 ipcRender 、ipcMain 作为主进程以及渲染进程间通讯的桥梁,该方式属于 Electron 特有传输方式,不适用于其他前端开发场景。Electron 沿用 Chromium 中的 IPC 方式,不同于 socket、http 等通讯方式,Chromium 使用的是命名管道 IPC ,能够提供更高的效率以及安全性。

  1. Remote( 不建议使用

Remote 模块为渲染进程和主进程通信提供了⼀种简单⽅法。 在Electron中, GUI 相关的模块 (如 dialog、menu 等) 仅在主进程中可⽤, 在渲染进程中不可 ⽤。 为了在渲染进程中使⽤它们, ipc 模块是向主进程发送进程间消息所必需的。 使⽤ remote 模块, 你可以调⽤ main 进程对象的⽅法, ⽽不必显式发送进程间消息 。

remote模块要慎用。Electron团队为什么要干掉remote模块

// 主进程
global.sharedObject = {
 someProperty: 'default value'
}
// 渲染进程1
require('electron').remote.getGlobal('sharedObject').someProperty = 'new value'
// 渲染进程2
console.log(require('electron').remote.getGlobal('sharedObject').someProperty)

注意事项

模块加载

在渲染进程当中,使用node模块不能使用import方式引用。因为渲染进程属于浏览器端没有node的环境,就算集成了node的环境,import是会被webpack等构建工具解析作为node_modules依赖被打包进来。

  • 方法一:需要使用require并且在主进程开启nodeIntegration:
// 渲染进程
const { ipcRenderer } = window.require('electron');

// 主进程
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
    },
  });
  • 方法二:修改webpack配置,target设置为electron-renderer, 如果是vue.config.js:
module.exports = {
  chainWebpack: (config) => {
    config.target('electron-renderer');
  },
};

然后在渲染进程直接使用import来引用我们的模块即可

谨慎地加载模块

我们需要谨慎加载 npm 模块,因为一个模块可能包含了超出实际所需的功能,而 require 模块消耗的时间相当可观。

加载模块是令人吃惊的繁重的操作,尤其是在Windows上。 当你的应用开始,不应该让用户等待当时不需要的操作。

优化手段:

  • 打包压缩代码
  • tree-shaking
  • 延迟加载,初始化时只加载重要模块
  • 减少不必要的依赖活模块
  • 减少磁盘io

......

避免阻塞主进程

主进程是你应用的所有其他进程的父进程,也是和操作系统交互的关键进程。在任何情况下你都不应阻塞此进程或者运行时间长的用户界面线程。 阻塞UI线程意味着您的整个应用程序将冻结直到主进程准备好继续处理。

手段:

  • 多进程处理cpu密集型
  • 避免使用同步ipc和remote模块
  • node核心模块,使用异步的方法

......

不要为远程内容启用 Node.js 集成

不管是使用BrowserWindowBrowserView 或者 <webview>,最重要的就是绝对不要启用 Node.js 集成。 其目的是限制您授予远程内容的权限, 从而使攻击者在您的网站上执行 JavaScript 时难以伤害用户。

无可避免的白屏

即使 Electron 通常从本地文件系统加载 JavaScript 代码,没有网络加载延迟,我们还是需要继续和页面白屏做斗争,因为 JavaScript 等资源的加载、解析和执行还是有相当大的代价。作为一个桌面端应用,细微的白屏延迟用户都可以感觉的到。我们要尽量让用户感觉不到这是一个 Web 页面。

我们可以采用常规的优化web项目手段去优化我们的electron应用

IPC通信的优化

涉及到多页面/窗口的 Electron 应用,IPC 会非常频繁,搞不好会成为性能瓶颈。

而且要避免使用同步ipc通信,同步的ipc通信会阻塞主进程/渲染进程。

参考资料

Electron 渲染进程之间的通信

Electron 进程间通讯详解

electron官方文档