跨進程通訊
跨進程通訊 (IPC) 是在 Electron 中建立功能豐富的桌面應用程式的關鍵部分。由於主進程和渲染進程在 Electron 的進程模型中具有不同的職責,IPC 是執行許多常見任務的唯一方法,例如從您的 UI 呼叫原生 API 或從原生選單觸發網頁內容中的變更。
IPC 通道
在 Electron 中,進程透過 ipcMain
和 ipcRenderer
模組,通過開發人員定義的「通道」傳遞訊息進行通訊。這些通道是任意的(您可以將它們命名為任何您想要的名稱)和雙向的(您可以對兩個模組使用相同的通道名稱)。
在本指南中,我們將透過您可以作為應用程式程式碼參考的具體範例,介紹一些基本的 IPC 模式。
了解上下文隔離的進程
在繼續進行實作細節之前,您應該熟悉使用預載腳本在上下文隔離的渲染進程中匯入 Node.js 和 Electron 模組的概念。
模式 1:渲染進程到主進程(單向)
若要從渲染進程向主進程觸發單向 IPC 訊息,您可以使用 ipcRenderer.send
API 發送訊息,然後由 ipcMain.on
API 接收訊息。
您通常會使用此模式從您的網頁內容呼叫主進程 API。我們將透過建立一個可以程式化變更其視窗標題的簡單應用程式來示範此模式。
對於此示範,您需要在主進程、渲染進程和預載腳本中加入程式碼。完整程式碼如下所示,但我們將在以下章節中分別說明每個檔案。
- main.js
- preload.js
- index.html
- renderer.js
const { app, BrowserWindow, ipcMain } = require('electron/main')
const path = require('node:path')
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
ipcMain.on('set-title', (event, title) => {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win.setTitle(title)
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
const { contextBridge, ipcRenderer } = require('electron/renderer')
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
<!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>
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value
window.electronAPI.setTitle(title)
})
1. 使用 ipcMain.on
監聽事件
在主進程中,使用 ipcMain.on
API 在 set-title
通道上設定 IPC 監聽器
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('node: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
回呼有兩個參數:一個 IpcMainEvent 結構和一個 title
字串。每當訊息通過 set-title
通道時,此函數將尋找附加到訊息發送器的 BrowserWindow 實例,並在其上使用 win.setTitle
API。
請確保您正在載入後續步驟的 index.html
和 preload.js
入口點!
2. 透過預載公開 ipcRenderer.send
若要將訊息傳送至上面建立的監聽器,您可以使用 ipcRenderer.send
API。預設情況下,渲染進程無法存取 Node.js 或 Electron 模組。作為應用程式開發人員,您需要選擇要使用 contextBridge
API 從預載腳本公開哪些 API。
在您的預載腳本中,加入以下程式碼,這將向您的渲染進程公開全域的 window.electronAPI
變數。
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
此時,您將能夠在渲染進程中使用 window.electronAPI.setTitle()
函數。
我們不會直接公開整個 ipcRenderer.send
API,因為這樣有安全上的考量。請務必盡可能限制渲染器對 Electron API 的存取權限。
3. 建置渲染進程 UI
在我們的 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>Hello World!</title>
</head>
<body>
Title: <input id="title"/>
<button id="btn" type="button">Set</button>
<script src="./renderer.js"></script>
</body>
</html>
為了使這些元素具有互動性,我們將在匯入的 renderer.js
檔案中加入幾行程式碼,以利用從預載腳本公開的 window.electronAPI
功能
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value
window.electronAPI.setTitle(title)
})
此時,您的示範應該可以完全運作了。試著使用輸入欄位,看看您的 BrowserWindow 標題會發生什麼事!
模式 2:渲染進程到主進程(雙向)
雙向 IPC 的常見應用是從您的渲染進程程式碼呼叫主進程模組並等待結果。這可以使用 ipcRenderer.invoke
配對 ipcMain.handle
來完成。
在以下範例中,我們將從渲染進程開啟原生檔案對話方塊,並傳回選取檔案的路徑。
對於此示範,您需要在主進程、渲染進程和預載腳本中加入程式碼。完整程式碼如下所示,但我們將在以下章節中分別說明每個檔案。
- main.js
- preload.js
- index.html
- renderer.js
const { app, BrowserWindow, ipcMain, dialog } = require('electron/main')
const path = require('node:path')
async function handleFileOpen () {
const { canceled, filePaths } = await dialog.showOpenDialog()
if (!canceled) {
return filePaths[0]
}
}
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
ipcMain.handle('dialog:openFile', handleFileOpen)
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
const { contextBridge, ipcRenderer } = require('electron/renderer')
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
<!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>
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')
btn.addEventListener('click', async () => {
const filePath = await window.electronAPI.openFile()
filePathElement.innerText = filePath
})
1. 使用 ipcMain.handle
監聽事件
在主進程中,我們將建立一個 handleFileOpen()
函數,該函數會呼叫 dialog.showOpenDialog
並傳回使用者選取檔案路徑的值。每當從渲染進程透過 dialog:openFile
通道傳送 ipcRender.invoke
訊息時,此函數都會用作回呼。然後,傳回值會作為 Promise 傳回至原始 invoke
呼叫。
在主進程中透過 handle
擲出的錯誤並不是透明的,因為它們會被序列化,並且只會將原始錯誤中的 message
屬性提供給渲染進程。請參閱 #24427 以了解詳細資訊。
const { app, BrowserWindow, dialog, ipcMain } = require('electron')
const path = require('node:path')
// ...
async function handleFileOpen () {
const { canceled, filePaths } = await dialog.showOpenDialog({})
if (!canceled) {
return filePaths[0]
}
}
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
ipcMain.handle('dialog:openFile', handleFileOpen)
createWindow()
})
// ...
IPC 通道名稱上的 dialog:
前置詞對程式碼沒有影響。它僅作為命名空間,有助於提高程式碼的可讀性。
請確保您正在載入後續步驟的 index.html
和 preload.js
入口點!
2. 透過預載公開 ipcRenderer.invoke
在預載腳本中,我們公開一個單行 openFile
函數,該函數會呼叫並傳回 ipcRenderer.invoke('dialog:openFile')
的值。我們將在下一步中使用此 API 從渲染器的使用者介面呼叫原生對話方塊。
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
我們不會直接公開整個 ipcRenderer.invoke
API,因為這樣有安全上的考量。請務必盡可能限制渲染器對 Electron API 的存取權限。
3. 建置渲染進程 UI
最後,讓我們建置載入到我們 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>
UI 由單個 #btn
按鈕元素組成,該元素將用於觸發我們的預載 API,以及一個 #filePath
元素,該元素將用於顯示所選檔案的路徑。若要使這些部分正常運作,需要在渲染進程腳本中加入幾行程式碼
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')
btn.addEventListener('click', async () => {
const filePath = await window.electronAPI.openFile()
filePathElement.innerText = filePath
})
在上面的程式碼片段中,我們監聽 #btn
按鈕上的點擊事件,並呼叫我們的 window.electronAPI.openFile()
API 以啟動原生開啟檔案對話方塊。然後,我們在 #filePath
元素中顯示選取的檔案路徑。
注意:舊方法
在 Electron 7 中新增了 ipcRenderer.invoke
API,作為一種開發人員友善的方式來處理從渲染器進程進行雙向 IPC 的問題。然而,這種 IPC 模式存在一些替代方法。
我們建議盡可能使用 ipcRenderer.invoke
。以下兩種渲染器到主進程的雙向模式僅供歷史參考。
在以下範例中,我們直接從預載腳本中呼叫 ipcRenderer
以保持程式碼範例簡短。
使用 ipcRenderer.send
我們用於單向通訊的 ipcRenderer.send
API 也可用於執行雙向通訊。這是 Electron 7 之前透過 IPC 進行非同步雙向通訊的建議方式。
// You can also put expose this code to the renderer
// process with the `contextBridge` API
const { ipcRenderer } = require('electron')
ipcRenderer.on('asynchronous-reply', (_event, arg) => {
console.log(arg) // prints "pong" in the DevTools console
})
ipcRenderer.send('asynchronous-message', 'ping')
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // prints "ping" in the Node console
// works like `send`, but returning a message back
// to the renderer that sent the original message
event.reply('asynchronous-reply', 'pong')
})
此方法有一些缺點
- 您需要在渲染器進程中設定第二個
ipcRenderer.on
監聽器來處理回應。使用invoke
,您會將回應值作為 Promise 回傳給原始 API 呼叫。 - 沒有明顯的方法可以將
asynchronous-reply
訊息與原始的asynchronous-message
訊息配對。如果這些通道之間有非常頻繁的訊息來回傳輸,您需要加入額外的應用程式碼來單獨追蹤每次呼叫和回應。
使用 ipcRenderer.sendSync
ipcRenderer.sendSync
API 將訊息傳送到主進程,並同步等待回應。
const { ipcMain } = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // prints "ping" in the Node console
event.returnValue = 'pong'
})
// You can also put expose this code to the renderer
// process with the `contextBridge` API
const { ipcRenderer } = require('electron')
const result = ipcRenderer.sendSync('synchronous-message', 'ping')
console.log(result) // prints "pong" in the DevTools console
此程式碼的結構與 invoke
模型非常相似,但我們建議基於效能考量避免使用此 API。它的同步性質意味著它會阻塞渲染器進程,直到收到回應。
模式 3:主進程到渲染器
當從主進程傳送訊息到渲染器進程時,您需要指定哪個渲染器接收訊息。訊息需要透過其 WebContents
實例傳送到渲染器進程。此 WebContents 實例包含一個 send
方法,其使用方式與 ipcRenderer.send
相同。
為了示範此模式,我們將建立一個由原生作業系統選單控制的數字計數器。
對於此示範,您需要在主進程、渲染進程和預載腳本中加入程式碼。完整程式碼如下所示,但我們將在以下章節中分別說明每個檔案。
- main.js
- preload.js
- index.html
- renderer.js
const { app, BrowserWindow, Menu, ipcMain } = require('electron/main')
const path = require('node: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')
// Open the DevTools.
mainWindow.webContents.openDevTools()
}
app.whenReady().then(() => {
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // will print value to Node console
})
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
const { contextBridge, ipcRenderer } = require('electron/renderer')
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)),
counterValue: (value) => ipcRenderer.send('counter-value', value)
})
<!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>
const counter = document.getElementById('counter')
window.electronAPI.onUpdateCounter((value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue.toString()
window.electronAPI.counterValue(newValue)
})
1. 使用 webContents
模組傳送訊息
對於這個示範,我們需要先在主進程中使用 Electron 的 Menu
模組建立一個自訂選單,該選單使用 webContents.send
API 將 IPC 訊息從主進程傳送到目標渲染器。
const { app, BrowserWindow, Menu, ipcMain } = require('electron')
const path = require('node: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')
}
// ...
就本教學而言,請務必注意 click
處理常式會透過 update-counter
通道傳送訊息(1
或 -1
)到渲染器進程。
click: () => mainWindow.webContents.send('update-counter', -1)
請確保您正在載入後續步驟的 index.html
和 preload.js
入口點!
2. 透過預載公開 ipcRenderer.on
與之前的渲染器到主進程範例一樣,我們在預載腳本中使用 contextBridge
和 ipcRenderer
模組,向渲染器進程公開 IPC 功能。
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value))
})
載入預載腳本後,您的渲染器進程應該可以存取 window.electronAPI.onUpdateCounter()
監聽器函數。
我們不會直接公開整個 ipcRenderer.on
API,這是為了安全考量。請務必盡可能限制渲染器對 Electron API 的存取。此外,不要只是將回呼傳遞給 ipcRenderer.on
,因為這會透過 event.sender
洩漏 ipcRenderer
。請使用自訂處理常式,僅使用所需的參數呼叫 callback
。
在此最小範例中,您可以直接在預載腳本中呼叫 ipcRenderer.on
,而不是透過內容橋樑公開它。
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
})
})
然而,相較於透過內容橋樑公開預載 API,此方法的彈性有限,因為您的監聽器無法直接與您的渲染器程式碼互動。
3. 建置渲染器進程 UI
為了將所有內容整合在一起,我們將在載入的 HTML 檔案中建立一個介面,其中包含一個 #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 操作,以便在我們觸發 update-counter
事件時更新 #counter
元素的值。
const counter = document.getElementById('counter')
window.electronAPI.onUpdateCounter((value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue.toString()
})
在上面的程式碼中,我們將回呼傳遞給從我們的預載腳本公開的 window.electronAPI.onUpdateCounter
函數。第二個 value
參數對應於我們從原生選單的 webContents.send
呼叫中傳入的 1
或 -1
。
選用:傳回回覆
對於主進程到渲染器的 IPC,沒有與 ipcRenderer.invoke
對應的功能。相反地,您可以從 ipcRenderer.on
回呼中將回覆傳送回主進程。
我們可以透過稍微修改前一個範例中的程式碼來示範這一點。在渲染器進程中,公開另一個 API,透過 counter-value
通道將回覆傳送回主進程。
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)),
counterValue: (value) => ipcRenderer.send('counter-value', value)
})
const counter = document.getElementById('counter')
window.electronAPI.onUpdateCounter((value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue.toString()
window.electronAPI.counterValue(newValue)
})
在主進程中,監聽 counter-value
事件並適當地處理它們。
// ...
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // will print value to Node console
})
// ...
模式 4:渲染器到渲染器
在 Electron 中,沒有直接的方法可以使用 ipcMain
和 ipcRenderer
模組在渲染器進程之間傳送訊息。若要實現此目的,您有兩個選項
- 使用主進程作為渲染器之間的訊息代理。這將涉及從一個渲染器傳送訊息到主進程,然後由主進程將訊息轉發到另一個渲染器。
- 將 MessagePort 從主進程傳遞到兩個渲染器。這將允許渲染器在初始設定後直接通訊。
物件序列化
Electron 的 IPC 實作使用 HTML 標準的 結構化複製演算法來序列化在進程之間傳遞的物件,這表示只有某些類型的物件可以透過 IPC 通道傳遞。
特別是,DOM 物件(例如 Element
、Location
和 DOMMatrix
)、由 C++ 類別支援的 Node.js 物件(例如 process.env
、Stream
的某些成員)以及由 C++ 類別支援的 Electron 物件(例如 WebContents
、BrowserWindow
和 WebFrame
)無法使用結構化複製進行序列化。