人人都会AI编程

10.1 多窗口创建与实例管理

更新时间:2026-07-11

真实桌面应用很少只有一个窗口。设置页、预览窗、弹窗确认、独立工具栏……这些都需要同时维护多个 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: falsealwaysOnTop: true 实现浮动工具栏、取色器等功能。

子窗口关闭时,务必在 closed 事件中将其从 Map 移除,避免内存泄漏和对已销毁窗口的引用。

10.1.3 管理窗口生命周期与内存安全

在实际项目中,多窗口最常引发的问题并不是创建,而是忘记销毁。每一个 BrowserWindow 实例都会占用上百 MB 内存(含独立的渲染进程),如果主进程一直持有已关闭窗口的引用,垃圾回收不会释放对应的 C++ 对象。

安全的窗口管理遵守三条原则:

  1. 始终监听 closed 事件并清除引用。 不要依赖 destroy() 之后的变量赋值,事件回调更可靠。
  2. 避免在数组或 Map 以外持有窗口引用。 集中管理能够快速定位和排查泄漏。
  3. 应用退出前销毁所有窗口。 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 转发给目标窗口。这能精确控制流向,适合窗口数量较多的情况。
  • 使用 MessageChannelBroadcastChannel(需 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 应用就能在功能扩展的同时保持结构清晰、资源可控。下一节将深入介绍窗口之间的高级通信模式,以及如何组织复杂应用的窗口架构。