Skip to content

Electron 通信鉴赏:场景实战与工程化进阶

· 10 min

先把盘子摆正:Electron 的通信,本质就是“不同世界如何说话”——主进程(Node 能力)vs 渲染进程(UI),中间靠预加载脚本(preload)搭桥。好消息是,常规项目 80% 的需求用两招就够了:ipcRenderer.invoke/ ipcMain.handle(问答式)+ webContents.send(广播式)。剩下 20% 的花活儿,交给场景来驱动。


一、通信全景图(很快过一遍)#

常用通道


二、最常用的桥:预加载脚本(一次写好,全局安心)#

electron/preload.ts

import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('api', {
// 请求-响应(优先)
openFile: (filters?: Electron.FileFilter[]) =>
ipcRenderer.invoke('dialog:openFile', filters),
// 事件流(主 -> 渲染)
onDownloadProgress: (cb: (p: { percent: number }) => void) => {
const listener = (_e, data) => cb(data)
ipcRenderer.on('download:progress', listener)
return () => ipcRenderer.removeListener('download:progress', listener)
},
})

渲染进程直接用:

const file = await window.api.openFile([{ name: 'Image', extensions: ['png','jpg'] }])
const off = window.api.onDownloadProgress(({ percent }) => { console.log(percent) })

安全小抄


三、场景 1:文件/目录选择(权限稳稳当当)#

主进程

electron/main.ts
import { ipcMain, dialog } from 'electron'
ipcMain.handle('dialog:openFile', async (_evt, filters) => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile'],
filters,
})
return canceled ? null : filePaths[0]
})

渲染进程

const file = await window.api.openFile([{ name: 'Text', extensions: ['txt','md'] }])
if (file) { /* 走你的业务 */ }

为什么要这样?


四、场景 2:下载任务与进度回传(主拉渲染看)#

主进程用 session 或三方库(如 electron-dl);这里示范自管进度并推给渲染。

import { session, BrowserWindow, ipcMain } from 'electron'
function setupDownload(win: BrowserWindow) {
session.defaultSession.on('will-download', (_e, item) => {
const total = item.getTotalBytes()
item.on('updated', (_evt, state) => {
if (state === 'progressing') {
const percent = Math.round((item.getReceivedBytes() / total) * 100)
win.webContents.send('download:progress', { percent })
}
})
item.once('done', (_evt, state) => {
win.webContents.send('download:progress', { percent: state === 'completed' ? 100 : -1 })
})
})
}
ipcMain.handle('download:start', async (_evt, url: string) => {
const win = BrowserWindow.getFocusedWindow()
if (win) await win.webContents.downloadURL(url)
return true
})

渲染进程

await window.api.downloadStart('https://example.com/big.zip')
const off = window.api.onDownloadProgress(({ percent }) => {
// percent: 0~100,-1 代表失败
})

要点


五、场景 3:自动更新提示(问主进程要结果,UI 在渲染)#

主进程

import { autoUpdater } from 'electron-updater'
import { BrowserWindow } from 'electron'
function setupUpdater(win: BrowserWindow) {
autoUpdater.on('update-available', (info) => {
win.webContents.send('updater:event', { type: 'available', info })
})
autoUpdater.on('download-progress', (p) => {
win.webContents.send('updater:event', { type: 'progress', percent: p.percent })
})
autoUpdater.on('update-downloaded', () => {
win.webContents.send('updater:event', { type: 'ready' })
})
}
ipcMain.handle('updater:check', async () => {
const r = await autoUpdater.checkForUpdates()
return { version: r?.updateInfo?.version }
})
ipcMain.handle('updater:install', async () => {
autoUpdater.quitAndInstall()
return true
})

渲染进程

await window.api.updaterCheck()
window.api.onUpdaterEvent((e) => {
if (e.type === 'ready') {
// 弹窗:确认后
window.api.updaterInstall()
}
})

套路


六、场景 4:窗口间同步(主进程当“消息总线”)#

主进程维护窗口池:

const windows = new Set<BrowserWindow>()
function broadcast(channel: string, data: any, exclude?: number) {
windows.forEach(w => {
if (w.webContents.id !== exclude) w.webContents.send(channel, data)
})
}
// 渲染 A 通知“主题改了”,主进程广播给其他窗口
ipcMain.on('theme:update', (evt, theme) => {
broadcast('theme:changed', theme, evt.sender.id)
})

渲染 A

ipcRenderer.send('theme:update', 'dark')

渲染 B

ipcRenderer.on('theme:changed', (_e, theme) => applyTheme(theme))

为什么不直接窗口间互相发?


七、场景 5:后台任务(CPU 劲活儿别在渲染进程硬抗)#

child_process.forkworker_threads,结果回到主进程,再发给渲染。

主进程

import { fork } from 'node:child_process'
import { ipcMain, BrowserWindow } from 'electron'
import path from 'node:path'
ipcMain.handle('task:heavy', async (_evt, input: any) => {
return new Promise((resolve, reject) => {
const p = fork(path.join(__dirname, 'worker.js'))
p.send({ input })
p.on('message', (msg) => {
if (msg.progress != null) {
BrowserWindow.getFocusedWindow()?.webContents.send('task:progress', msg)
}
if (msg.done) resolve(msg.result)
})
p.on('error', reject)
p.on('exit', (code) => code !== 0 && reject(new Error(`worker exit ${code}`)))
})
})

worker.js

process.on('message', ({ input }) => {
let acc = 0
for (let i = 0; i <= 100; i++) {
// ...重活
process.send?.({ progress: i })
}
process.send?.({ done: true, result: acc })
})

渲染进程

const result = await window.api.taskHeavy({ foo: 1 })
window.api.onTaskProgress(({ progress }) => { /* 更新进度条 */ })

要点


八、场景 6:打印 / 导出 PDF(传路径,不传二进制)#

主进程

ipcMain.handle('print:pdf', async (evt, { file }) => {
const wc = BrowserWindow.fromWebContents(evt.sender)!.webContents
const data = await wc.printToPDF({ landscape: false })
const fs = await import('node:fs/promises')
await fs.writeFile(file, data)
return true
})

渲染进程

const ok = await window.api.printPdf({ file: '/Users/me/output.pdf' })

别在 IPC 里传巨大的 Buffer,走文件系统落地更稳。


九、工程化进阶(把“能用”打磨成“耐用”)#


十、踩坑速记(遇见过就忘不了)#


收个尾#

通信的高级感,不在于你会多少 API,而在于“场景下的组合拳”:什么时候问答、什么时候广播、什么时候落到磁盘、什么时候忍痛割爱别传大对象。前半场把常见场景掰开揉碎,后半场给出工程化守则——够你从“能跑”进化到“敢上生产”。剩下的,就是多做几回,踩过一次坑就记一辈子。