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
|
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
|
window.myAPI = { desktop: true }
|
这时在渲染器进程中打印 window.myAPI
会输出 undefined
。
这是上下文隔离(Context Isolation)造成的,BrowserWindow
构造方法中的 webPreferences
选项里的 contextIsolation
默认开启,这意味着预加载脚本与渲染器的主要运行环境是隔离开来的,以避免泄漏任何特权 API 到您的网页内容代码中。
我们可以通过 contextBridge
模块来解决这一问题。
1 2 3 4 5 6 7 8 9
|
const { contextBridge } = require("electron");
contextBridge.exposeInMainWorld("versions", { node: () => process.versions.node, chrome: () => process.versions.chrome, electron: () => process.versions.electron });
|
这时,versions
会被挂载到渲染器进程的 window 对象上,可在渲染器进程中直接使用:
进程间通信
在 Electron 中,进程使用 ipcMain 和 ipcRenderer 模块,通过开发人员定义的“通道”传递消息来进行通信。
根据字面意思,主进程使用 ipcMain 模块收发消息,渲染器进程使用 ipcRenderer 模块收发消息,但是渲染器进程无法直接访问到 ipcRenderer 模块,这时,预加载脚本就发挥作用了,我们可以在预加载脚本中向渲染器进程暴露收发消息的方法,以供渲染器进程使用。
1 渲染器进程到主进程(单向)
渲染器进程通过 ipcRenderer.send
发送消息,主进程通过 ipcMain.on
来接收消息。
1 2 3 4 5 6 7
|
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
|
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
|
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) })
|
主进程到渲染器进程不是双向的,但可以通过在 ipcRenderer.on
的回调中主动通过 ipcRenderer.send
向主进程发送消息。
对象序列化
Electron 的 IPC 实现使用 HTML 标准的结构化克隆算法(JSON 序列化)来序列化进程之间传递的对象,这意味着只有某些类型的对象可以通过 IPC 通道传递。