6月4日 14:00

Electron多窗口管理:IPC通信、MessagePort和窗口状态恢复

Electron 应用超过一个窗口就会遇到两个问题:怎么管(创建、销毁、引用回收)、怎么通(主窗口改了设置,设置窗口怎么知道)。管理不好就内存泄漏,通信不好就数据不一致。这篇文章把多窗口管理和 IPC 通信的常用模式讲清楚。

窗口管理

创建不同类型的窗口

javascript
const { BrowserWindow } = require('electron') // 主窗口 function createMainWindow() { const win = new BrowserWindow({ width: 1200, height: 800, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') } }) win.loadFile('index.html') return win } // 设置窗口(模态,附属于主窗口) function createSettingsWindow(parent) { const win = new BrowserWindow({ width: 600, height: 400, parent: parent, modal: true, // 模态:打开时主窗口不可操作 show: false, // 先不显示,等 ready-to-show webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') } }) win.loadFile('settings.html') win.once('ready-to-show', () => win.show()) // 避免白屏闪烁 return win } // 工具窗口(无边框、置顶、透明) function createToolWindow() { const win = new BrowserWindow({ width: 300, height: 200, frame: false, // 无标题栏 alwaysOnTop: true, // 始终置顶 transparent: true, // 透明背景 resizable: false }) win.loadFile('tool.html') return win }

窗口引用管理

用一个 Map 统一管理所有窗口,防止引用泄漏:

javascript
const windows = new Map() function createWindow(id, options) { // 如果已存在,聚焦而不是再创建 if (windows.has(id) && !windows.get(id).isDestroyed()) { windows.get(id).focus() return windows.get(id) } const win = new BrowserWindow(options) windows.set(id, win) win.on('closed', () => { windows.delete(id) // 窗口关闭时移除引用,允许 GC }) return win } // 使用 createWindow('main', { width: 1200, height: 800 }) createWindow('settings', { width: 600, height: 400, parent: windows.get('main'), modal: true })

最常见的内存泄漏:窗口关闭了但引用还在——win.on('closed') 里必须把引用清除,否则 BrowserWindow 对象不会被回收。

单例窗口

某些窗口只能有一个(如设置窗口),重复点击应该聚焦已有窗口而不是新开:

javascript
let settingsWindow = null function openSettings() { if (settingsWindow && !settingsWindow.isDestroyed()) { settingsWindow.focus() return } settingsWindow = new BrowserWindow({ /* ... */ }) settingsWindow.on('closed', () => { settingsWindow = null }) }

isDestroyed() 检查很关键——窗口可能已经被 destroy() 了但变量还没清空。

IPC 通信

Electron 的 IPC 分两种方向:渲染→主(invoke/send)、主→渲染(send/webContents)。

现代 IPC 模式(contextIsolation + preload)

Electron 12+ 默认开启 contextIsolation,渲染进程不能直接用 require('electron')。正确做法是通过 preload 暴露安全 API:

javascript
// preload.js const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('electronAPI', { // invoke: 等待主进程返回结果(Promise) getSettings: () => ipcRenderer.invoke('get-settings'), saveSettings: (settings) => ipcRenderer.invoke('save-settings', settings), // send/on: 单向通知,不等返回 onSettingsChanged: (callback) => { ipcRenderer.on('settings-changed', (event, settings) => callback(settings)) } })
javascript
// main.js const { ipcMain } = require('electron') // invoke 对应 handle ipcMain.handle('get-settings', () => { return store.get('settings', { theme: 'light', fontSize: 14 }) }) ipcMain.handle('save-settings', (event, settings) => { store.set('settings', settings) // 通知所有窗口设置变了 BrowserWindow.getAllWindows().forEach(win => { win.webContents.send('settings-changed', settings) }) return true })

invoke/handle 返回 Promise,适合需要返回值的场景。send/on 单向,适合通知类消息。

窗口间通信

两个渲染窗口之间不能直接 IPC,必须经过主进程中转:

shell
渲染进程A → ipcRenderer.invoke() → 主进程 ipcMain.handle() → win.webContents.send() → 渲染进程B

主进程充当消息总线:

javascript
// 主进程:转发消息 ipcMain.on('relay-message', (event, targetWindowId, channel, data) => { const targetWin = windows.get(targetWindowId) if (targetWin && !targetWin.isDestroyed()) { targetWin.webContents.send(channel, data) } })

MessagePort:双向通信通道

Electron 14+ 支持 MessagePort,可以建立渲染进程间的双向通信通道,不需要每次都过主进程:

javascript
// 主进程:为两个窗口创建通道 ipcMain.handle('create-channel', (event, targetId) => { const targetWin = windows.get(targetId) if (!targetWin) return null const { port1, port2 } = new MessageChannelMain() // 给发起方 port1 event.sender.postMessage('channel-created', { port: port1 }, [port1]) // 给目标方 port2 targetWin.webContents.postMessage('channel-created', { port: port2 }, [port2]) })

渲染进程收到 port 后就可以直接通信:

javascript
// 渲染进程 window.electronAPI.onChannelCreated((port) => { port.onmessage = (event) => { console.log('收到消息:', event.data) } port.postMessage('hello from the other side') })

MessagePort 适合实时数据流(如编辑器里主窗口和预览窗口的同步),比每次 invoke 少一次主进程中转。

窗口状态持久化

记住窗口位置和大小,下次打开时恢复:

javascript
function createMainWindow() { const bounds = store.get('windowBounds', { width: 1200, height: 800, x: undefined, y: undefined }) const win = new BrowserWindow({ ...bounds, webPreferences: { /* ... */ } }) // 窗口移动或缩放时保存 win.on('resize', () => saveBounds(win)) win.on('move', () => saveBounds(win)) return win } function saveBounds(win) { clearTimeout(saveBounds.timer) saveBounds.timer = setTimeout(() => { store.set('windowBounds', win.getBounds()) }, 500) // 防抖,避免频繁写入 }

要注意:如果用户外接显示器拔掉了,保存的坐标可能在屏幕外,窗口"消失"了。恢复时检查坐标是否在可见区域内:

javascript
const { screen } = require('electron') function ensureVisible(win) { const bounds = win.getBounds() const displays = screen.getAllDisplays() const visible = displays.some(d => { const { x, y, width, height } = d.workArea return bounds.x >= x && bounds.x < x + width && bounds.y >= y && bounds.y < y + height }) if (!visible) win.center() }
标签:Electron