likes
comments
collection
share

Electron进程间通信小结

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

前言

本文章测试时依赖的的vue版本为 2.6.10,vue-cli版本为 3.12.1,node版本为 v14.17.5,electron 版本 11.0.0及12.0.0。

※注:本文代码区域每行开头的“+”表示新增,“-”表示删除,“M”表示修改;代码中的“...”表示省略。

1 进程模型

首先介绍一下Electron中的进程模型,Electron 继承了来自 Chromium 的多进程架构,这使得此框架在架构上非常相似于一个现代的网页浏览器。

1.1 为什么不是一个单一的进程

网页浏览器是个极其复杂的应用程序。 除了显示网页内容的主要能力之外,他们还有许多次要的职责,例如:管理众多窗口 ( 或 标签页 ) 和加载第三方扩展。在早期,浏览器通常使用单个进程来处理所有这些功能。 虽然这种模式意味着您打开每个标签页的开销较少,但也同时意味着一个网站的崩溃或无响应会影响到整个浏览器

1.2 多进程模型

Chrome 团队决定让每个标签页在自己的进程中渲染, 从而限制了一个网页上的有误或恶意代码可能导致的对整个应用程序造成的伤害。 然后用单个浏览器进程控制这些标签页进程,以及整个应用程序的生命周期。

来自 Chrome 漫画 的图表可视化了此模型:

Electron进程间通信小结

Electron 应用程序的结构非常相似。 作为应用开发者,您控制着两种类型的进程:主进程和渲染器。 这些类似于上面概述的 Chrome 自己的浏览器和其渲染器进程

1.3 主进程

1.3.1 概念

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

1.3.2 窗口管理

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

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

main.js:

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 实例被销毁时,与其相应的渲染器进程也会被终止

1.3.3 应用程序生命周期

主进程还能通过 Electron 的 app 模块来控制您应用程序的生命周期。 该模块提供了一整套的事件和方法,可以使你添加自定义的应用程序行为 ( 例如:以编程方式退出您的应用程序、修改程序坞或显示关于面板 )

用 app API 创建了一个更原生的应用程序窗口体验,实例如下:

main.js:

// quitting the app when no windows are open on non-macOS platforms
​
app.on('window-all-closed', () => {
        if (process.platform !== 'darwin') app.quit()
})

1.3.4 原生 API

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

关于 Electron 主进程模块的完整列表,请 API 文档:www.electronjs.org/zh/docs/lat…

1.4 渲染器进程

1.4.1 概念

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

1.4.2 使用限制

渲染器无权直接访问 require 或其他 Node.js API。 为了在渲染器中直接包含 NPM 模块,您必须使用与在 web 开发时相同的打包工具 (例如 webpack 或 parcel)

注意: 渲染器进程可以生成一个完整的 Node.js 环境以便于开发。 在过去这是默认的,但如今此功能考虑到安全问题已经被禁用

1.4.3 怎样使用nodejs 和 electron 原生桌面功能

渲染器进程用户界面怎样才能与 Node.js 和 Electron 的原生桌面功能进行交互。 而事实上,确实没有直接导入 Electron 內容脚本的方法

其实在 Electron 12.0.0 开始虽然出于安全考虑,配置中的 contextIsolation 值默认就是 true,从而在渲染进程不能直接使用 nodejs。但是我在 Electron 12.0.0 中通过配置 nodeIntegration:true 及 contextIsolation:false 后就可以在渲染进程中使用nodejs了。

1.4.4 Preload 脚本

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

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

main.js:

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

因为预加载脚本与浏览器共享同一个全局 Window 接口,并且可以访问 Node.js API,所以它通过在全局 window 中暴露任意 API 来增强渲染器,以便你的网页内容使用

  1. 无效方式:直接在 window 添加

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

preload.js

window.myAPI = {
  desktop: true
}

renderer.js

console.log(window.myAPI)
// => undefined

原因:

语境隔离(Context Isolation)意味着预加载脚本与渲染器的主要运行环境是隔离开来的,以避免泄漏任何具特权的 API 到您的网页内容代码中。
  1. 正确方式:使用 contextBridge 模块来安全地实现

preload.js

const { contextBridge } = require('electron')
​
contextBridge.exposeInMainWorld('myAPI', {
  desktop: true
})

renderer.js

