Electron 進(jìn)程間通信(IPC)方法詳解
Electron 是一個使用 JavaScript、HTML 和 CSS 構(gòu)建桌面應(yīng)用程序的框架,它是基于 Chromium 和 Node.js 構(gòu)建的,而 Chromium 本身是采用多進(jìn)程架構(gòu)的,所以 Electron 也是多進(jìn)程的。
Electron 是一個多進(jìn)程框架,它的進(jìn)程主要分為兩類:主進(jìn)程(Main Process) 和 渲染進(jìn)程(Renderer Process) ,兩者分工協(xié)作,共同完成桌面應(yīng)用的運行。
Electron 主進(jìn)程
每個 Electron 應(yīng)用有且僅有一個主進(jìn)程,它是應(yīng)用程序的入口點。主進(jìn)程在 Node.js 環(huán)境中運行,這意味著它具有使用 require 模塊和所有 Node.js API 的能力,擁有完整的系統(tǒng)權(quán)限。主進(jìn)程常用來負(fù)責(zé)應(yīng)用的生命周期管理(啟動、退出)、窗口創(chuàng)建、系統(tǒng)事件處理(如文件操作、菜單交互)等。主進(jìn)程通過 BrowserWindow 創(chuàng)建和管理窗口,每個窗口對應(yīng)一個獨立的渲染進(jìn)程。
Electron 渲染進(jìn)程
每個 Electron 應(yīng)用都會為 BrowserWindow 創(chuàng)建的窗口生成一個單獨的渲染器進(jìn)程。 渲染進(jìn)程負(fù)責(zé)渲染網(wǎng)頁內(nèi)容,處理用戶界面交互,運行于渲染進(jìn)程中的代碼要遵照網(wǎng)頁創(chuàng)建標(biāo)準(zhǔn)。渲染進(jìn)程以一個 HTML 文件作為渲染器進(jìn)程的入口點,這也意味著渲染進(jìn)程中無權(quán)直接訪問 require 或其他 Node.js API。
進(jìn)程間通信(IPC)
Electron 提供了一個特殊的預(yù)加載腳本,它將 Electron 的主進(jìn)程和渲染進(jìn)程橋接在一起,我們可以通過配置預(yù)加載腳本來實現(xiàn) Electron 不同進(jìn)程之間的通信。
預(yù)加載腳本在 BrowserWindow 構(gòu)造器中使用 webPreferences.preload 引入,它在渲染器加載網(wǎng)頁之前注入,所以在預(yù)加載腳本中可以訪問 document、window、部分權(quán)限的 Node.js 和 Electron 的 API。
// main.js 主進(jìn)程文件
const { app, BrowserWindow } = require('electron/main')
const path = require('node:path')
const createWindow = () => {
// 創(chuàng)建瀏覽器窗口
const win = new BrowserWindow({
width: 800, // 窗口寬度
height: 600, // 窗口高度
webPreferences: {
// sandbox: false, // 是否開啟沙盒模式
// nodeIntegration: false, // 是否開啟node集成
// contextIsolation: true, // 是否開啟上下文隔離
preload: path.join(__dirname, 'preload.js'), // 預(yù)加載腳本
},
})
win.webContents.openDevTools() // 打開窗口的開發(fā)者工具
// 把html文件加載到窗口中
win.loadFile('index.html')
}
在預(yù)加載腳本中,我們通過 Electron 中的 contextBridge.exposeInMainWorld 方法定義一個變量暴露給渲染器,在這個變量中可以添加主進(jìn)程中的一些 API,渲染器可以在全局 window 對象中訪問它。下面是預(yù)加載腳本的部分代碼,展示的是如何把 Node 和 Electron 的版本號暴露給渲染器。
// preload.js 預(yù)加載文件
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node, // node版本號
chrome: () => process.versions.chrome, // chrome版本號
electron: () => process.versions.electron, // electron版本后
// 除函數(shù)之外,我們也可以暴露變量
})
在 html 文件中我們可以直接獲取變量 versions,代碼展示如下:
<body>
<p id="version-info"></p>
</body>
<script>
// 渲染腳本
window.addEventListener('DOMContentLoaded', () => {
if (versionInfo) {
const versionInfoElement = document.getElementById('version-info')
versionInfoElement.innerHTML = `
<p>Node.js 版本: v${versionInfo?.node()}</p>
<p>Chrome 版本: v${versionInfo?.chrome()}</p>
<p>Electron 版本: v${versionInfo?.electron()}</p>
`
}
})
</script>
1. 模式一:渲染器進(jìn)程 => 主進(jìn)程
通常使用此模式從 web 頁面觸發(fā)預(yù)加載腳本中定義的事件,主進(jìn)程中監(jiān)聽此事件并處理相關(guān)內(nèi)容。下面展示的是設(shè)置窗口標(biāo)題的例子,只部分代碼,詳情請從 github 下載。
點擊查看代碼
// main.js 主進(jìn)程文件
// 監(jiān)聽渲染進(jìn)程的 set-title 消息,主進(jìn)程設(shè)置窗口標(biāo)題
ipcMain.on('set-title', (event, title) => {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win.setTitle(title)
})
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: title => ipcRenderer.send('set-title', title), // set-title 是自定義的頻道名稱,主進(jìn)程監(jiān)聽 set-title 頻道并設(shè)置窗口標(biāo)題
})
// html
<div class="demo-2 demo-box">
<p>修改窗口標(biāo)題</p>
<div>
標(biāo)題: <input id="title"/>
<button id="set-title-btn" type="button">設(shè)置標(biāo)題</button>
</div>
</div>
// setTitle.js 渲染腳本
window.addEventListener('DOMContentLoaded', () => {
const setButton = document.getElementById('set-title-btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value
if (window?.electronAPI) {
window.electronAPI.setTitle(title)
}
})
})
2. 模式二:渲染器進(jìn)程 <= 主進(jìn)程
我們構(gòu)建一個由原生操作系統(tǒng)菜單控制的數(shù)字計數(shù)器,由主進(jìn)程控制計數(shù)器的增減,頁面負(fù)責(zé)更新數(shù)據(jù)。在主進(jìn)程中,調(diào)用窗口實例的 webContents.send 方法向渲染進(jìn)程傳遞數(shù)據(jù)。
點擊查看代碼
// main.js 主進(jìn)程
const { app, BrowserWindow, Menu, ipcMain } = require('electron/main')
const path = require('node:path')
const createWindow = () => {
const win = new BrowserWindow({
width: 800, // 彈窗寬度
height: 600, // 彈窗高度
webPreferences: {
preload: path.join(__dirname, 'preload.js'), // 預(yù)加載腳本
},
})
// 打開窗口的開發(fā)者工具
win.webContents.openDevTools()
// 生成原生操作系統(tǒng)菜單
const menu = Menu.buildFromTemplate([
{
label: '計數(shù)器',
submenu: [
{
click: () => win.webContents.send('update-counter', 1), // update-counter 是自定義的頻道名稱
label: '加1',
},
{
click: () => win.webContents.send('update-counter', -1),
label: '減1',
},
],
},
])
// 設(shè)置窗口菜單
Menu.setApplicationMenu(menu)
// 把html文件加載到彈窗中
win.loadFile('index.html')
}
// repload.js 預(yù)加載腳本
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: callback => ipcRenderer.on('update-counter', (_event, value) => callback(value)), // 渲染進(jìn)程監(jiān)聽 update-counter 頻道并更新計數(shù)器
})
// html
<div class="demo-3 demo-box">
<p>
計數(shù)器:
<span id="counter">0</span>
</p>
</div>
// counter.js 計數(shù)器渲染腳本
window.addEventListener('DOMContentLoaded', () => {
let counter = document.getElementById('counter').innerText
counter = Number(counter)
// 渲染進(jìn)程監(jiān)聽主進(jìn)程,當(dāng)主進(jìn)程發(fā)出 update-counter 消息時,更新計數(shù)器
if (window.electronAPI?.onUpdateCounter) {
window.electronAPI.onUpdateCounter(value => {
counter += value
document.getElementById('counter').innerText = counter
})
}
})
頁面更新數(shù)據(jù)后,也可以向主進(jìn)程發(fā)送信息,讓主進(jìn)程知道頁面已更新,具體實現(xiàn)方式請查看源碼。頁面計數(shù)器更新及主進(jìn)程獲取更新信息的截圖如下:

