UNPKG

@lynker-desktop/electron-window-manager

Version:

electron-window-manager

564 lines (561 loc) 17 kB
const eIpc = require('@lynker-desktop/electron-ipc/renderer'); /** * Electron 窗口管理器 - 渲染进程模块 * 提供窗口创建、管理、查询等功能 * * @author Lynker Desktop Team * @version 1.0.0 */ /** * 通用获取函数:从全局变量或 require 获取模块 * @param globalKey 全局对象中的键名 * @param requirePath require 路径 * @param errorMessage 错误提示信息 * @returns 获取到的模块或空对象 */ const getModule = (globalKey, requirePath, errorMessage) => { try { // 优先从全局变量获取(预加载脚本注入) const globalValue = window?.__ELECTRON_WINDOW_MANAGER__?.[globalKey]; if (globalValue) { return globalValue; } // 回退到 require 方式 return window?.require?.(requirePath); } catch (error) { console.error(errorMessage, error); return {}; } }; /** * 获取 IPC 渲染器实例 * 优先从全局变量获取,回退到 require 方式 * * @returns IpcRenderer 实例或空对象 */ const getIpc = () => { return getModule('ipcRenderer', 'electron', '当前非桌面端环境, 请在桌面端中调用'); }; /** * 获取 Electron Remote 模块 * 优先从全局变量获取,回退到 require 方式 * * @returns RemoteType 实例或空对象 */ const getRemote = () => { return getModule('remote', '@electron/remote', '获取 Remote 模块失败'); }; /** * 获取所有窗口的 BrowserView 列表 * 遍历所有窗口并收集其 BrowserView(带缓存优化) * * @param forceRefresh 是否强制刷新缓存 * @returns BrowserView 数组 */ const getAllBrowserViews = (() => { let cachedViews = null; let cacheTime = 0; const CACHE_DURATION = 100; // 缓存 100ms return (forceRefresh = false) => { const now = Date.now(); if (!forceRefresh && cachedViews && (now - cacheTime < CACHE_DURATION)) { return cachedViews; } const remote = getRemote(); try { const allWindows = remote.BrowserWindow?.getAllWindows?.() || []; const allBrowserViews = []; allWindows.forEach(window => { try { const views = window.getBrowserViews?.() || []; allBrowserViews.push(...views); } catch (error) { // 忽略获取 views 失败的情况 } }); cachedViews = allBrowserViews; cacheTime = now; return allBrowserViews; } catch (error) { console.error('获取 BrowserView 列表失败:', error); return cachedViews || []; } }; })(); /** * 通过 WebContents 查找对应的 BrowserView * 如果找不到已挂载的 BrowserView,则创建虚拟的 BrowserView 对象 * * @param webContent - WebContents 实例 * @returns BVItem 实例或 null */ const getBrowserViewByWebContent = (webContent) => { if (!webContent) { return null; } try { // 检查 webContents 是否已销毁 if (webContent.isDestroyed?.()) { return null; } // 查找已挂载的 BrowserView const allBrowserViews = getAllBrowserViews(); const existingView = allBrowserViews.find(view => view.webContents === webContent); if (existingView) { return existingView; } // 如果 WebContents 存在但未挂载到窗口,创建虚拟 BrowserView return createVirtualBrowserView(webContent); } catch (error) { console.error('获取 BrowserView 失败:', error); return null; } }; /** * 统一的 IPC 调用封装 * @param type IPC 消息类型 * @param data IPC 消息数据 * @returns Promise<T> IPC 调用结果 */ const invokeIpc = async (type, data) => { try { const ipc = getIpc(); if (!ipc || !ipc.invoke) { throw new Error('IPC 不可用'); } return await ipc.invoke('__ELECTRON_WINDOW_MANAGER_IPC_CHANNEL__', { type, data }); } catch (error) { console.error(`IPC 调用失败 [${type}]:`, error); throw error; } }; /** * 创建虚拟的 BrowserView 对象 * 用于处理未挂载到窗口的 WebContents * * @param webContent - WebContents 实例 * @returns 虚拟的 BVItem 对象 */ const createVirtualBrowserView = (webContent) => { const webContentId = webContent.id; const virtualView = { id: webContentId, _id: webContentId, _type: 'BV', _name: '', _extraData: '', webContents: webContent, // 代理方法,通过 IPC 调用主进程 getBounds: async () => { return invokeIpc('borrowView_getBounds', { webContentId, options: {} }); }, setBounds: async (bounds) => { return invokeIpc('borrowView_setBounds', { webContentId, options: bounds }); }, setAutoResize: async (autoResize) => { return invokeIpc('borrowView_setAutoResize', { webContentId, options: autoResize }); }, setBackgroundColor: async (color) => { return invokeIpc('borrowView_setBackgroundColor', { webContentId, options: color }); }, }; return virtualView; }; /** * 为窗口对象设置扩展属性 * * @param window - 窗口对象 * @param data - 窗口数据 */ const setWindowProperties = (window, data) => { if (!window || !data) return; try { window._name = data.winName || ''; window._type = data.winType || ''; window._extraData = data.winExtraData || ''; window._initUrl = data.winInitUrl || ''; window._zIndex = data.winZIndex ?? 0; // 尝试设置 ID if (data.winId) { try { window.id = Number(data.winId); } catch (error) { // 忽略设置 ID 失败的情况 } } } catch (error) { console.error('设置窗口属性失败:', error); } }; /** * 根据 webContents ID 查找窗口实例 * 通过 webContents 查找对应的 BrowserWindow 或 BrowserView * 如果是 webview,则找到它的父 BrowserWindow * * @param webContentsId - webContents ID * @returns 窗口实例或 null */ const findWindowById = (webContentsId) => { if (!webContentsId || webContentsId <= 0) { return null; } const remote = getRemote(); try { // 首先通过 webContentsId 获取 webContents const webContent = remote.webContents?.fromId?.(webContentsId); if (!webContent) { return null; } // Case 1: 查找 BrowserView // 遍历所有窗口的 BrowserView,查找匹配的 const allWindows = remote.BrowserWindow?.getAllWindows?.() || []; for (const win of allWindows) { try { const views = win.getBrowserViews?.() || []; for (const view of views) { if (view.webContents?.id === webContentsId) { return view; } } } catch (error) { // 忽略获取 views 失败的情况 } } // Case 2: WebView // webview 有 hostWebContents,指向它所在的 BrowserWindow 的 webContents if (webContent.hostWebContents) { const parentWindow = remote.BrowserWindow?.fromWebContents?.(webContent.hostWebContents); if (parentWindow) { return parentWindow; } } // Case 3: 普通 BrowserWindow 本身 const browserWindow = remote.BrowserWindow?.fromWebContents?.(webContent); if (browserWindow) { return browserWindow; } // Case 4: 如果找不到已挂载的 BrowserView,尝试创建虚拟 BrowserView return getBrowserViewByWebContent(webContent); } catch (error) { console.error(`查找窗口失败 [${webContentsId}]:`, error); } return null; }; async function create(options) { try { // 通过 IPC 调用主进程创建窗口 const data = await eIpc.RendererIPC.invokeMain('__ELECTRON_WINDOW_MANAGER_IPC_CHANNEL__', { type: 'create', data: options }); if (!data?.winId) { throw new Error('创建窗口失败: 未返回有效的窗口 ID'); } // 查找窗口实例 const window = findWindowById(data.winId); if (!window) { throw new Error(`创建窗口失败: 无法找到窗口实例 [${data.winId}]`); } // 设置窗口属性 setWindowProperties(window, data); return window; } catch (error) { console.error('创建窗口失败:', error); throw error; } } /** * 获取当前窗口实例 * * @returns Promise<BWItem | BVItem | undefined> 当前窗口实例 */ const getCurrentWindow = async () => { const remote = getRemote(); const currentWindow = remote.getCurrentWindow(); return await get(currentWindow?.webContents?.id); }; /** * 根据 ID 或名称获取窗口 * * @param idOrName - 窗口 ID 或名称 * @returns Promise<BWItem | BVItem | undefined> 窗口实例 * * @example * ```typescript * // 通过 ID 获取 * const window = await get(1); * * // 通过名称获取 * const window = await get('main-window'); * ``` */ const get = async (idOrName) => { try { // 通过 IPC 获取窗口信息 const data = await invokeIpc('get', idOrName); if (!data?.winId) { return undefined; } // 查找窗口实例 const window = findWindowById(data.winId); if (window) { setWindowProperties(window, data); } return window || undefined; } catch (error) { console.error('获取窗口失败:', error); return undefined; } }; async function getAll(type) { try { // 通过 IPC 获取所有窗口信息 const windowData = await invokeIpc('getAll', undefined); if (!windowData || typeof windowData !== 'object') { return new Map(); } const allWindows = new Map(); const browserWindows = new Map(); const browserViews = new Map(); // 遍历窗口数据 for (const key in windowData) { const element = windowData[key]; if (!element?.winId) { continue; } // 查找窗口实例 const window = findWindowById(element.winId); if (window) { // 设置窗口属性 setWindowProperties(window, element); // 统一使用 webContents.id 作为键 const windowId = window.webContents?.id || element.winId; allWindows.set(windowId, window); // 按类型分类 if (window._type === 'BW') { browserWindows.set(windowId, window); } else if (window._type === 'BV') { browserViews.set(windowId, window); } } } // 根据类型返回对应的映射表 if (type === 'BW') { return browserWindows; } if (type === 'BV') { return browserViews; } return allWindows; } catch (error) { console.error('获取所有窗口失败:', error); return new Map(); } } /** * 关闭指定窗口 * * @param idOrName - 窗口 ID 或名称 * @returns Promise<boolean> 操作是否成功 */ const close = async (idOrName) => { try { await invokeIpc('close', idOrName); return true; } catch (error) { console.error('关闭窗口失败:', error); return false; } }; /** * 关闭所有窗口 * * @returns Promise<boolean> 操作是否成功 */ const closeAll = async () => { try { await invokeIpc('closeAll', undefined); return true; } catch (error) { console.error('关闭所有窗口失败:', error); return false; } }; /** * 根据 WebContents ID 查找对应的窗口 * * @param webContentId - WebContents ID * @returns Promise<BWItem | BVItem | undefined> 窗口实例 */ const getWindowForWebContentsId = async (webContentId) => { try { const winId = await invokeIpc('getWindowForWebContentsId', webContentId); if (!winId) { return undefined; } return get(winId); } catch (error) { console.error('根据 WebContents ID 查找窗口失败:', error); return undefined; } }; /** * 重命名窗口 * * @param idOrName - 窗口 ID 或名称 * @param newName - 新名称 * @returns Promise<BWItem | BVItem | undefined> 重命名后的窗口实例 */ const rename = async (idOrName, newName) => { try { const data = await invokeIpc('rename', { idOrName, newName }); if (!data?.winId) { return undefined; } return get(data.winId); } catch (error) { console.error('重命名窗口失败:', error); return undefined; } }; /** * 重置窗口初始化URL * * @param idOrName - 窗口 ID 或名称 * @param url - 新URL * @returns Promise<BWItem | BVItem | undefined> 重置后的窗口实例 */ const reInitUrl = async (idOrName, url) => { try { const data = await invokeIpc('reInitUrl', { idOrName, url }); if (!data?.winId) { return undefined; } return get(data.winId); } catch (error) { console.error('重置窗口 URL 失败:', error); return undefined; } }; /** * 设置预加载 WebContents 配置 * * @param preloadWebContentsConfig - 预加载配置 */ const setPreloadWebContentsConfig = async (preloadWebContentsConfig) => { try { await invokeIpc('setPreloadWebContentsConfig', preloadWebContentsConfig); } catch (error) { console.error('设置预加载配置失败:', error); throw error; } }; /** * 获取预加载脚本路径 * 使用缓存机制避免重复请求 * * @returns Promise<string | undefined> 预加载脚本路径 */ const getPreload = (() => { let cachedPreload = null; let isLoading = false; let loadPromise = null; return async () => { // 如果已有缓存,直接返回 if (cachedPreload !== null) { return cachedPreload; } // 如果正在加载,返回同一个 Promise if (isLoading && loadPromise) { return loadPromise; } // 开始加载 isLoading = true; loadPromise = (async () => { try { // 通过 IPC 获取预加载脚本路径 const data = await invokeIpc('getPreload', undefined); // 缓存结果(即使是 undefined 也缓存,避免重复请求) cachedPreload = data; return data; } catch (error) { console.error('获取预加载脚本路径失败:', error); // 失败时也缓存 null,避免重复请求 cachedPreload = undefined; return undefined; } finally { isLoading = false; loadPromise = null; } })(); return loadPromise; }; })(); // 初始化时异步获取预加载脚本(不阻塞) getPreload().catch(() => { // 静默处理初始化失败 }); // ==================== 开发工具相关(已注释) ==================== /* * 开发工具相关功能(已注释,可根据需要启用) * * const handleOpenDevTools = (e: HTMLElement, ev: KeyboardEvent): any => { * const remote = getRemote() * const webContents = remote.getCurrentWebContents() * webContents.openDevTools({ * mode: 'detach' * }) * return ''; * } * * export const registerDevTools = () => { * document.body.removeEventListener('keydown', handleOpenDevTools) * document.body.addEventListener('keydown', handleOpenDevTools) * } */ exports.close = close; exports.closeAll = closeAll; exports.create = create; exports.get = get; exports.getAll = getAll; exports.getCurrentWindow = getCurrentWindow; exports.getPreload = getPreload; exports.getWindowForWebContentsId = getWindowForWebContentsId; exports.reInitUrl = reInitUrl; exports.rename = rename; exports.setPreloadWebContentsConfig = setPreloadWebContentsConfig; //# sourceMappingURL=index.js.map