electron窗口管理封装和页面通讯

一、项目总结

electron是多窗口应用,因此如果分散的定义窗口,难以管理和操作窗口事件。为了让代码更模块化和易维护,我们可以封装一个工具对象来处理常用功能。

我们的工具对象包含以下核心功能:

  • 创建普通窗口,父子窗口
  • 对窗口进行显示、隐藏、查找、放大、缩小等操作
  • 通过指定的窗口id向聚焦的窗口发送消息等操作。

二、工具管理函数

electron在开发环境中,可以直接使用开发服务器的地址;而在生产环境中,则需要加载本地文件的地址,因此要进行区分。

js 复制代码
import { BrowserWindow } from 'electron';
import { join } from 'path';
declare type EWindow = Electron.BrowserWindow;

// 配置public 文件目录路径,VITE_DEV_SERVER_URL表示vite默认生成的服务url
process.env.PUBLIC = process.env.VITE_DEV_SERVER_URL
  ? join(process.env.DIST_ELECTRON, '../public')
  : process.env.DIST;
  
// 配置构建时输出文件目录路径
process.env.DIST_ELECTRON = join(__dirname, '..');
  
//窗口配置参数
const defaultConfig = {
  width: 1200,
  height: 800,
  show: true,
  id: '',
  parentId: '', //父窗口id  创建父子窗口 -- 子窗口永远显示在父窗口顶部 【父窗口可以操作】
  autoHideMenuBar: true,
  isMainWin: false, //是否主窗口(当为true时会替代当前主窗口)
  router: '', //路由
  ...(process.platform === 'linux' ? { icon } : {}),
  webPreferences: {
    preload:join(__dirname, '../preload/index.js'),
    nodeIntegration: true, //在网页中集成Node
    enableWebSQL: false,
    webviewTag: true,
    contextIsolation: false,
  },
};

//工具类函数
class WinTools {
     constructor() {
            //窗口管理列表
            const windowsList = new Map();
     },
     //创建窗口
     createWin = (config?: object): EWindow => {
          const params = {
            ...defaultConfig,
            ...config,
          };
       
          const window = new BrowserWindow(params);

          if (url) {
            window.webContents.openDevTools({
              mode: 'bottom',
            });
            window.loadURL(`${url}/${params.router}.html`);
          } else {
            window.loadFile(join(process.env.DIST, `/${params.router}.html`));
          }

          //注册管理列表
          this.windowsList.set(params.id, window);
          
          //注册父子窗口
          if (params.parentId) {
            params.parent = this.getWindow(params.parentId);
            this.setParentWindow(params.id, params.parentId);
          }

          return window;
     };
     //通过id判断窗口是否注册
    isWindow = (id: string): boolean => {
      if (!this.windowsList.has(id)) {
        console.log('没有这个窗口id');
      }
      return this.windowsList.has(id);
    };

    //通过id显示窗口
    windowShow = (id: string): void => {
      if (!isWindow(id)) return;
      this.windowsList.get(id).show();
      windowsList.get(id).focus();
    };

    //通过id隐藏窗口
    const windowHide = (id: string): void => {
      if (!this.isWindow(id)) return;
      windowsList.get(id).hide();
    };

    //通过id最大化窗口
    windowMax = (id: string): void => {
      if (!this.isWindow(id)) return;
      windowsList.get(id).maximize();
      windowsList.get(id).webContents.send('isMaxWin', 'max');
    };

    //通过id最小化窗口
    windowMin = (id: string): void => {
      if (!this.isWindow(id)) return;
      windowsList.get(id).minimize();
    };

    //通过id还原窗口
    windowRestore = (id: string): void => {
      if (!this.isWindow(id)) return;
      windowsList.get(id).restore();
      windowsList.get(id).webContents.send('isMaxWin', 'min');
    };

    //关闭单个窗口
    windowClose = (id: string): void => {
      if (!this.isWindow(id)) return;
      windowsList.get(id).close();
    };

    //重新加载
    windowReload = (id: string): void => {
      if (!this.isWindow(id)) return;
      windowsList.get(id).reload();
    };

    //关闭所有窗口
    closeAllWindow = (): void => {
      windowsList.forEach((item) => {
        item.close();
      });
    };

    //获取某个窗口实例
    getWindow = (id: string): EWindow => {
      return windowsList.get(id);
    };

    //窗口是否可见
    windowIsVisible = (id: string): EWindow => {
      return windowsList.get(id).isVisible();
    };