console.log(window.myAPI)
// => { desktop: true }

这里只是一个很简单的例子,更加详细的请参考官方文档:www.electronjs.org/zh/docs/lat…

2 主进程和渲染进程通信

2.1 渲染进程主动给主进程发送消息

2.1.1 渲染进程给主进程发送消息

相当于:渲染进程调用主进程方法

渲染进程:

const {ipcRenderer} = require('electron')     
ipcRenderer.send('msg', '我是渲染进程传递过来的数据')

主进程:

const { ipcMain } = require('electron')
ipcMain.on('msg', (e, data) => {
  console.log(e, data);
})

2.1.2 渲染进程给主进程发送异步消息,主进程接收到异步消息以后通知渲染进程

相当于:渲染进程调用主进程方法,再在主进程方法中调用渲染进程方法

渲染进程:

ipcRenderer.send('msg', '我是渲染进程给主进程发送的异步消息')

ipcRenderer.on('reply', (e, data) => {
  console.log(data);
})

主进程:

方法一:

ipcMain.on('msg', (e, data) => {
  e.sender.send('reply', '我先被渲染进程调用,然后调用渲染进程中的 reply 事件绑定的监听函数')
})

此方法将总是把消息发送到主 frame

e.sender:返回发送消息的 webContents

方法二:

ipcMain.on('msg', (e, data) => {
  e.reply('reply', '我先被渲染进程调用,然后调用渲染进程中的 reply 事件绑定的监听函数')
})

此方法将自动处理从非主 frame 发送的消息(比如: iframes)

e.reply:将 IPC 消息发送到渲染器框架的函数,该渲染器框架发送当前正在处理的原始消息。 您应该使用“reply”方法回复发送的消息,以确保回复将转到正确的进程和框架。

2.1.3 渲染进程给主进程发送同步消息

渲染进程:

sendMsgSync.addEventListener('click', () => {
  // 同步方式调用 主进程中的方法 sendMsgReplySync, 返回值mainReplyValue为主进程中的 e.returnValue 的值
 const mainReplyValue = ipcRenderer.sendSync('sendMsgReplySync', 'this is renderer msg - sendMsgReplySync')
})

主进程:

ipcMain.on('sendMsgReplySync', (e, data) => {
  e.returnValue = '我是渲染进程中同步调用主进程方法 时的返回值'
})

官方建议: 出于性能原因,我们建议避免使用此API。它的同步特性意味着它将阻塞呈现程序进程,直到接收到应答

2.2 主进程主动给渲染进程发消息

主进程:

BrowserWindow.getFocusedWindow().webContents.send('rendererMsg', '主进程-主动触发渲染进程中的方法')

其中BrowserWindow.getFocusedWindow() 返回聚焦窗口实例对象,没有聚焦时它为空,此时可能报错。所以针对单页面程序来说:最好在主进程中直接用const win = new BrowserWindow(...) 中的 win.webContents.send(...) 比较保险。

渲染进程:

ipcRenderer.on('rendererMsg', (e, data) => {
  console.log(data);
})

3 渲染进程与渲染进程之间的通信

3.1 方法一

要传值的窗口: window.localStorage.setItem(key,value)

要接收值的窗口:window.localStorage.getItem(key)

3.2 方法二

渲染进程和渲染进程之间不能直接通信,需要借助主进程作为桥梁

渲染进程 index.html:

ipcRenderer.send('openNews', '123456')

主进程 ipcMain. js:

ipcMain.on('openNews', (e, data) => {
  // 将从 index.html 接收到的参数传递给 打开的 news.html 新窗口
  newsWindow.webContents.on('did-finish-load', () => {
    newsWindow.webContents.send('toNews', data)
  })
})

新渲染进程 news.html:

// 注册 toNews 方法,主进程中调用
ipcRenderer.on('toNews', (e, data) => {
  console.log(data)
})

4 更加安全的进程间通信方法

根据最新官方文档说明,直接在渲染进程中暴露 ipcRenderer 模块其实是有安全隐患的,更好的方法应该借助 Preload 脚本 及 contextBridge 模块来实现。可以参照 1.4.4 节中的例子,更加详细的案例请参考官方文档:www.electronjs.org/zh/docs/lat…

参考文献:www.electronjs.org

参考的electron官方文档版本:

Electron进程间通信小结