@lynker-desktop/electron-window-manager
Version:
electron-window-manager
564 lines (561 loc) • 17 kB
JavaScript
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