    //设置子窗口
    setParentWindow = (id: string, parentId: string): void => {
      if (this.isWindow(id) && this.isWindow(parentId)) {
        const childWindow = this.getWindow(id);
        const parentWindow = this.getWindow(parentId);
        childWindow.setParentWindow(parentWindow);
      }
    };

    //闪烁窗口
    flashWindow = (id: string): void => {
      if (!this.isWindow(id)) return;
      windowsList.get(id).flashFrame(true);
      let timer;
      if (timer) {
        clearTimeout(timer);
      }
      timer = setTimeout(() => {
        windowsList.get(id).flashFrame(false);
      }, 10);
    }
}

export default const winTool = new WinTools()

二、窗口监听函数

Electron 提供了多种进程间通信(IPC)方式,让主进程(Main Process)和渲染进程(Renderer Process)能够相互通信。

  • ipcMain.on(消息通道名称, 回调函数)用来监听渲染进程的消息。

  • ipcMain.once(消息通道名称, 回调函数)一次性监听渲染进程的消息

  • ipcMain.handle(消息通道名称, 接受参数并返回值的回调函数)异步响应渲染进程的消息

  • 当前窗口名\].webContents.send(消息通道名称, 参数)当前窗口发送消息到渲染进程

  • ipcRenderer.on(消息通道名称,回调函数) 监听主进程回复

  • ipcRenderer.invoke(消息通道名称,参数).then()异步等待主进程的回复

js 复制代码
import { shell, ipcMain, app } from 'electron';
import winTool from './WinTools'

 //新开web窗口
