跳到主要內容

程序間通訊

程序間通訊(IPC)是在 Electron 中建構功能豐富的桌面應用程式的關鍵部分。由於主程序和渲染程序在 Electron 的程序模型中具有不同的職責,因此 IPC 是執行許多常見任務的唯一方法,例如從您的 UI 呼叫原生 API 或從原生選單觸發您的網頁內容中的變更。

IPC 通道

在 Electron 中,程序透過使用 ipcMainipcRenderer 模組,透過開發人員定義的「通道」傳遞訊息來進行通訊。這些通道是任意的(您可以將它們命名為任何您想要的名稱)並且是雙向的(您可以在兩個模組中使用相同的通道名稱)。

在本指南中,我們將介紹一些基本的 IPC 模式,並提供具體的範例,您可以將其用作應用程式碼的參考。

了解上下文隔離的程序

在繼續進行實作細節之前,您應該熟悉使用 預載腳本 在上下文隔離的渲染程序中匯入 Node.js 和 Electron 模組的概念。

  • 如需 Electron 程序模型的完整概述,您可以閱讀程序模型文件
  • 如需使用 contextBridge 模組從您的預載腳本公開 API 的入門知識,請查看上下文隔離教學

模式 1:渲染程序到主程序(單向)

若要從渲染程序向主程序觸發單向 IPC 訊息,您可以使用 ipcRenderer.send API 發送訊息,然後由 ipcMain.on API 接收。

您通常使用此模式從您的網頁內容呼叫主程序 API。我們將透過建立一個可以程式化變更其視窗標題的簡單應用程式來示範此模式。

對於此示範,您需要在您的主程序、渲染程序和預載腳本中新增程式碼。完整的程式碼如下,但我們將在以下章節中個別說明每個檔案。

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()
})

1. 使用 ipcMain.on 監聽事件

在主程序中,使用 ipcMain.on API 在 set-title 通道上設定 IPC 監聽器

main.js(主程序)
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.htmlpreload.js 入口點!

2. 透過預載公開 ipcRenderer.send

若要將訊息傳送到上面建立的監聽器,您可以使用 ipcRenderer.send API。預設情況下,渲染程序無法存取 Node.js 或 Electron 模組。作為應用程式開發人員,您需要選擇要使用 contextBridge API 從您的預載腳本公開哪些 API。

在您的預載腳本中,新增以下程式碼,這將向您的渲染程序公開一個全域 window.electronAPI 變數。

preload.js(預載腳本)
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 檔案中,新增一個基本使用者介面,包含一個文字輸入框和一個按鈕

index.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 功能

renderer.js(渲染程序)
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.invokeipcMain.handle 配對來完成。

在以下範例中,我們將從渲染程序開啟一個原生檔案對話方塊,並傳回選取檔案的路徑。

對於此示範,您需要在您的主程序、渲染程序和預載腳本中新增程式碼。完整的程式碼如下,但我們將在以下章節中個別說明每個檔案。

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()
})

1. 使用 ipcMain.handle 監聽事件

在主程序中,我們將建立一個 handleFileOpen() 函式,該函式會呼叫 dialog.showOpenDialog 並傳回使用者選取的檔案路徑值。每當從渲染程序透過 dialog:openFile 通道傳送 ipcRender.invoke 訊息時,此函式都會用作回呼。然後,傳回值會作為 Promise 傳回給原始的 invoke 呼叫。

關於錯誤處理的說明

透過主程序中的 handle 擲出的錯誤不是透明的,因為它們被序列化,並且僅向渲染程序提供來自原始錯誤的 message 屬性。請參閱 #24427 以了解詳細資訊。

main.js(主程序)
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.htmlpreload.js 入口點!

2. 透過預載公開 ipcRenderer.invoke

在預載腳本中,我們公開了一個單行 openFile 函式,該函式呼叫並傳回 ipcRenderer.invoke('dialog:openFile') 的值。我們將在下一步中使用此 API 從我們的渲染器使用者介面呼叫原生對話方塊。

preload.js(預載腳本)
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
安全警告

