手把手使用 Vite + React + Electron 构建的现代桌面应用程序模板 - 窗口管理

371 阅读4分钟

手把手使用 Vite + React + Electron 构建的现代桌面应用程序模板

一个使用 Vite + React + Electron 构建的现代桌面应用程序模板。

仓库地址:github.com/leaf0412/vi…

手把手使用 Vite + React + Electron 构建的现代桌面应用程序模板 一 初始化

项目结构

├── electron/               # Electron 主进程相关代码
│   ├── config/            # 配置文件
│   ├── handlers/          # 事件处理程序
│   ├── main.ts           # 主进程入口文件
│   └── preload.ts        # 预加载脚本
├── src/                   # 渲染进程源代码
│   ├── assets/           # 静态资源
│   ├── App.tsx           # 主应用组件
│   └── main.tsx          # 渲染进程入口文件
├── public/                # 静态资源目录
└── dist-electron/         # 编译后的 Electron 代码

1. 为什么需要窗口管理 WindowManager?

1.1 解决的核心问题

  1. 窗口管理混乱

    • 传统方式下每个窗口独立创建和管理,容易造成代码重复和管理混乱
    • 不同窗口之间状态同步困难
    • 窗口生命周期管理复杂
  2. 通信机制不统一

    • 窗口间通信方式不一致
    • IPC 事件分散在各处
    • 缺乏统一的事件处理机制
  3. 配置重复

    • 窗口配置分散
    • 相似配置大量重复
    • 维护成本高

1.2 设计目标

  1. 统一管理

    class WindowManager {
      main: BrowserWindow | null = null;
      group = new Map();
    }
    

    为什么这样设计?

    • 单一职责原则:一个类专注于窗口管理
    • 集中管理:所有窗口状态在一处维护
    • 便于扩展:统一的接口便于添加新功能
  2. 灵活配置

    type WindowOptions = BrowserWindowConstructorOptions & {
      id?: number;
      isMainWin?: boolean;
      route?: string;
      isMultiWindow?: boolean;
      parentId?: number;
      maximize?: boolean;
    };
    

    为什么需要这些配置?

    • isMainWin: 主窗口需要特殊处理(如关闭行为)
    • route: 支持路由化管理,实现窗口复用
    • isMultiWindow: 控制是否允许多个相同窗口
    • parentId: 支持父子窗口关系,实现模态窗口

2. 如何实现?

2.1 窗口创建的智能处理

createWindow(options?: WindowOptions) {
  // 1. 配置合并
  const args = Object.assign({}, this.windowOptionsConfig, options);

  // 2. 窗口复用检查
  for (const i in this.group) {
    const currentWindow = this.getWindow(Number(i));
    const { route, isMultiWindow } = this.group.get(i);
    if (currentWindow && route === args.route && !isMultiWindow) {
      currentWindow.focus();
      return currentWindow;
    }
  }

  // 3. 创建新窗口
  const win = new BrowserWindow(args);
  
  // 4. 注册到管理器
  this.group.set(win.id, {
    route: args.route || '',
    isMultiWindow: args.isMultiWindow || false,
  });
}

实现要点:

  1. 配置合并:为什么使用 Object.assign?

    • 保留默认配置
    • 允许覆盖自定义配置
    • 确保配置完整性
  2. 窗口复用:为什么要复用?

    • 减少资源消耗
    • 提升应用性能
    • 确保功能一致性
  3. 状态管理:为什么使用 Map?

    • 高效的键值对存储
    • 便于查找和更新
    • 支持动态属性

2.2 IPC 通信设计

const Events = {
  WINDOW_NEW: 'WINDOW_NEW',
  WINDOW_CLOSED: 'WINDOW_CLOSED',
  // ...
} as const;

initIpcHandlers() {
  ipcMain.handle(Events.WINDOW_CLOSED, (_event, winId) => {
    if (winId) {
      this.getWindow(Number(winId))?.close();
      this.group.delete(winId);
    } else {
      this.closeAllWindow();
    }
  });
}

为什么这样设计?

  • 常量枚举:避免字符串错误
  • 统一处理:集中的事件管理
  • 类型安全:TypeScript 类型检查

3. 如何使用?

3.1 基础使用

// 1. 创建管理器实例
const windowManager = new WindowManager();

// 2. 初始化 IPC 处理器
windowManager.initIpcHandlers();

// 3. 创建主窗口
const mainWindow = windowManager.createWindow({
  isMainWin: true,
  width: 800,
  height: 600
});

3.2 创建子窗口

// 从渲染进程创建新窗口
ipcRenderer.invoke(Events.WINDOW_NEW, {
  route: '/settings',
  width: 400,
  height: 300,
  parentId: currentWindowId
});

3.3 窗口通信

// 在渲染进程中
// 最小化窗口
ipcRenderer.invoke(Events.WINDOW_MINI, windowId);

// 关闭窗口
ipcRenderer.invoke(Events.WINDOW_CLOSED, windowId);

// 获取窗口边界
const bounds = await ipcRenderer.invoke(Events.WINDOW_GET_BOUNDS);

3.4 最佳实践

  1. 窗口创建

    // 好的实践
    const settingsWindow = windowManager.createWindow({
      route: '/settings',
      isMultiWindow: false,  // 确保只有一个设置窗口
      width: 400,
      height: 300
    });
    
    // 避免这样做
    const win = new BrowserWindow({ width: 400, height: 300 });
    
  2. 事件处理

    // 好的实践
    windowManager.initIpcHandlers();
    // 清理时
    windowManager.destroyIpcHandlers();
    
    // 避免这样做
    ipcMain.on('some-event', () => {});  // 没有统一管理
    
  3. 窗口管理

    // 好的实践
    if (windowManager.getWindow(windowId)) {
      windowManager.getWindow(windowId).focus();
    }
    
    // 避免这样做
    const allWindows = BrowserWindow.getAllWindows();
    const targetWindow = allWindows.find(w => w.id === windowId);
    

4. 实际应用场景

4.1 主窗口应用

// app.ts
const windowManager = new WindowManager();

app.on('ready', () => {
  windowManager.initIpcHandlers();
  
  // 创建主窗口
  windowManager.createWindow({
    isMainWin: true,
    route: '/',
    width: 1200,
    height: 800,
    maximize: true
  });
});

// 确保应用退出时清理资源
app.on('window-all-closed', () => {
  windowManager.destroyIpcHandlers();
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

4.2 模态窗口

// 在渲染进程中打开设置窗口
async function openSettings() {
  const currentWindowId = await window.electron.getCurrentWindowId();
  
  window.electron.createWindow({
    route: '/settings',
    width: 500,
    height: 400,
    parentId: currentWindowId,
    modal: true,  // 模态窗口
    resizable: false
  });
}

4.3 多窗口应用

// 打开多个文档窗口
function openDocument(documentId: string) {
  windowManager.createWindow({
    route: `/document/${documentId}`,
    isMultiWindow: true,  // 允许多个文档窗口
    width: 800,
    height: 600
  });
}

5. 总结

WindowManager 的设计理念是"统一管理、灵活配置、简单使用"。通过:

  1. 集中的窗口管理
  2. 统一的通信机制
  3. 灵活的配置系统

解决了 Electron 应用中窗口管理的常见问题。使用时要注意:

  1. 合理使用窗口复用机制
  2. 正确处理窗口生命周期
  3. 及时清理资源

通过遵循这些原则和最佳实践,可以构建出稳定、高效的多窗口 Electron 应用。