Electron进程间通信

Preload 脚本

electron 主进程运行在 Node 环境中,渲染器进程运行在浏览器环境中,正常情况下,渲染器进程访问不了 Node.js API。而预加载(preload)脚本运行于渲染器进程中(自然能访问 window document 等 Web API),却能够访问 Node.js API 和 Electron API,这也为主进程和渲染器进程之间的通信奠定了基础。
预加载脚本可以在 BrowserWindow 构造方法中的 webPreferences 选项里被附加到主进程:

1
2
3
4
5
6
7
8
9
const { BrowserWindow } = require("electron");
const path = require("path");
// ...
const win = new BrowserWindow({
webPreferences: {
preload: path.resolve(__dirname, "./preload.js")
}
})
// ...

我们可以在预加载脚本中同时访问 Web API 和 Node.js API,例如在页面上打印 node、chrome、electron 的版本号:

1
2
3
4
5
6
7
8
// preload.js

window.addEventListener('DOMContentLoaded', () => {
for (const item of ["node", "chrome", "electron"]) {
const dom = document.getElementById(`version-${item}`);
dom.innerText = process.versions[item];
}
})

这种方式有局限性,就是无法与界面进行交互。那可不可以把之后需要在渲染进程中用到的 Nodejs API 暴露出来呢?是可以的。

预加载脚本与其所附着的渲染器共享一个全局 window 对象。

这样的话,我们就可以将需要用到的 Node.js API 挂载到 window 对象上,以供后续使用。

1
2
3
4
5
// preload.js

window.myAPI = {
desktop: true
}

这时在渲染器进程中打印 window.myAPI 会输出 undefined
这是上下文隔离(Context Isolation)造成的,BrowserWindow 构造方法中的 webPreferences 选项里的 contextIsolation 默认开启,这意味着预加载脚本与渲染器的主要运行环境是隔离开来的,以避免泄漏任何特权 API 到您的网页内容代码中。
我们可以通过 contextBridge 模块来解决这一问题。

1
2
3
4
5
6
7
8
9
// preload.js

const { contextBridge } = require("electron");

contextBridge.exposeInMainWorld("versions", {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron
});

这时,versions 会被挂载到渲染器进程的 window 对象上,可在渲染器进程中直接使用:

1
window.versions.node

进程间通信

在 Electron 中,进程使用 ipcMain 和 ipcRenderer 模块,通过开发人员定义的“通道”传递消息来进行通信。
根据字面意思,主进程使用 ipcMain 模块收发消息,渲染器进程使用 ipcRenderer 模块收发消息,但是渲染器进程无法直接访问到 ipcRenderer 模块,这时,预加载脚本就发挥作用了,我们可以在预加载脚本中向渲染器进程暴露收发消息的方法,以供渲染器进程使用。

1 渲染器进程到主进程(单向)

渲染器进程通过 ipcRenderer.send 发送消息,主进程通过 ipcMain.on 来接收消息。

1
2
3
4
5
6
7
// preload.js

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

contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
1
2
3
4
5
6
// 渲染器进程

const setButton = document.getElementById('btn')
setButton.addEventListener('click', () => {
window.electronAPI.setTitle("hello world")
})
1
2
3
4
5
6
7
// 主进程

ipcMain.on('set-title', (event, title) => {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win.setTitle(title)
})

主进程代码中 event.sender 是发送消息的渲染器进程的上下文,然后通过上下文找到对应的浏览器窗口,再去设置窗口的标题。


2 渲染器进程到主进程(双向)

渲染器进程通过 ipcRenderer.invoke 发送消息,主进程通过 ipcMain.handle 来接收消息。
ipcRenderer.invoke 会返回一个 Promise,其 fullfilled 的结果是 ipcMain.handle 处理函数的返回值。

1
2
3
4
5
6
7
// preload.js

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

contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
1
2
3
4
5
6
7
8
9
// 渲染器进程

const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')

btn.addEventListener('click', async () => {
const filePath = await window.electronAPI.openFile()
filePathElement.innerText = filePath
})
1
2
3
4
5
6
7
8
// 主进程

ipcMain.handle('dialog:openFile', () => {
const { canceled, filePaths } = await dialog.showOpenDialog()
if (!canceled) {
return filePaths[0]
}
});

3 主进程到渲染器进程

主进程通过 new WebContents().send 方法发送消息,渲染器进程通过 ipcRenderer.on 接收消息。

1
2
3
4
5
6
7
// preload.js

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

contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value))
})

这里暴露了一个监听器 onUpdateCounter,其参数是一个回调函数,回调函数的参数就是主进程发送的消息。

1
2
3
4
5
6
7
8
// 主进程

const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.webContents.send('update-counter', 1)
1
2
3
4
5
// 渲染器进程

window.electronAPI.onUpdateCounter((value) => {
console.log(value) // 1
})

主进程到渲染器进程不是双向的,但可以通过在 ipcRenderer.on 的回调中主动通过 ipcRenderer.send 向主进程发送消息。


对象序列化

Electron 的 IPC 实现使用 HTML 标准的结构化克隆算法(JSON 序列化)来序列化进程之间传递的对象,这意味着只有某些类型的对象可以通过 IPC 通道传递。