基于代理和单例模式的 electron 多窗口管理方案

简介: 基于代理和单例模式的 electron 多窗口管理方案

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;
    }
  });
目录
相关文章
|
UED
解决Electron窗口白屏问题的预创建方案
在使用Electron创建窗口时,有时会遇到窗口显示白屏的问题。这篇文章将介绍一种解决方案,即预创建窗口,并提供了针对窗口关闭和应用退出的管理方法,以确保 Electron 应用的顺畅运行和用户体验
755 0
|
1月前
|
JSON JavaScript 前端开发
开发桌面程序-Electron入门
【10月更文挑战第16天】Electron 是一个使用 JavaScript、HTML 和 CSS 构建跨平台桌面应用的框架,嵌入了 Chromium 和 Node.js。本文介绍了如何搭建 Electron 开发环境,包括安装 Node.js、创建项目、配置 main.js 和打包应用。通过简单的步骤,你可以快速创建并运行一个基本的 Electron 应用程序。
开发桌面程序-Electron入门
|
4月前
|
JavaScript 开发工具
Electron 开发过程中主进程的无法看到 console.log 输出怎么办
Electron 开发过程中主进程的无法看到 console.log 输出怎么办
|
5月前
|
前端开发
PC端01,桌面端,electron的开发,electron的开发的系列课程,软件开发必备流程,electron的讲解,electron的开发,vitepress博主的gitee链接,PC端效率软件
PC端01,桌面端,electron的开发,electron的开发的系列课程,软件开发必备流程,electron的讲解,electron的开发,vitepress博主的gitee链接,PC端效率软件
PC端01,桌面端,electron的开发,electron的开发的系列课程,软件开发必备流程,electron的讲解,electron的开发,vitepress博主的gitee链接,PC端效率软件
|
7月前
|
移动开发 开发框架 JavaScript
Vue3 Vite electron 开发桌面程序
Vue3 Vite electron 开发桌面程序
351 0
|
前端开发 算法 JavaScript
从零开始开发图床工具:使用 Gitee 和 Electron 实现上传、管理和分享(下)
从零开始开发图床工具:使用 Gitee 和 Electron 实现上传、管理和分享(下)
197 0
|
存储 Web App开发 JavaScript
从零开始开发图床工具:使用 Gitee 和 Electron 实现上传、管理和分享(上)
从零开始开发图床工具:使用 Gitee 和 Electron 实现上传、管理和分享(上)
248 0
|
Web App开发 资源调度 前端开发
基于NeteaseCloudMusicApi和electron-vue开发网易云音乐--electron-vue初始化
基于NeteaseCloudMusicApi和electron-vue开发网易云音乐--electron-vue初始化
150 0
|
前端开发 JavaScript API
React使用Electron开发桌面端
React是一个流行的JavaScript库,用于构建Web应用程序。结合Electron框架,可以轻松地将React应用程序打包为桌面应用程序。以下是使用React和Electron开发桌面应用程序的步骤:
React使用Electron开发桌面端
|
前端开发 JavaScript 开发者
前端桌面应用开发:Electron介绍与实践(3)
前端桌面应用开发:Electron介绍与实践(3)
134 0