3. 模式三:渲染器進(jìn)程 <=> 主進(jìn)程
雙向 IPC 的一個常見應(yīng)用是從渲染器進(jìn)程代碼調(diào)用主進(jìn)程模塊并等待結(jié)果。對于兩進(jìn)程間的相互通信,我們可以配合使用 ipcRenderer.invoke 和 ipcMain.handle 方法完成。現(xiàn)在我們實現(xiàn)一個例子,從渲染器進(jìn)程打開一個原生的文件對話框,并返回所選文件的路徑。
點擊查看代碼
// main.js 主進(jìn)程
const { app, BrowserWindow, Menu, ipcMain, dialog } = require('electron/main')
const path = require('node:path')
// 創(chuàng)建瀏覽器窗口
const createWindow = () => {
const win = new BrowserWindow({
width: 800, // 彈窗寬度
height: 600, // 彈窗高度
webPreferences: {
preload: path.join(__dirname, 'preload.js'), // 預(yù)加載腳本
},
})
// 打開窗口的開發(fā)者工具
win.webContents.openDevTools()
// 把html文件加載到彈窗中
win.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
// 監(jiān)聽渲染進(jìn)程的 open-file 頻道,顯示文件選擇框
ipcMain.handle('open-file', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog()
if (canceled) {
return
} else {
return filePaths[0]
}
})
// replod.js 預(yù)加載腳本
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('open-file'), // 渲染進(jìn)程觸發(fā)通信的頻道名稱open-file,主進(jìn)程監(jiān)聽open-file頻道并返回文件路徑
})
// html
<div class="demo-4 demo-box">
<p>
<button type="button" id="open-btn">打開文件</button>
文件路徑:
<strong id="filePath"></strong>
</p>
</div>
// openFile.js 渲染腳本
window.addEventListener('DOMContentLoaded', () => {
const electronAPI = window.electronAPI
if (electronAPI?.openFile) {
const openBtn = document.getElementById('open-btn')
const pathElement = document.getElementById('filePath')
openBtn.addEventListener('click', async () => {
const filePath = await electronAPI.openFile()
pathElement.innerText = filePath
})
}
})
主進(jìn)程與渲染進(jìn)程之間的通信大概可以通過以上幾種方式實現(xiàn),閱讀完代碼大家會發(fā)現(xiàn),進(jìn)程間通信主要使用的是 ipcRenderer.invoke/ipcMain.handle 和 ipcRenderer.send/ipcMain.on 兩種不同模式,這兩種模式的具體區(qū)別及使用場景我們后面再說。
github倉庫地址:https://hgithub.xyz/zhench0515/electron-ipc.git 或者 https://github.com/zhench0515/electron-ipc.git

浙公網(wǎng)安備 33010602011771號