進(jìn)程間通信 (IPC) 是在 Electron 中構(gòu)建功能豐富的桌面應(yīng)用程序的關(guān)鍵部分之一。 由于主進(jìn)程和渲染器進(jìn)程在 Electron 的進(jìn)程模型具有不同的職責(zé),因此 IPC 是執(zhí)行許多常見任務(wù)的唯一方法,例如從 UI 調(diào)用原生 API 或從原生菜單觸發(fā) Web 內(nèi)容的更改。
在 Electron 中,進(jìn)程使用 ?ipcMain
? 和 ?ipcRenderer
? 模塊,通過開發(fā)人員定義的“通道”傳遞消息來進(jìn)行通信。 這些通道是 任意 (您可以隨意命名它們)和 雙向 (您可以在兩個(gè)模塊中使用相同的通道名稱)的。
在本指南中,我們將介紹一些基本的 IPC 模式,并提供具體的示例。您可以將這些示例作為您應(yīng)用程序代碼的參考。
在開始實(shí)現(xiàn)細(xì)節(jié)之前,您應(yīng)該熟悉使用 預(yù)加載腳本 在上下文隔離渲染器進(jìn)程中導(dǎo)入 Node.js 和 Electron 模塊的概念。
contextBridge
? 模塊從預(yù)加載腳本暴露 API 的入門知識(shí),請(qǐng)查看 上下文隔離教程。要將單向 IPC 消息從渲染器進(jìn)程發(fā)送到主進(jìn)程,您可以使用 ?ipcRenderer.send
? API 發(fā)送消息,然后使用 ?ipcMain.on
? API 接收。
通常使用此模式從 Web 內(nèi)容調(diào)用主進(jìn)程 API。 我們將通過創(chuàng)建一個(gè)簡(jiǎn)單的應(yīng)用來演示此模式,可以通過編程方式更改它的窗口標(biāo)題。
對(duì)于此演示,您需要將代碼添加到主進(jìn)程、渲染器進(jìn)程和預(yù)加載腳本。 完整代碼如下,我們將在后續(xù)章節(jié)中對(duì)每個(gè)文件進(jìn)行單獨(dú)解釋。
main.js | preload.js | index.html | renderer.js |
|
|
|
|
DOCS/FIDDLES/IPC/PATTERN-1 (22.0.2)
在主進(jìn)程中,使用 ipcMain.on
API 在 set-title
通道上設(shè)置一個(gè) IPC 監(jiān)聽器:
const {app, BrowserWindow, ipcMain} = require('electron')
const path = require('path')
//...
function handleSetTitle (event, title) {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win.setTitle(title)
}
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
ipcMain.on('set-title', handleSetTitle)
createWindow()
}
//...
上面的 handleSetTitle
回調(diào)函數(shù)有兩個(gè)參數(shù):一個(gè) ?IpcMainEvent
? 結(jié)構(gòu)和一個(gè) title
字符串。 每當(dāng)消息通過 set-title
通道傳入時(shí),此函數(shù)找到附加到消息發(fā)送方的 BrowserWindow 實(shí)例,并在該實(shí)例上使用 win.setTitle
API。
INFO
請(qǐng)確保您為以下步驟加載了
index.html
和preload.js
入口點(diǎn)!
要將消息發(fā)送到上面創(chuàng)建的監(jiān)聽器,您可以使用 ipcRenderer.send
API。 默認(rèn)情況下,渲染器進(jìn)程沒有權(quán)限訪問 Node.js 和 Electron 模塊。 作為應(yīng)用開發(fā)者,您需要使用 contextBridge
API 來選擇要從預(yù)加載腳本中暴露哪些 API。
在您的預(yù)加載腳本中添加以下代碼,向渲染器進(jìn)程暴露一個(gè)全局的 window.electronAPI
變量。
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
此時(shí),您將能夠在渲染器進(jìn)程中使用 window.electronAPI.setTitle()
函數(shù)。
安全警告
出于 安全原因,我們不會(huì)直接暴露整個(gè)
ipcRenderer.send
API。 確保盡可能限制渲染器對(duì) Electron API 的訪問。
在 BrowserWindow 加載的我們的 HTML 文件中,添加一個(gè)由文本輸入框和按鈕組成的基本用戶界面:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Hello World!</title>
</head>
<body>
Title: <input id="title"/>
<button id="btn" type="button">Set</button>
<script src="./renderer.js"></script>
</body>
</html>
為了使這些元素具有交互性,我們將在導(dǎo)入的 renderer.js
文件中添加幾行代碼,以利用從預(yù)加載腳本中暴露的 window.electronAPI
功能:
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value
window.electronAPI.setTitle(title)
});
此時(shí),您的演示應(yīng)用應(yīng)該已經(jīng)功能齊全。 嘗試使用輸入框,看看 BrowserWindow 的標(biāo)題會(huì)發(fā)生什么變化!
雙向 IPC 的一個(gè)常見應(yīng)用是從渲染器進(jìn)程代碼調(diào)用主進(jìn)程模塊并等待結(jié)果。 這可以通過將 ?ipcRenderer.invoke
? 與 ?ipcMain.handle
? 搭配使用來完成。
在下面的示例中,我們將從渲染器進(jìn)程打開一個(gè)原生的文件對(duì)話框,并返回所選文件的路徑。
對(duì)于此演示,您需要將代碼添加到主進(jìn)程、渲染器進(jìn)程和預(yù)加載腳本。 完整代碼如下,我們將在后續(xù)章節(jié)中對(duì)每個(gè)文件進(jìn)行單獨(dú)解釋。
main.js | preload.js | index.html | renderer.js |
|
|
|
|
DOCS/FIDDLES/IPC/PATTERN-2 (22.0.2)
在主進(jìn)程中,我們將創(chuàng)建一個(gè) handleFileOpen()
函數(shù),它調(diào)用 dialog.showOpenDialog
并返回用戶選擇的文件路徑值。 每當(dāng)渲染器進(jìn)程通過 dialog:openFile
通道發(fā)送 ipcRender.invoke
消息時(shí),此函數(shù)被用作一個(gè)回調(diào)。 然后,返回值將作為一個(gè) Promise
返回到最初的 invoke
調(diào)用。
關(guān)于錯(cuò)誤處理
在主進(jìn)程中通過
handle
引發(fā)的錯(cuò)誤是不透明的,因?yàn)樗鼈儽恍蛄谢耍⑶抑挥性煎e(cuò)誤的message
屬性會(huì)提供給渲染器進(jìn)程。 詳情請(qǐng)參閱 [#24427](https://github.com/electron/electron/issues/24427)。
const { BrowserWindow, dialog, ipcMain } = require('electron')
const path = require('path')
//...
async function handleFileOpen() {
const { canceled, filePaths } = await dialog.showOpenDialog()
if (canceled) {
return
} else {
return filePaths[0]
}
}
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}
app.whenReady(() => {
ipcMain.handle('dialog:openFile', handleFileOpen)
createWindow()
})
//...
關(guān)于通道名稱
IPC 通道名稱上的
dialog:
前綴對(duì)代碼沒有影響。 它僅用作命名空間以幫助提高代碼的可讀性。
INFO
請(qǐng)確保您為以下步驟加載了
index.html
和preload.js
入口點(diǎn)!
在預(yù)加載腳本中,我們暴露了一個(gè)單行的 openFile
函數(shù),它調(diào)用并返回 ipcRenderer.invoke('dialog:openFile')
的值。 我們將在下一步中使用此 API 從渲染器的用戶界面調(diào)用原生對(duì)話框。
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
安全警告
出于 安全原因,我們不會(huì)直接暴露整個(gè)
ipcRenderer.invoke
API。 確保盡可能限制渲染器對(duì) Electron API 的訪問。
最后,讓我們構(gòu)建加載到 BrowserWindow 中的 HTML 文件。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Dialog</title>
</head>
<body>
<button type="button" id="btn">Open a File</button>
File path: <strong id="filePath"></strong>
<script src='./renderer.js'></script>
</body>
</html>
用戶界面包含一個(gè) #btn
按鈕元素,將用于觸發(fā)我們的預(yù)加載 API,以及一個(gè) #filePath
元素,將用于顯示所選文件的路徑。 要使這些部分起作用,需要在渲染器進(jìn)程腳本中編寫幾行代碼:
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')
btn.addEventListener('click', async () => {
const filePath = await window.electronAPI.openFile()
filePathElement.innerText = filePath
})
在上面的代碼片段中,我們監(jiān)聽 #btn
按鈕的點(diǎn)擊,并調(diào)用 window.electronAPI.openFile()
API 來激活原生的打開文件對(duì)話框。 然后我們?cè)?nbsp;#filePath
元素中顯示選中文件的路徑。
ipcRenderer.invoke
API 是在 Electron 7 中添加的,作為處理渲染器進(jìn)程中雙向 IPC 的一種開發(fā)人員友好的方式。 但這種 IPC 模式存在幾種替代方法。
如果可能,請(qǐng)避免使用舊方法
我們建議盡可能使用
ipcRenderer.invoke
。 出于保留歷史的目地,記錄了下面雙向地渲染器到主進(jìn)程模式。
INFO
對(duì)于以下示例,我們將直接從預(yù)加載腳本調(diào)用
ipcRenderer
,以保持代碼示例短小。
我們用于單向通信的 ipcRenderer.send
API 也可用于雙向通信。 這是在 Electron 7 之前通過 IPC 進(jìn)行異步雙向通信的推薦方式。
// 您也可以使用 `contextBridge` API
// 將這段代碼暴露給渲染器進(jìn)程
const { ipcRenderer } = require('electron')
ipcRenderer.on('asynchronous-reply', (_event, arg) => {
console.log(arg) // 在 DevTools 控制臺(tái)中打印“pong”
})
ipcRenderer.send('asynchronous-message', 'ping')
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // 在 Node 控制臺(tái)中打印“ping”
// 作用如同 `send`,但返回一個(gè)消息
// 到發(fā)送原始消息的渲染器
event.reply('asynchronous-reply', 'pong')
})
這種方法有幾個(gè)缺點(diǎn):
ipcRenderer.on
監(jiān)聽器來處理渲染器進(jìn)程中的響應(yīng)。 使用 invoke
,您將獲得作為 Promise 返回到原始 API 調(diào)用的響應(yīng)值。asynchronous-reply
消息與原始的 asynchronous-message
消息配對(duì)。 如果您通過這些通道非常頻繁地來回傳遞消息,則需要添加其他應(yīng)用代碼來單獨(dú)跟蹤每個(gè)調(diào)用和響應(yīng)。ipcRenderer.sendSync
API 向主進(jìn)程發(fā)送消息,并 同步 等待響應(yīng)。
const { ipcMain } = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // 在 Node 控制臺(tái)中打印“ping”
event.returnValue = 'pong'
})
// 您也可以使用 `contextBridge` API
// 將這段代碼暴露給渲染器進(jìn)程
const { ipcRenderer } = require('electron')
const result = ipcRenderer.sendSync('synchronous-message', 'ping')
console.log(result) // 在 DevTools 控制臺(tái)中打印“pong”
這份代碼的結(jié)構(gòu)與 invoke
模型非常相似,但出于性能原因,我們建議避免使用此 API。 它的同步特性意味著它將阻塞渲染器進(jìn)程,直到收到回復(fù)為止。
將消息從主進(jìn)程發(fā)送到渲染器進(jìn)程時(shí),需要指定是哪一個(gè)渲染器接收消息。 消息需要通過其 WebContents
實(shí)例發(fā)送到渲染器進(jìn)程。 此 WebContents 實(shí)例包含一個(gè) send
方法,其使用方式與 ipcRenderer.send
相同。
為了演示此模式,我們將構(gòu)建一個(gè)由原生操作系統(tǒng)菜單控制的數(shù)字計(jì)數(shù)器。
對(duì)于此演示,您需要將代碼添加到主進(jìn)程、渲染器進(jìn)程和預(yù)加載腳本。 完整代碼如下,我們將在后續(xù)章節(jié)中對(duì)每個(gè)文件進(jìn)行單獨(dú)解釋。
main.js | preload.js | index.html | renderer.js |
|
|
|
|
DOCS/FIDDLES/IPC/PATTERN-3 (22.0.2)
對(duì)于此演示,我們需要首先使用 Electron 的 Menu
模塊在主進(jìn)程中構(gòu)建一個(gè)自定義菜單,該模塊使用 webContents.send
API 將 IPC 消息從主進(jìn)程發(fā)送到目標(biāo)渲染器。
const {app, BrowserWindow, Menu, ipcMain} = require('electron')
const path = require('path')
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
const menu = Menu.buildFromTemplate([
{
label: app.name,
submenu: [
{
click: () => mainWindow.webContents.send('update-counter', 1),
label: 'Increment',
},
{
click: () => mainWindow.webContents.send('update-counter', -1),
label: 'Decrement',
}
]
}
])
Menu.setApplicationMenu(menu)
mainWindow.loadFile('index.html')
}
//...
出于本教程的目的,請(qǐng)務(wù)必注意, click
處理函數(shù)通過 update-counter
通道向渲染器進(jìn)程發(fā)送消息(1
或 -1
)。
click: () => mainWindow.webContents.send('update-counter', -1)
INFO
請(qǐng)確保您為以下步驟加載了
index.html
和preload.js
入口點(diǎn)!
與前面的渲染器到主進(jìn)程的示例一樣,我們使用預(yù)加載腳本中的 contextBridge
和 ipcRenderer
模塊向渲染器進(jìn)程暴露 IPC 功能:
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', callback)
})
加載預(yù)加載腳本后,渲染器進(jìn)程應(yīng)有權(quán)訪問 window.electronAPI.onUpdateCounter()
監(jiān)聽器函數(shù)。
安全警告
出于 安全原因,我們不會(huì)直接暴露整個(gè)
ipcRenderer.on
API。 確保盡可能限制渲染器對(duì) Electron API 的訪問。
INFO
在這個(gè)最小示例中,您可以直接在預(yù)加載腳本中調(diào)用
ipcRenderer.on
,而不是通過 context bridge 暴露它。
const { ipcRenderer } = require('electron') window.addEventListener('DOMContentLoaded', () => { const counter = document.getElementById('counter') ipcRenderer.on('update-counter', (_event, value) => { const oldValue = Number(counter.innerText) const newValue = oldValue + value counter.innerText = newValue }) })
但是,與通過 context bridge 暴露預(yù)加載 API 相比,此方法的靈活性有限,因?yàn)楸O(jiān)聽器無法直接與渲染器代碼交互。
為了將它們聯(lián)系在一起,我們將在加載的 HTML 文件中創(chuàng)建一個(gè)接口,其中包含一個(gè) #counter
元素,我們將使用該元素來顯示值:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Menu Counter</title>
</head>
<body>
Current value: <strong id="counter">0</strong>
<script src="./renderer.js"></script>
</body>
</html>
最后,為了更新 HTML 文檔中的值,我們將添加幾行 DOM 操作的代碼,以便在每次觸發(fā) update-counter
事件時(shí)更新 #counter
元素的值。
const counter = document.getElementById('counter')
window.electronAPI.onUpdateCounter((_event, value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue
})
在上面的代碼中,我們將回調(diào)傳遞給從預(yù)加載腳本中暴露的 window.electronAPI.onUpdateCounter
函數(shù)。 第二個(gè) value
參數(shù)對(duì)應(yīng)于我們傳入 webContents.send
函數(shù)的 1
或 -1
,該函數(shù)是從原生菜單調(diào)用的。
對(duì)于從主進(jìn)程到渲染器進(jìn)程的 IPC,沒有與 ipcRenderer.invoke
等效的 API。 不過,您可以從 ipcRenderer.on
回調(diào)中將回復(fù)發(fā)送回主進(jìn)程。
我們可以對(duì)前面例子的代碼進(jìn)行略微修改來演示這一點(diǎn)。 在渲染器進(jìn)程中,使用 event
參數(shù),通過 counter-value
通道將回復(fù)發(fā)送回主進(jìn)程。
const counter = document.getElementById('counter')
window.electronAPI.onUpdateCounter((event, value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue
event.sender.send('counter-value', newValue)
})
在主進(jìn)程中,監(jiān)聽 counter-value
事件并適當(dāng)?shù)靥幚硭鼈儭?
//...
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // 將打印到 Node 控制臺(tái)
})
//...
沒有直接的方法可以使用 ipcMain
和 ipcRenderer
模塊在 Electron 中的渲染器進(jìn)程之間發(fā)送消息。 為此,您有兩種選擇:
MessagePort
? 傳遞到兩個(gè)渲染器。 這將允許在初始設(shè)置后渲染器之間直接進(jìn)行通信。Electron 的 IPC 實(shí)現(xiàn)使用 HTML 標(biāo)準(zhǔn)的 結(jié)構(gòu)化克隆算法 來序列化進(jìn)程之間傳遞的對(duì)象,這意味著只有某些類型的對(duì)象可以通過 IPC 通道傳遞。
特別是 DOM 對(duì)象(例如 Element
,Location
和 DOMMatrix
),Node.js 中由 C++ 類支持的對(duì)象(例如 process.env
,Stream
的一些成員)和 Electron 中由 C++ 類支持的對(duì)象(例如 WebContents
、BrowserWindow
和 WebFrame
)無法使用結(jié)構(gòu)化克隆序列化。
更多建議: