Electron主进程与渲染进程

546 阅读5分钟

一、基本概念

Electron 继承了来自 Chromium 的多进程架构,这使得此框架在架构上非常相似于一个现代的网页浏览器,而网页浏览器除了展示网页之外还需要负责管理众多窗口 ( 或 标签页 ) 和加载第三方扩展等其他事务

因此让每个标签页在独立进程中渲染, 从而限制了一个网页上的有误或恶意代码可能导致的对整个应用程序造成的伤害

然后用单个浏览器进程控制这些标签页进程,以及整个应用程序的生命周期

Chrome的多进程架构

同样的,Electron程序中也如此划分,将进程划分为两类:主进程和渲染进程

1.1主进程

每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点

主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。

可以理解为:

Electron 中,启动项目时运行的 main.js 脚本就是我们说的主进程。在主进程运行的脚本可以以创建 web 页面的形式展示 GUI

一个 Electron 应用有且只有一个主进程。并且创建窗口等所有系统事件都要在主进程中进行

1.2渲染进程

每个 Electron 应用都会为每个打开的 BrowserWindow ( 与每个网页嵌入 ) 生成一个单独的渲染器进程

每个 web 页面都运行在它自己的渲染进程中,每个渲染进程是独立的,它只关心它所运行的页面

1.3Preload脚本

Preload脚本包含了那些执行于渲染器进程中,且于网页内容开始加载的代码

这些脚本虽运行于渲染器的环境中,却因能访问 Node.js API 而拥有了更多的权限

预加载脚本可以在 BrowserWindow 构造方法中的 webPreferences 选项里被附加到主进程

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

虽然预加载脚本与其所附着的渲染器在共享着一个全局 window 对象,但您并不能从中直接附加任何变动到 window 之上,因为存在Context Isolation(语境隔离)

因此只能使用contextBridge方法进行安全的语境交互:

// preload.js
const { contextBridge } = require('electron')
​
contextBridge.exposeInMainWorld('myAPI', {
  desktop: true,
})
// renderer.js
console.log(window.myAPI)
// => { desktop: true }

此功能能够起到一个非常关键的作用:通过暴露 ipcRenderer 帮手模块于渲染器中,使用进程间通讯 ( inter-process communication, IPC ) 来从渲染器触发主进程任务 ( 反之亦然 )

二、进程通信

Electron中进程使用ipcMain和ipcRenderer模块,通过定义的Channel进行进程间的通信

下面是常用的三种模式

2.1渲染进程到主进程

要将单向 IPC 消息从渲染器进程发送到主进程,可以使用ipcRenderer.send发送消息,并在ipcMain.on进行消息接收

通常使用此模式从 Web 内容调用主进程 API

举例如下:

/* 主进程main.js */
const mainWindow = new BrowserWindow({
  webPreferences: {
    preload: path.join(__dirname, 'preload.js')
  }
})
​
// 启动ipcMain对消息进行接受和监听
ipcMain.on('set-title', (event, title) => {
  const webContents = event.sender
  const win = BrowserWindow.fromWebContents(webContents)
  win.setTitle(title)
})
/* preload.js */
contextBridge.exposeInMainWorld('electronAPI', {
  setTitle: (title) => ipcRenderer.send('set-title', title)
})
<script src="./renderer.js"></script>
/* renderer.js */
window.electronAPI.setTitle(title)

可以理解为主进程通过createWindow方法通过webPreferences加载preload.js预渲染脚本并启动渲染进程,preload.js中通过contextBridge方法给渲染进程定制了一个electronAPI对象并绑定在window对象上,渲染进程加载后能够在window对象上获取这个electronAPI对象,并调用定义在上面的方法(该方法定义了一个ipcRenderer管道,方便渲染进程与主进程进行通信),渲染进程即可以调用该管道与主进程进行通信

image-20230425175316862

2.2双向通信

双向 IPC 的一个常见应用是从渲染器进程代码调用主进程模块并等待结果。 这可以通过将ipcRenderer.invokeipcMain.handle搭配使用来完成

  • ipcRenderer.invoke()
  • ipcRenderer.handle()
/* main.js */
webPreferences: {
  preload: path.join(__dirname, 'preload.js')
}
ipcMain.handle('dialog:openFile', handleFileOpen)
/* preload.js */
contextBridge.exposeInMainWorld('electronAPI', {
  openFile: () => ipcRenderer.invoke('dialog:openFile')
})
<!DOCTYPE html>
<script src='./renderer.js'></script>
/* renderer.js */
const filePath = await window.electronAPI.openFile()

image-20230425175514609

2.3主进程到渲染器进程

将消息从主进程发送到渲染器进程时,需要指定是哪一个渲染器接收消息

消息需要通过其WebContents实例发送到渲染器进程。 此 WebContents 实例包含一个send方法,其使用方式与 ipcRenderer.send 相同

/* main.js */
// 反馈监听器
ipcMain.on('counter-value', (_event, value) => {
  console.log(value)
})
// 发送目标
mainWindow.webContents.send('update-counter', 1)
/* preload.js */
contextBridge.exposeInMainWorld('electronAPI', {
  handleCounter: (callback) => ipcRenderer.on('update-counter', callback)
})
<script src="./renderer.js"></script>
// 接收数据
window.electronAPI.handleCounter((event, value) => {
  const oldValue = Number(counter.innerText)
  const newValue = oldValue + value
  counter.innerText = newValue
  // (可选,发送反馈,触发main的ipcMain.on)
  event.sender.send('counter-value', newValue)
})

image-20230425182618583

2.4渲染进程到渲染进程

没有直接的方法可以使用 ipcMainipcRenderer 模块在 Electron 中的渲染器进程之间发送消息,但是能够采取以下两种方式完成渲染进程间的通信:

  • 将主进程作为渲染器之间的消息代理,将消息从一个渲染进程发送到主进程,然后主进程将消息转发到另一个渲染进程
  • 从主进程将一个MessagePort传递到两个渲染进程,在初始设置后渲染进程之间直接进行通信

image-20230426102548219

三、contextBridge使用方法

contextBridge方法的作用是在隔离的上下文中创建一个安全的、双向的、同步的桥梁

contextBridge模块有以下方法:

  • exposeInMainWorld(apiKey, api)

    • apiKey是一个字符串类型数据,将API注入到window的键,通过 window[apiKey] 访问的方式访问api
    • api 可以是FunctionstringnumberArrayboolean等类型,在API中发送的任何数据/原始数据将不可改变,在桥接器其中一侧的更新不会导致另一侧的更新
  • exposeInIsolateWorld(worldId, apiKey, api)

    • worldId是一个字符串类型数据,表示要注入 API 的 world 的 ID。 0 是默认 world,999 的 world 被 Electron 的 contextIsolation 使用。 使用 999 would 为 preload 上下文暴露对象。 我们建议使用 1000+ 来创建隔离的 world
    • apiKey 是一个字符串类型数据,将 API 注入到 window 的键,通过 window[apiKey] 访问的方式访问api