目标:
- IPC 是什么意思?
- 为何需要 IPC?
- Electron 将主进程和渲染进程分开有什么好处?
IPC 通信是什么?
在 Electron 中,IPC(Inter-Process Communication)进程间通信是一种机制,它允许主进程(Main Process)和渲染进程(Renderer Process)互相发送和接收消息,以实现信息交换和协同工作。
为什么需要 IPC 通信?
因为主进程和渲染进程有各自的职责和权限,IPC 允许这两种进程进行信息交换和协同工作,以实现应用的完整功能。
- 有些事儿,主进程能做,但是渲染进程不能做
- 有些事儿,主进程不能做,但是渲染进程能做
为什么 Electron 要将主进程和渲染进程分开呢?
因为这么设计,可以保证每个浏览器窗口(渲染进程、页面)的独立性和稳定性,同时也有利于提高应用程序的安全性(因为 只有主进程才能访问系统级别的资源和操作)。
Electron 架构中主进程对系统级别的资源和操作的独特访问权限主要是出于安全和稳定性的考虑:
- 安全性:限制对系统级别资源和操作的访问可以防止恶意代码或者攻击对系统级别资源造成破坏。例如,如果一个 Electron 应用的渲染进程被某种形式的恶意代码利用,那么该恶意代码的破坏范围将被限制在该渲染进程内,而无法直接对系统级资源造成更大的影响。
- 稳定性:将系统级操作限制在主进程中,可以防止渲染进程由于运行错误或者崩溃而影响到系统级别的操作。例如,如果一个渲染进程因为某种原因崩溃了,那么主进程和其他渲染进程可以继续运行,应用程序的其他部分不会受到影响。
实现 Electron 中的 IPC 通信
参考资料:
- Electron 官方进程间通信教程 👉🏻 https://www.electronjs.org/zh/docs/latest/tutorial/ipc
- Electron 官方 ipcMain 接口说明文档 👉🏻 https://www.electronjs.org/zh/docs/latest/api/ipc-main
- Electron 官方 ipcRenderer 接口说明文档 👉🏻 https://www.electronjs.org/zh/docs/latest/api/ipc-renderer
- Electron 社区 👉🏻 https://www.electronjs.org/zh/community
IPC 模式 1:渲染进程到主进程(单向)
- 了解 ipcRenderer.send、ipcMain.on 方法
- 掌握 ipcRenderer.invoke、ipcMain.handle 方法
开始介绍的是 👉🏻 ipcRenderer.send
,它出现得更早,是一种比较传统的用于实现 Electron 中进程间通信的方式。
重点掌握好 👉🏻 ipcRenderer.invoke
,实际开发中主要使用的是新版的 ipcRenderer.invoke
API 来实现进程间通信的。
(1)ipcRenderer.send
本节介绍的 ipcRenderer.send
,它出现得更早,是一种比较传统的用于实现 Electron 中进程间通信的方式。并不是很重要,实际开发中用得相对较少,不过我们还是有必要了解一下这种通信方式,起码要能够读懂程序。
源码 👉🏻 codes.zip
目标:掌握使用 ipcRenderer.send、ipcMain.on 实现从渲染进程到主进程的单向通信
{
"name": "0000",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "electron ."
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"electron": "^24.3.1"
}
}
"start": "electron ."
简单配置一下启动命令,以便后续启动 electron 应用程序
"electron": "^24.3.1"
当前时间安装的默认最新版 Electro 是 v24
const {app, BrowserWindow, ipcMain} = require('electron')
let win
function createWindow() {
win = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
win.webContents.openDevTools()
win.loadFile("./index.html")
}
function handleIPC() {
ipcMain.on('event1', (event, ...args) => {
console.log('receive message from renderer process', ...args)
})
}
app.on('ready', () => {
createWindow()
handleIPC()
})
这段代码主要包含了使用 Electron 创建一个新的浏览器窗口并处理来自渲染进程的 IPC 事件的过程:
const {app, BrowserWindow, ipcMain} = require('electron')
:这行代码首先引入了 Electron 模块,并解构了其中的app
、BrowserWindow
和ipcMain
对象。let win
:声明一个win
变量,这个变量将用于保存创建的浏览器窗口实例。function createWindow() {...}
:这是一个创建新窗口的函数。win = new BrowserWindow({ webPreferences: { nodeIntegration: true, contextIsolation: false }})
:创建一个新的BrowserWindow
实例,设置nodeIntegration
为true
,使得在渲染进程中可以使用 Node.js 的 API;contextIsolation
设置为false
,表示主进程和渲染进程共享同一个全局上下文。win.webContents.openDevTools()
:打开开发者工具。win.loadFile("./index.html")
:加载指定的 HTML 文件作为窗口的内容。
function handleIPC() {...}
:这是一个处理 IPC 事件的函数。ipcMain.on('event1', (event, ...args) => {...})
:使用ipcMain
对象监听event1
事件,当这个事件被触发时,输出从渲染进程接收到的消息。
app.on('ready', () => {...})
:当 Electron 完成初始化时,调用createWindow
和handleIPC
函数,创建新的窗口并开始处理 IPC 事件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>230519</title>
</head>
<body>
<h1>renderer process</h1>
<button id="btn">ipcRenderer.send('event1', 1, 2, 3)</button>
<script>
const { ipcRenderer } = require('electron')
document.getElementById('btn').addEventListener('click', () => {
console.log('123')
ipcRenderer.send('event1', 1, 2, 3)
})
</script>
</body>
</html>
这是一个简单的 HTML 页面,其中包含了一个标题(h1)和一个按钮(button)。这个 HTML 页面主要用于在 Electron 渲染进程中运行。
在页面的 <script>
标签中,有以下操作:
const { ipcRenderer } = require('electron')
:这行代码首先引入了 Electron 模块,并解构出其中的ipcRenderer
对象。ipcRenderer
是 Electron 提供的一个模块,它用于在渲染进程(即 Web 页面)中发送同步或异步消息给主进程,或者接收主进程的响应。document.getElementById('btn').addEventListener('click', () => {...})
:这行代码给页面上 id 为 ‘btn’ 的按钮元素添加了一个点击事件监听器。当用户点击这个按钮时,就会执行该事件监听器中的回调函数。console.log('123')
:在回调函数中,首先会输出 ‘123’ 到控制台。ipcRenderer.send('event1', 1, 2, 3)
:然后使用ipcRenderer
的send
方法向主进程发送一个名为 ‘event1’ 的消息,并传递了几个参数(1, 2, 3)。主进程可以监听这个事件,并在事件触发时获取这些参数。
总的来说,这个 HTML 页面主要是用来体验 Electron 中渲染进程和主进程之间的通信的。用户点击按钮后,渲染进程会向主进程发送一个 ‘event1’ 事件,主进程在收到这个事件后,可以进行相应的处理。
(2)ipcRenderer.invoke
基于上节的 demo 做了些许细微的修改,使用 ipcRenderer.invoke 实现单向的从渲染进程到主进程之间的单向 IPC 通信。ipcRenderer.invoke 是新版的 API,是比较重要的内容,目前大部分 Electron 应用中的 IPC 通信,都是使用它来实现的,并且官方也推荐我们使用新版的 API,尽量放弃旧版的 API。
源码:codes.zip
目标:掌握使用 ipcRenderer.send、ipcMain.on 实现从渲染进程到主进程的单向通信
const {
app,
BrowserWindow,
ipcMain
} = require('electron')
let win
function createWindow() {
win = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
win.webContents.openDevTools()
win.loadFile("./index.html")
}
const sleep = (duration) => new Promise((resolve) => setTimeout(resolve, duration))
function handleIPC() {
// ipcMain.on('event1', async (event, ...args) => {
// await sleep(3000)
// console.log('receive message from renderer process', ...args)
// })
ipcMain.handle('event1', async (event, ...args) => {
await sleep(3000)
console.log('receive message from renderer process', ...args)
})
}
app.on('ready', () => {
createWindow()
handleIPC()
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>230519</title>
<style>
button {
font-size: 3rem;
}
</style>
</head>
<body>
<h1>renderer process</h1>
<button id="btn">ipcRenderer.send('event1', 1, 2, 3)</button>
<script>
const { ipcRenderer } = require('electron')
// document.getElementById('btn').addEventListener('click', () => {
// console.log('123')
// ipcRenderer.send('event1', 1, 2, 3)
// console.log('after call send')
// })
btn.onclick = () => {
console.log('btn clicked')
ipcRenderer.invoke('event1', 1, 2, 3)
console.log('after call invoke')
}
</script>
</body>
</html>
IPC 模式 2:渲染进程到主进程(双向)
- 理解在通信时,同步、异步之间的差异,同步的
ipcRenderer.sendSync
会导致渲染进程直接阻塞 - 认识 ipcRender.send、ipcRendere.sendSync、ipcRenderer.sendSync 之间的一些差异
- 掌握好使用 ipcRenderer.invoke、ipcMain.handle 实现渲染进程和主进程之间的双向通信
(1)ipcRenderer.send
源码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>230521</title>
<style>
button {
font-size: 3rem;
}
</style>
</head>
<body>
<h1>renderer process</h1>
<button id="btn">send</button>
<script>
const { ipcRenderer } = require('electron')
btn.onclick = () => {
ipcRenderer.send('message-from-renderer', 1, 2, 3)
}
ipcRenderer.on('message-from-main', (_, res) => {
console.log('receive message from main process', res)
})
</script>
</body>
</html>
const {app, BrowserWindow, ipcMain} = require('electron')
let win
function createWindow() {
win = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
win.webContents.openDevTools()
win.loadFile("./index.html")
}
function handleIPC() {
ipcMain.on('message-from-renderer', (event, ...args) => {
console.log('receive message from renderer process', ...args)
const sum = args.reduce((a, b) => a + b, 0)
// win.webContents.send('message-from-main', sum)
// event.sender.send('message-from-main', sum)
// console.log('win.webContents === event.sender', win.webContents === event.sender)
event.reply('message-from-main', sum)
})
}
app.on('ready', () => {
createWindow()
handleIPC()
})
{
"name": "0000",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "electron ."
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"electron": "^24.3.1"
}
}
package.json 的内容都是一样的,后续保持不变
(2)ipcRenderer.sendSync
const {app, BrowserWindow, ipcMain} = require('electron')
let win
function createWindow() {
win = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
win.webContents.openDevTools()
win.loadFile("./index.html")
}
const sleep = (duration) => new Promise((resolve) => setTimeout(resolve, duration))
function handleIPC() {
ipcMain.on('send-message', async (event, ...args) => {
await sleep(3000)
console.log('主进程收到了来自渲染进程的 ipcRenderer.send 方法发送的消息', ...args)
const sum = args.reduce((a, b) => a + b, 0)
event.reply('message-from-main', sum)
})
ipcMain.on('sendSync-message', async (event, ...args) => {
await sleep(3000)
console.log('主进程收到了来自渲染进程的 ipcRenderer.sendSync 方法发送的消息', ...args)
const sum = args.reduce((a, b) => a + b, 0)
event.returnValue = sum
})
}
app.on('ready', () => {
createWindow()
handleIPC()
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>230521</title>
<style>
button {
font-size: 3rem;
}
</style>
</head>
<body>
<h1>renderer process</h1>
<button id="btn1">send</button>
<button id="btn2">sendSync</button>
<script>
const {
ipcRenderer
} = require('electron')
btn1.onclick = () => {
const res = ipcRenderer.send('send-message', 1, 2, 3)
console.log('ipcRenderer.send 方法收到的返回结果:', res)
}
btn2.onclick = () => {
const res = ipcRenderer.sendSync('sendSync-message', 1, 2, 3)
console.log('收到了主进程的消息 event.returnValue:', res)
}
ipcRenderer.on('message-from-main', (_, res) => {
console.log('receive message from main process', res)
})
</script>
</body>
</html>
对比 send 和 sendSync:
ipcRenderer.send
和 ipcRenderer.sendSync
都是 Electron 的 ipcRenderer
模块中用于发送消息到主进程的方法。但是它们在发送消息的方式上有一些差异:
同步 vs 异步:
ipcRenderer.send
是一个 异步 方法,它会 立即返回,不会阻塞渲染进程。
当主进程接收到消息并处理完后,如果需要回复消息,主进程会再发送一个消息给渲染进程,渲染进程需要另外设置监听来接收。ipcRenderer.sendSync
是一个 同步 方法,它会 阻塞渲染进程,等待主进程接收消息并返回结果。当这个方法返回时,返回的就是主进程处理的结果。
在主进程返回结果之前,渲染进程将始终处于阻塞状态。
由于 ipcRenderer.send
是非阻塞的,所以在性能上通常优于 ipcRenderer.sendSync
。因为 ipcRenderer.sendSync
会阻塞渲染进程,直到主进程返回结果,这可能会导致渲染进程界面的暂停或卡顿,影响用户体验。
⚠️ 阻塞 JavaScript 的执行线程是非常危险的
因为 JavaScript 本身就是单线程运行,一旦某个方法阻塞了这个仅有的线程,JavaScript 的运行就停滞了,只能等这个方法退出。假设此时预期需要有一个 setTimeout 事件或 setInterval 事件被执行,那么此预期也落空了。这可能使我们的业务处于不可知的异常中。
JavaScript 语言本身以“异步编程”著称,因此 我们应该尽量避免用它的同步方法和长耗时方法,避免造成执行线程阻塞。
PS:remote 模块不推荐使用的原因之一就是因为它底层的执行逻辑是同步的,玩不好很可能导致程序卡死。
返回值:
ipcRenderer.send
的返回值是 undefined
,因为它只是发送消息,不关心主进程是否有返回结果。ipcRenderer.sendSync
的返回值是主进程处理结果,因为它会等待主进程处理完消息并返回结果。
小结:
如果我们的应用对性能有较高要求,或者不需要即刻得到主进程处理的结果,那么应优先使用 ipcRenderer.send
如果我们的应用需要即刻得到主进程处理的结果,并且对可能的性能影响可以接受,那么可以使用 ipcRenderer.sendSync
⚠️ 官方建议
如果我们开发的应用所使用的 Electron 的版本高于 v7,那么推荐使用新版的 API
ipcRenderer.invoke
来实现渲染进程到主进程之间的通信。放弃使用传统的ipcRenderer.send
、ipcRenderer.sendSync
(3)ipcRenderer.invoke
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>230521</title>
<style>
button {
font-size: 3rem;
}
</style>
</head>
<body>
<h1>renderer process</h1>
<button id="btn1">请求</button>
<button id="btn2">请求 + 响应</button>
<script>
const {
ipcRenderer
} = require('electron')
// 单向(请求)
btn1.onclick = () => {
ipcRenderer.invoke('invoke-message1', 1, 2, 3)
}
// 双向(请求 + 响应)
btn2.onclick = async () => {
const res = await ipcRenderer.invoke('invoke-message2', 4, 5, 6)
console.log('ipcRenderer.invoke 方法收到的返回结果:', res)
}
</script>
</body>
</html>
const {app, BrowserWindow, ipcMain} = require('electron')
let win
function createWindow() {
win = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
win.webContents.openDevTools()
win.loadFile("./index.html")
}
function handleIPC() {
ipcMain.handle('invoke-message1', (_, ...args) => {
console.log('invoke-message1', ...args)
// ...
})
ipcMain.handle('invoke-message2', (_, ...args) => {
console.log('invoke-message2', ...args)
// ...
return args.reduce((a, b) => a + b, 0)
})
}
app.on('ready', () => {
createWindow()
handleIPC()
})
对比 invoke 和 send:
invoke 更简洁,仅需要一个事件即可完成通信:
在渲染进程中,通过 ipcRenderer.invoke 将请求发送给主进程,在主进程的 ipcMain.handle 中,将处理完的结果直接 return 即可返回给我们的渲染进程。
send 更麻烦,因为需要绑定俩事件:
- 事件 1:渲染进程发起请求,主进程接收请求
- 事件 2:主进程发起响应,渲染进程接收响应
IPC 模式 3:主进程到渲染进程
🤔 主进程向渲染进程发消息,是向页面发吗?
答:并不是,而是向具体的 BrowserWindow 实例发,具体得看这个实例加载的是哪个页面。所以说,如果一个页面被多个实例都引用了,只有对应的实例才能收到消息,虽然它们都是同一个页面。
IPC 模式 4:渲染进程到渲染进程
const {
app,
BrowserWindow,
ipcMain
} = require('electron')
const {
v4: uuidv4
} = require('uuid')
let win1, win2
function createWin() {
win1 = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
win2 = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
win1.webContents.openDevTools()
win2.webContents.openDevTools()
win1.loadFile('./index1.html')
win2.loadFile('./index2.html')
}
function handleIPC() {
const promises = new Map()
ipcMain.on('message-from-renderer2', (event, {
id,
result
}) => {
const {
resolve
} = promises.get(id)
promises.delete(id)
resolve(result)
})
ipcMain.handle('message-from-renderer1', async (event, ...args) => {
console.log('main process received message from renderer1 with args:', args)
return await sendRequestToRenderer2(...args)
})
function sendRequestToRenderer2(...args) {
return new Promise((resolve, reject) => {
const id = uuidv4()
promises.set(id, {
resolve,
reject
})
win2.webContents.send('message-to-renderer2', id,
...args)
})
}
}
app.whenReady().then(() => {
console.log('app ready')
createWin()
handleIPC()
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>renderer1</title>
</head>
<body>
<h1>renderer1</h1>
<script>
// import { ipcRenderer } from "electron"
const { ipcRenderer } = require('electron');
(async () => {
console.log('1 + 2 =', await ipcRenderer.invoke('message-from-renderer1', 1, 2))
console.log('1 + 2 + 3 =', await ipcRenderer.invoke('message-from-renderer1', 1, 2, 3))
console.log('1 + 2 + 3 + 4 =', await ipcRenderer.invoke('message-from-renderer1', 1, 2, 3, 4))
})()
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>renderer2</title>
</head>
<body>
<h1>renderer2</h1>
<script>
const { ipcRenderer } = require('electron')
const sleep = async (duration) => {
return new Promise(resolve => setTimeout(() => {
resolve()
}, duration))
}
ipcRenderer.on('message-to-renderer2', async (event, id, ...args) => {
console.log('message-to-renderer2', id, ...args)
await sleep(args.length * 1000)
ipcRenderer.send('message-from-renderer2', { id, result: args.reduce((a, b) => a + b, 0) })
})
</script>
</body>
</html>
渲染进程之间互相通信的方式:
- 方式1:主进程作为中转站,渲染进程1给主进程发消息,主进程接收到消息后,主进程再给渲染进程2发消息;(2 次通信)
- 方式2:主进程不做转发操作,仅提供对应渲染进程的 id,渲染进程1给主进程发消息,主进程接收到消息后,将需要和渲染进程1通信的其它渲染进程的id给返回,渲染进程1接收到主进程返回的其它渲染进程的id之后,由渲染进程1来发送消息;(3 次通信)
demo 渲染进程相互通信
const { ipcMain, app, BrowserWindow } = require('electron')
let win1, win2
app.on('ready', () => {
createWindows()
handleIPC()
hanldeWinClosed()
})
app.on('window-all-closed', () => app.quit())
function createWindows() {
win1 = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
win1.loadFile('./index1.html')
win1.webContents.openDevTools()
win2 = new BrowserWindow({
y: 0,
x: 0,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
win2.loadFile('./index2.html')
win2.webContents.openDevTools()
}
function handleIPC() {
// 写法1
// ipcMain.handle('getWin2ID', async () => {
// const res = await new Promise(resolve => resolve(win2.webContents.id))
// return res
// })
// 写法2(如果仅仅是获取一个值的话,这种写法更简洁)
ipcMain.handle('getWin1ID', async () => win1.webContents.id)
}
function hanldeWinClosed() {
win1.on('closed', () => win1 = null)
win2.on('closed', () => win2 = null)
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3.4.3 渲染进程之间消息传递</title>
</head>
<body>
<h1>窗口1</h1>
<button id="btn">给窗口2发送消息</button>
<script>
const {
ipcRenderer
} = require('electron')
document.getElementById('btn').addEventListener('click', async () => {
const win2ID = await ipcRenderer.invoke('getWin2ID')
console.log('获取到窗口2的id:', win2ID, '并给它发送消息')
ipcRenderer.sendTo(win2ID, 'index1_to_index2', 1, 2)
})
ipcRenderer.on('index2_to_index1', (e, a, b) => {
console.log('窗口1 收到了 窗口2 发送来的消息')
console.log('发送者「窗口2」的 id 为:', e.senderId)
console.log('参数为:', a, b)
})
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3.4.3 渲染进程之间消息传递</title>
</head>
<body>
<h1>窗口2</h1>
<button id="btn">给窗口1发送消息</button>
<script>
const {
ipcRenderer
} = require('electron')
document.getElementById('btn').addEventListener('click', async () => {
const win1ID = await ipcRenderer.invoke('getWin1ID')
console.log('获取到窗口1的id:', win1ID, '并给它发送消息')
ipcRenderer.sendTo(win1ID, 'index2_to_index1', 3, 4)
})
ipcRenderer.on('index1_to_index2', (e, a, b) => {
console.log('窗口2 收到了 窗口1 发送来的消息')
console.log('发送者「窗口1」的 id 为:', e.senderId)
console.log('参数为:', a, b)
})
</script>
</body>
</html>
:::warning
注意:
在发送消息之前,应该先确保打开了 win1
、win2
窗口
:::