electron多窗口管理方案
仅发在以上地址发布,请勿转载!
简介: 单例模式进行全局状态管理的解决方案有很多。本文介绍一种围绕electron使用单个对象的代理并配套多个相关函数的窗口管理解决方案。
1. 概述
管理 electron 窗口即在主进程中管理由 BrowserWindow
等 electron API 创建的渲染进程。在工程化的大型 electron 项目中,可以单独创建进程管理的工具模块。本文分享一些窗口管理与控制上的经验。
一般而言,我们是通过窗口 id
来唯一表示一个 electron 的,
2. BrowserWindow 对象
2.1 类型
该对象的类型为 Electron.CrossProcessExports.BrowserWindow
,在 typescript 项目中为使用方便起见,你可以像下面这样为其起一个简短的别名:
// types.ts export declare type EWindow = Electron.CrossProcessExports.BrowserWindow;
2.2 创建窗口
创建
目录:apps.mian
文件:index.ts
// index.ts import { BrowserWindow } from "electron"; import { logger, WINDOW_PORT, RENDER_HTML_PATH } from "../../params"; // 全局配置的参数 import { mainWindowIDProxy, setWindowById } from "../../window_manager"; // 窗口管理工具 import { winparams } from "./params"; // 当前窗口参数 function newWinMain() { const window = new BrowserWindow(winparams as any); // 该函数用于记录该新建窗口的 ID ,在后面小节会讲解到 const id = setWindowById(window); // 这条语句用于记录主窗口 ID ,在后面小节会提到 mainWindowIDProxy.value = id.toString(); // 一般而言: // - 在生产环境下加载的是打包构建好的 HTML 文件,需要指定当前渲染进程的 index.html 文件的路径 // - 在开发环境下,往往会使用开发服务器,比如 vite 等都有提供,这时一般只需要指定本地端口 window.loadURL( process.env.NODE_ENV === "production" ? `file://${RENDER_HTML_PATH.main}` : `http://127.0.0.1:${WINDOW_PORT.main}/` ); logger.debug( `Start Main Window At URL: http://localhost:${WINDOW_PORT.main}/` ); // 仅仅在非生产环境下,打开调试工具 if (process.env.NODE_ENV !== "production") { window.webContents.openDevTools(); } require("@electron/remote/main").enable(window.webContents); mainWindowIDProxy.value = id.toString() return id; } export { newWinMain };
文件:params.ts
该文件用于指定创建窗口所需要的参数,以下仅仅是示例,你可以自己根据需要指定参数。
// params.ts import { APP_NAME } from "../../params"; export const winparams = { width: 1239, height: 766, resizable: true, minWidth: 966, minHeight: 696, transparent: false, opacity: 1, hasShadow: true, movable: true, minimizable: true, maximizable: true, closable: true, focusable: true, alwaysOnTop: false, fullscreen: false, fullscreenable: true, fullscreenWindowTitle: false, title: APP_NAME, icon: "../../../public/favicon_256x256.ico", show: true, paintWhenInitiallyHidden: true, frame: false, // Specify `false` to create a frameless window. Default is `true`. parent: null, webPreferences: { devTools: true, nodeIntegration: true, nodeIntegrationInWorker: true, contextIsolation: false, } }
3. 通过 id 管理 BrowserWindow 对象
3.1 目标概述
管理 BrowserWindow 对象主要是在窗口创建后管理其显示、隐藏、最小化、最大话、还原、关闭等等行为。要管理窗口的行为就必先须唯一的指定所需要管理的窗口是谁——这是我们主要的管理目标。
因此窗口一旦创建,我们就需要记录窗口的 id,并且在所有其它需要使用的时候,在其它的模块中都能够获取到这些被记录的窗口 id。
3.2 窗口字典(线程池)及其代理
// window_manager.ts const winDict:WindowDict = {}; import { logger } from "./params"; import { WindowDict, EWindow } from "./types"; const WindowDictProxy = new Proxy(winDict, { get: function(obj:WindowDict, prop:string){ return obj[prop] }, set: function(obj:WindowDict, prop:string, value:EWindow){ if(obj[prop]){ logger.error(`Window id '${prop}' has already existed.`) return false }else{ obj[prop] = value; return true; } }, has(target:WindowDict, key:string) { if(Object.getOwnPropertyNames(target).includes(key)){ return true }else{ return false } }, ownKeys:function(target:WindowDict){ return [...Object.getOwnPropertyNames(target)] }, deleteProperty: function(target:WindowDict, prop:string){ try{ delete target[prop] return true; }catch(e){ logger.warn(e); return false; } } })
3.3 通过 id 管理窗口
3.3.1 对窗口的 id 记录
// window_manager.ts /** 通过 id 托管窗口 */ function setWindowById(Window: EWindow){ try{ const id = Window.id.toString(); WindowDictProxy[id] = Window; return id; } catch(e){ logger.error(`Can not set Window By ID '${Window.id}', as the following reasons:\n${e}`) return ; } }
3.3.2 通过 id 索引窗口实例
// window_manager.ts /**通过ID索引窗口 */ function getWindowById(id:string):EWindow{ return WindowDictProxy[id] }
3.3.3 通过窗口实例操作窗口行为
// window_manager.ts /**通过 ID 关闭窗口 */ function hideWindowById(id:string){ try{ getWindowById(id).hide() return true; }catch(e){ logger.error(`Can not hide Window By ID '${id}', as the following reasons:\n${e}`) return false; } } /**通过 ID 显示窗口 */ function showWindowById(id:string){ try{ getWindowById(id).show() return true; }catch(e){ logger.error(`Can not show Window By ID '${id}', as the following reasons:\n${e}`) return false; } } /**通过 ID 隐藏窗口 */ function closeWindowById(id:string){ try{ getWindowById(id).close(); delete WindowDictProxy[id]; return true; }catch(e){ logger.error(`Can not close Window By ID '${id}', as the following reasons:\n${e}`) return false; } } /**通过 ID 最大化窗口 */ function maximizeWindowById(id:string){ try{ getWindowById(id).maximize(); delete WindowDictProxy[id]; return true; }catch(e){ logger.error(`Can not maximize Window By ID '${id}', as the following reasons:\n${e}`) return false; } } /**通过 ID 最小化窗口 */ function minimizeWindowById(id:string){ try{ getWindowById(id).minimize(); delete WindowDictProxy[id]; return true; }catch(e){ logger.error(`Can not minimize Window By ID '${id}', as the following reasons:\n${e}`) return false; } } /**通过 ID 还原窗口 */ function restoreWindowById(id:string){ try{ getWindowById(id).restore(); delete WindowDictProxy[id]; return true; }catch(e){ logger.error(`Can not restore Window By ID '${id}', as the following reasons:\n${e}`) return false; } }
3.4 操作所有窗口
// window_manager.ts /**关闭所有窗口 */ function closeAllWindows(){ Object.getOwnPropertyNames(WindowDictProxy).forEach( (id:string)=>{ WindowDictProxy[id].close(); delete WindowDictProxy[id]; } ) }
3.5. 关于主窗口
主窗口是我最常使用的窗口,对于这样的窗口有时候往往会在创建其它窗口或者创建对话框(dialog)等场景用到,因此我倾向于将其 id 记录下来,使用时不通过 id 而直接获取会比较方便。这样实现:
// window_manager.ts const mainWindowID:{value:undefined|string} = {value:undefined}; const mainWindowIDProxy = new Proxy(mainWindowID,{ get: function(obj:{value:undefined|string}, prop:string){ return obj.value }, set: function(obj:{value:undefined|string}, prop:'value', value){ obj['value'] = value return true; } })
4. 在渲染进程中控制窗口实例
4.1 确定你要控制的窗口实例
需要在渲染进程中控制窗口的关键在于获取当前窗口实例。当前的 electron 版本中已经移除了 remote
模块,在不使用该模块的前提下,只有通过 ipc 通信对模块进行控制,这意味着每个窗口对应的渲染进程在其从追进程中创建时就应该在主进程中记录这个窗口并唯一告诉通过创建后在渲染进程中使用 ipcrender
对主进程进行询问的 id
值。
还有一种方法就是使用 remote
模块。尽管官方已经从 electron API 中删除了,但我们仍然可以通过安装 @electron/remote
及逆行使用。
npm i -D @electron/remote
要使用该模块,首先你需要在 主进程 中通过以下语句启用它:
require("@electron/remote/main").initialize();
然后你可以使用以下方法获取当前窗口的实例:
// 返回当前窗口实例 getCurrentWindow(){ return require('@electron/remote').getCurrentWindow(); },
4.2 在渲染进程直接操作窗口
在渲染进程中,你可以参考以下方式进行使用:
// 渲染进程 // 窗口的最大化和还原的切换 windowToggle(){ if(this.windowSizeIcon === "window-max"){ this.getCurrentWindow().maximize(); this.windowSizeIcon = "window-middle"; } else{ this.getCurrentWindow().restore(); this.windowSizeIcon = "window-max"; } }, // 最小化窗口 windowMin(){ this.getCurrentWindow().minimize() },
4.3 增减进程一类操作
对于关闭窗口等涉及增减进程一类的操作,会显得比较特殊。对于从渲染进程关闭窗口,在渲染进程中,我是这样处理的:
// 渲染进程 const { ipcRenderer } = require('electron'); // 窗口关闭 windowClose(){ const id = this.getCurrentWindow().id; ipcRenderer.send("ipc-window-manager-by-id",{ action: 'close', id: id }) },
可以看出,我并没有直接在渲染进程中关闭窗口。这是为了我们的进程能在主进程的 进程池 中进程统一管理,避免从渲染进程关闭窗口后,主进程的进程池中却还有该进程的记录。因此,在主进程中我们需要监听该关闭消息,并调用 closeWindowById
方法。
// 主进程 import { ipcMain } from "electron"; ipcMain.on("ipc-window-manager-by-id", (event, arg) => { const id = arg.id; const action = arg.action; switch (action) { case "hide": hideWindowById(id); break; case "show": showWindowById(id); break; case "maximize": maximizeWindowById(id); break; case "minimize": minimizeWindowById(id); break; case "restore": restoreWindowById(id); break; // 从渲染进程关闭最终在主进程实现关闭 case "close": // 调用该方法,能够删除进程池中的记录 closeWindowById(id); break; } });