ipcMain.on('newWin', (_, config: object, data:any): void => {
  const newWin = winTool.createWin(config);
  //窗口发送消息
  if(data){
      newWin.on('ready-to-show', () => {
          newWin.webContents.send('msg',data)
      }
  }
  
});

//关闭窗口
ipcMain.on('close-window', (_, id: string): void => {
  if (id) {
    winTool.windowClose(id);
  } else {
    winTool.closeAllWindow();
  }
});

//隐藏窗口
ipcMain.on('hide-window', (_, id: string): void => {
  winTool.windowHide(id);
});

//显示窗口
ipcMain.on('show-window', (_, id: string): void => {
  winTool.windowShow(id);
});
//最大化
ipcMain.on('max-window', (_, id: string): void => {
  winTool.windowMax(id);
});
//最小化
ipcMain.on('min-window', (_, id: string): void => {
  winTool.windowMin(id);
});
//还原
ipcMain.on('restore-window', (_, id: string): void => {
  winTool.windowRestore(id);
});
//闪烁
ipcMain.on('flash-window', (_, id: string): void => {
  winTool.flashWindow(id);
});

//打开网页
ipcMain.on('open-url', (_, url) => {
  shell.openExternal(url); //打开系统默认浏览器到指定的url
});

//退出应用
ipcMain.on('Exit', (_, url) => {
  app.quit();
})

// 异步方法 支持同时获取多个key的数据 
ipcMain.handle('get-data', async (event, name) => {
    //获取数据的方法
    let data = await getData(name)
    return data; 
})

三、主进程

在主进程中引出工具函数,并创建主窗口页面,设置开机自启动、托盘等操作。

js 复制代码
import { app, BrowserWindow, Tray, Menu } from 'electron';
import winTool from './WinTool'
import './util/hook';
let mainWind: BrowserWindow | null = null;

function createWindow() {
  mainWind = winTool.createWin({
    isMainWin: true,
    id: 'mainWindow',
    router: 'index',
    frame: false,
  });

  
  //窗口显示前事件
   mainWind.on('ready-to-show', () => {
    //线上环境清空缓存
    if(!process.env.VITE_DEV_SERVER_URL){
      mainWind?.webContents.session.clearStorageData({
      storages:['localstorage','indexdb','cookies']
      });
      session.defaultSession.clearCache()
    }

    mainWind?.show(); // 初始化后再显示
   
  });
  
  // 程序崩溃的处理
  mainWind?.webContents.on('render-process-gone', (res) => {
        setTimeout(() => {
          mainWind?.reload();
        }, 1000);
  });
  
  createTray(mainWind);
}

app.whenReady().then(() => {
  createWindow();

  app.on('activate', () => {
    // 此处解决mac系统关闭app后,但程序坞中还存在图标,再次点击可以重新创建进程
    if (BrowserWindow.getAllWindows.length === 0) createWindow();
  });
});

app.on('window-all-closed', () => {
  // electron 运行在三个环境(win32 Windows系统、linux Linux系统、 darwin Mac系统)
  // 此处解决的是非mac系统,程序退出进程 (Mac系统关闭app会保留在程序坞中)
  if (process.platform !== 'darwin') app.quit();
});


//托盘
const createTray = (mainWind: any) => {
  const icon = join(__dirname, '../../public/assets/images/main/logo-32.png');
  let tray = new Tray(icon);
  //设置菜单
  const contextMenu = Menu.buildFromTemplate([
    {
      label: '退出系统',
      type: 'normal',
      icon: '',
      click: () => {
        app.quit();
      },
    },
    { label: '设置', icon: '', type: 'normal' },
  ]);

  tray.setContextMenu(contextMenu);

  tray.setToolTip('智能自动化平台');
  tray.setTitle('智能自动化平台');

  // 点击托盘图标,显示主窗口
  tray.on('click', () => {
    mainWind.show();
  });
};

//开机自启动
function autoOpen() {
  const isDevelopment = process.env.NODE_ENV === 'development';
   //读取本地设置是否开机自启动
  const isAutoOpen = getLocalData('setting')?.autoOpen;
 //当前执行程序的地址
  const ex = path.basename(process.execPath);

  if (!isDevelopment) {
    if (!isAutoOpen) {
      app.setLoginItemSettings({
        openAtLogin: false, //是否开机启动
      });
      logger.info('关闭开机自启动:' + isAutoOpen);
    } else {
      app.setLoginItemSettings({
        openAtLogin: true,
        openAsHidden: false,
        path: process.execPath,
        args: ['--processStart', `"${ex}"`, '--process-start-args'],
      });
      logger.info('开启开机自启动:' + isAutoOpen);
    }
  }
}

autoOpen()

四、preload文件

在 Electron 中,HTML 页面(渲染进程)默认不能直接使用 ipcRenderer 的主要原因是为了 安全性

上下文隔离(Context Isolation)

  • Electron 默认启用 contextIsolation: true 。渲染进程的 JavaScript 运行环境(如 window)和 Node.js 环境(如 require)是隔离的。
  • 直接访问 require('electron').ipcRenderer 会报错
  • 目的 :防止恶意代码通过 require 直接访问 Node.js API,避免安全漏洞(如 RCE 攻击)。

启用 Node.js 集成时仍有限制

  • 即使你手动关闭 contextIsolation 并启用 nodeIntegration: true
  • 虽然此时可以直接用 require('electron').ipcRenderer,但 极度不安全,因为网页中的第三方脚本(如广告、CDN 库)也能调用 Node.js API,可能导致系统被入侵。

因此需要通过 preload 脚本 暴露有限的 IPC 功能给渲染进程

js 复制代码
import { ipcRenderer , contextBridge } from 'electron';
//方法一:
(window as any).ipcRenderer = ipcRenderer;


//方法二: 通过 contextBridge 安全地暴露 API
contextBridge.exposeInMainWorld('electronAPI', {
  send: (channel, data) => ipcRenderer.send(channel, data),
  on: (channel, callback) => ipcRenderer.on(channel, callback),
  invoke: (channel, data) => ipcRenderer.invoke(channel, data),
});

五、渲染进程中使用

在渲染页面中通过window挂载的方法去调用窗口的通讯方法。

js 复制代码
//发送数据给窗口
window.ipcRenderer.send('Exit');

//接口窗口数据
window.ipcRenderer.on('msg', (_, val: string) => {
  msg.value = val;
});

//异步等待窗口回应
let msg = await window.ipcRenderer.invoke('get-data', 'setting')


// 方法二的使用: 发送消息到主进程
window.electronAPI.send('message', 'Hello from renderer!');
相关推荐
jingling55512 分钟前
Git 常用命令指南:从入门到高效开发
前端·javascript·git·前端框架
索西引擎14 分钟前
【前端】网站favicon图标制作
前端
程序员海军21 分钟前
告别低质量Prompt!:字节跳动PromptPilot深度测评
前端·后端·aigc
华洛22 分钟前
关于可以控制大模型提升任意产品的排名这件事📈
前端·github·产品经理
Yanc23 分钟前
翻了vue源码 终于解决了这个在SFC中使用tsx的bug
前端·vue.js
nujnewnehc27 分钟前
失业落伍前端, 尝试了一个月 ai 协助编程的真实感受
前端·ai编程·github copilot
大熊学员30 分钟前
HTML 媒体元素概述
前端·html·媒体
好好好明天会更好32 分钟前
那些关于$event在vue中不得不说的事
前端·vue.js
默默地离开40 分钟前
CSS定位全解析:从static到sticky的5种position属性详解(第五回)
前端·css
JosieBook43 分钟前
【web应用】前后端分离项目基本框架组成:Vue + Spring Boot 最佳实践指南
前端·vue.js·spring boot