由於安全考量,我們不會直接公開整個 ipcRenderer.invoke API。請務必盡可能限制渲染程序對 Electron API 的存取。

3. 建構渲染程序 UI

最後,讓我們建構載入到我們的 BrowserWindow 中的 HTML 檔案。

index.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 元素,將用於顯示選取檔案的路徑。使這些部分運作將需要在渲染程序腳本中新增幾行程式碼

renderer.js(渲染程序)
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 元素中顯示選取的檔案路徑。

注意:舊版方法

ipcRenderer.invoke API 在 Electron 7 中新增,作為開發人員友善的方式來處理來自渲染程序的雙向 IPC。但是,此 IPC 模式存在幾種替代方法。

盡可能避免舊版方法

我們建議盡可能使用 ipcRenderer.invoke。以下雙向渲染程序到主程序的模式是為了歷史目的而記錄的。

資訊

對於以下範例,我們直接從預載腳本呼叫 ipcRenderer,以保持程式碼範例簡潔。

使用 ipcRenderer.send

我們用於單向通訊的 ipcRenderer.send API 也可以用於執行雙向通訊。這是 Electron 7 之前建議用於透過 IPC 進行非同步雙向通訊的方式。

preload.js(預載腳本)
// 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')
main.js(主程序)
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 將訊息傳送到主程序,並同步等待回應。

main.js(主程序)
const { ipcMain } = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // prints "ping" in the Node console
event.returnValue = 'pong'
})
preload.js(預載腳本)
// 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 相同。

為了示範此模式,我們將建構一個由原生作業系統選單控制的數字計數器。

對於此示範,您需要在您的主程序、渲染程序和預載腳本中新增程式碼。完整的程式碼如下,但我們將在以下章節中個別說明每個檔案。

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()
})

1. 使用 webContents 模組傳送訊息

對於此示範,我們首先需要在主程序中使用 Electron 的 Menu 模組建構一個自訂選單,該選單使用 webContents.send API 將 IPC 訊息從主程序傳送到目標渲染器。

main.js(主程序)
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.htmlpreload.js 入口點!

2. 透過預載公開 ipcRenderer.on

與先前的渲染程序到主程序範例類似,我們在預載腳本中使用 contextBridgeipcRenderer 模組,以將 IPC 功能公開給渲染程序

preload.js(預載腳本)
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,而不是透過上下文橋接公開它。

preload.js(預載腳本)
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 元素,我們將使用該元素來顯示值

index.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>Menu Counter</title>
</head>
<body>
Current value: <strong id="counter">0</strong>
<script src="./renderer.js"></script>
</body>
</html>

最後,為了使值在 HTML 文件中更新,我們將新增幾行 DOM 操作,以便在我們觸發 update-counter 事件時更新 #counter 元素的值。

renderer.js(渲染程序)
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 通道將回覆傳送回主程序。

preload.js(預載腳本)
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)
})
renderer.js(渲染程序)
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 事件並適當地處理它們。

main.js(主程序)
// ...
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // will print value to Node console
})
// ...

模式 4:渲染程序到渲染程序

在 Electron 中,沒有使用 ipcMainipcRenderer 模組在渲染程序之間直接傳送訊息的方法。若要實現此目的,您有兩個選項

  • 使用主程序作為渲染器之間的訊息代理。這將涉及從一個渲染器向主程序發送訊息,主程序將訊息轉發到另一個渲染器。
  • MessagePort 從主程序傳遞到兩個渲染器。這將允許渲染器在初始設定後直接通訊。

物件序列化

Electron 的 IPC 實作使用 HTML 標準 結構化複製演算法 來序列化在程序之間傳遞的物件,這表示只有特定類型的物件可以透過 IPC 通道傳遞。

特別是,DOM 物件(例如 ElementLocationDOMMatrix)、由 C++ 類別支援的 Node.js 物件(例如 process.envStream 的某些成員)以及由 C++ 類別支援的 Electron 物件(例如 WebContentsBrowserWindowWebFrame)無法使用結構化複製進行序列化。