真实桌面应用很少只有一个窗口。设置页、预览窗、弹窗确认、独立工具栏……这些都需要同时维护多个 BrowserWindow 实例。如果管理不善,很容易出现窗口泄漏、重复创建或进程间通信混乱。本节会给出经过实践验证的多窗口管理方案,确保代码清晰、资源安全。
10.1.1 创建第二个窗口
创建新窗口与创建主窗口的 API 完全一样。最常见的做法是在渲染进程中触发一个事件,通过 IPC 通知主进程打开新窗口。
主进程(main.js):
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
let mainWindow = null;
const childWindows = new Map(); // 用于跟踪子窗口
function createMainWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: { preload: path.join(__dirname, 'preload.js') }
});
mainWindow.loadFile('index.html');
}
function createChildWindow(route, params) {
// 如果已经存在相同路由的窗口,激活并返回,避免重复创建
if (childWindows.has(route)) {
const existing = childWindows.get(route);
existing.focus();
return existing;
}
const child = new BrowserWindow({
width: 800,
height: 600,
parent: mainWindow, // 设为主窗口的子窗口
modal: false, // 非模态
webPreferences: { preload: path.join(__dirname, 'preload.js') }
});
child.loadURL(`file://${path.join(__dirname, 'index.html')}#/settings`);
// 或者 child.loadFile('settings.html')
child.on('closed', () => {
childWindows.delete(route); // 清理引用
});
childWindows.set(route, child);
return child;
}
// 监听渲染进程的请求
ipcMain.handle('open-child-window', (event, route, params) => {
return createChildWindow(route, params);
});
app.whenReady().then(createMainWindow);
渲染进程触发(任何组件内):
// 通过预加载暴露的 API
window.electronAPI.openChildWindow('settings', { tab: 'general' });
预加载脚本暴露方法(preload.js):
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
openChildWindow: (route, params) => ipcRenderer.invoke('open-child-window', route, params)
});
10.1.2 多窗口实例管理策略
在上述代码中,我们用 Map 来存储打开的子窗口,以路由名称为键。这可以防止用户反复点击“设置”按钮导致窗口堆积。根据实际场景,你还可以选择更灵活的策略:
- 单例窗口:全局只能存在一个该类型窗口,重复请求时激活已有窗口。
- 多例窗口:允许同时打开多个实例(如多个文档编辑窗口),此时 Map 的键可以使用唯一 ID 或文件路径。
- 模态窗口:设置
modal: true并指定parent,子窗口会阻塞父窗口操作,适用于确认对话框或强制设置页。 - 无边框/工具窗口:配合
frame: false、alwaysOnTop: true实现浮动工具栏、取色器等功能。
子窗口关闭时,务必在 closed 事件中将其从 Map 移除,避免内存泄漏和对已销毁窗口的引用。
10.1.3 管理窗口生命周期与内存安全
在实际项目中,多窗口最常引发的问题并不是创建,而是忘记销毁。每一个 BrowserWindow 实例都会占用上百 MB 内存(含独立的渲染进程),如果主进程一直持有已关闭窗口的引用,垃圾回收不会释放对应的 C++ 对象。
安全的窗口管理遵守三条原则:
- 始终监听
closed事件并清除引用。 不要依赖destroy()之后的变量赋值,事件回调更可靠。 - 避免在数组或 Map 以外持有窗口引用。 集中管理能够快速定位和排查泄漏。
- 应用退出前销毁所有窗口。
app.on('before-quit')中可以调用BrowserWindow.getAllWindows()遍历销毁,但浏览器窗口的destroy()通常也会触发closed事件,需要注意防止多次处理。
app.on('before-quit', () => {
// 关闭所有窗口,保证渲染进程彻底退出
BrowserWindow.getAllWindows().forEach(win => {
if (!win.isDestroyed()) win.destroy();
});
});
10.1.4 窗口间通信的方式
多窗口之间经常需要同步状态,比如设置窗口修改了主题,主窗口要立刻响应。在 Electron 中,不同渲染进程不能直接通信(它们运行在独立沙箱中),必须通过主进程转发。
常用模式:
- 主进程中转:子窗口发送 IPC 消息给主进程,主进程再通过
webContents.send转发给目标窗口。这能精确控制流向,适合窗口数量较多的情况。 - 使用
MessageChannel或BroadcastChannel(需 contextIsolation 配置):支持渲染进程之间直接通信,但会引入安全风险,不推荐在启用上下文隔离的环境中使用。 - 共享状态 (Store):如果只是为了同步数据(如用户偏好),可以让所有窗口读取同一个本地文件或使用
electron-store,通过 IPC 通知重读即可。
示例:子窗口修改主题后通知主窗口刷新:
// 子窗口渲染进程
window.electronAPI.sendToMain('theme-changed', 'dark');
// 主进程
ipcMain.on('theme-changed', (event, theme) => {
// 转发给主窗口的渲染进程
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('apply-theme', theme);
}
});
10.1.5 避免常见的多窗口陷阱
- 不要用
new BrowserWindow()创建无界窗口再隐藏作为后台服务。 如果需要后台计算或定时任务,应使用Web Workers或在主进程中启动 Node.js Worker。额外的隐藏窗口会浪费大量资源。 - 父子窗口的层级问题。 如果
parent窗口最小化或关闭,子窗口通常也会自动处理,但跨平台行为略有差异(尤其在 Linux 环境),需实际测试。 - URL 策略要一致。 当多窗口加载不同的 HTML 文件或路由时,务必保证每个窗口加载的资源都是安全的,且预加载脚本逻辑覆盖所有路径。
掌握这些多窗口创建的规范与实例管理模式,你的 Electron 应用就能在功能扩展的同时保持结构清晰、资源可控。下一节将深入介绍窗口之间的高级通信模式,以及如何组织复杂应用的窗口架构。