UNPKG

@lynker-desktop/electron-window-manager

Version:

electron-window-manager

1,228 lines (1,227 loc) 70.7 kB
import lodash from 'lodash'; import { app, session, BrowserWindow, BrowserView, webContents } from 'electron'; import * as remote from '@electron/remote/main'; import eIpc from '@lynker-desktop/electron-ipc/main'; import md5 from 'md5'; import PQueue from 'p-queue'; const getQueue = (() => { let queue; return async () => { if (!queue) { // const { default: PQueue } = await import('p-queue') queue = new PQueue({ concurrency: 1 }); } return queue; }; })(); const getCustomSession = (() => { let customSession; return () => { if (!customSession) { customSession = session.fromPartition('persist:__global_main_session__'); } return customSession; }; })(); app.on('ready', () => { getCustomSession(); }); if (!remote.isInitialized()) { remote.initialize(); } const enable = (win) => { remote.enable(win); }; const initWebContentsVal = (win, preload) => { try { if (!win?.webContents || win.webContents.isDestroyed()) { return; } const webContentsId = win.webContents.id; win.webContents.executeJavaScript(` try { const data = { __ELECTRON_WINDOW_MANAGER_WEB_CONTENTS_ID__: ${JSON.stringify(webContentsId)}, __ELECTRON_WINDOW_MANAGER_TYPE__: ${JSON.stringify(win._type)}, __ELECTRON_WINDOW_MANAGER_NAME__: ${JSON.stringify(win._name)}, __ELECTRON_WINDOW_MANAGER_EXTRA_DATA__: ${JSON.stringify(win._extraData || '')}, __ELECTRON_WINDOW_MANAGER_PRELOAD__: ${JSON.stringify(preload)}, __ELECTRON_WINDOW_MANAGER_INIT_URL__: ${JSON.stringify(win._initUrl || '')} }; Object.entries(data).forEach(([key, value]) => { window[key] = value; }); } catch (error) {} `); } catch (error) { // 忽略错误,webContents 可能已销毁 } }; class WindowsManager { /** * webview 域名白名单 * 传入格式示例: * [ * 'example.com', // 精确匹配 example.com * '.example.com', // 匹配所有 example.com 的子域名,如 a.example.com、b.example.com * 'sub.domain.com', // 精确匹配 sub.domain.com * 'localhost', // 匹配 localhost 及本地回环地址 * '127.0.0.1', // 匹配 127.0.0.1 * '::1' // 匹配 IPv6 本地回环 * ] * 注意: * - 以点开头的(如 .example.com)会匹配所有子域名。 * - 不带点的(如 example.com)只匹配主域名。 * - 'localhost'、'127.0.0.1'、'::1' 以及局域网 IP(如 192.168.x.x、10.x.x.x、172.16.x.x~172.31.x.x)都视为本地白名单。 */ constructor(preload, loadingViewUrl, errorViewUrl, preloadWebContentsConfig, webviewDomainWhiteList) { // 按名称索引的 Map,用于加速查找 this.windowsByName = new Map(); // 预加载的窗口 this.preloadedBW = null; // 预加载的窗口(无边框,有按钮) this.preloadedBW_FramelessWithButtons = null; // 预加载的窗口(无边框,无按钮) this.preloadedBW_FramelessNoButtons = null; // 预加载的浏览器视图 this.preloadedBV = null; this.preloading = false; this.webviewDomainWhiteList = []; // 窗口销毁检查的防抖函数,避免频繁检查 this.cleanupDestroyedWindowsDebounced = lodash.debounce(() => { this._cleanupDestroyedWindows(); }, 500); /** * 防抖的排序方法 * @param window 目标窗口 */ this.sortBrowserViewsDebounced = lodash.debounce((window, view) => { this._sortBrowserViews(window, view); }, 50); this.preload = preload; this.windows = new Map(); this.loadingViewUrl = `${loadingViewUrl ?? ''}`; this.errorViewUrl = `${errorViewUrl ?? ''}`; this.preloadWebContentsConfig = preloadWebContentsConfig; this.webviewDomainWhiteList = webviewDomainWhiteList || []; log('log', 'preloadWebContentsConfig: ', this.preloadWebContentsConfig); if (this.preloadWebContentsConfig) { if (this.preloadWebContentsConfig.nodeIntegration === undefined) { this.preloadWebContentsConfig.nodeIntegration = true; } if (this.preloadWebContentsConfig.contextIsolation === undefined) { this.preloadWebContentsConfig.contextIsolation = false; } getQueue(); app.whenReady().then(() => { if (this.preloadWebContentsConfig) { this.setPreloadWebContentsConfig(this.preloadWebContentsConfig); } }); } } /** * 设置预加载的webContents配置 * @param preloadWebContentsConfig 预加载的webContents配置 */ setPreloadWebContentsConfig(preloadWebContentsConfig) { try { this.preloadWebContentsConfig = preloadWebContentsConfig; if (this.preloadWebContentsConfig) { getQueue().then(q => q.add(async () => { return await this._preloadInstances(); })); } else { this.preloadedBW = null; this.preloadedBW_FramelessWithButtons = null; this.preloadedBW_FramelessNoButtons = null; this.preloadedBV = null; } } catch (error) { log('error', 'setPreloadWebContentsConfig error:', error); } } /** * Promise 超时包装函数 * @param promise 要包装的 Promise * @param timeout 超时时间(毫秒) * @param errorMessage 超时错误信息 */ async _withTimeout(promise, timeout, errorMessage) { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error(errorMessage)); }, timeout); }); return Promise.race([promise, timeoutPromise]); } /** * 预加载实例 */ async _preloadInstances() { if (this.preloading) return; this.preloading = true; try { if (this.preloadWebContentsConfig) { log('log', 'preloadWebContentsConfig: ', this.preloadWebContentsConfig); const preloadPromises = []; // 根据配置决定是否预加载普通窗口 if (this.preloadWebContentsConfig.enableBW !== false && !this.preloadedBW) { preloadPromises.push(this._createPreloadBW({}).then(i => { this.preloadedBW = i; log('log', 'init preloadedBW: ', !!this.preloadedBW); }).catch(error => { log('error', '预加载 BW 失败:', error); })); } // 根据配置决定是否预加载无边框有按钮的窗口 if (this.preloadWebContentsConfig.enableBW_FramelessWithButtons !== false && !this.preloadedBW_FramelessWithButtons) { preloadPromises.push(this._createPreloadBW({ frame: false, autoHideMenuBar: true, titleBarStyle: 'hidden', }).then(i => { this.preloadedBW_FramelessWithButtons = i; log('log', 'init preloadedBW_FramelessWithButtons: ', !!this.preloadedBW_FramelessWithButtons); }).catch(error => { log('error', '预加载 BW_FramelessWithButtons 失败:', error); })); } // 根据配置决定是否预加载无边框无按钮的窗口 if (this.preloadWebContentsConfig.enableBW_FramelessNoButtons !== false && !this.preloadedBW_FramelessNoButtons) { preloadPromises.push(this._createPreloadBW({ frame: false, autoHideMenuBar: true, titleBarStyle: 'default', }).then(i => { this.preloadedBW_FramelessNoButtons = i; log('log', 'init preloadedBW_FramelessNoButtons: ', !!this.preloadedBW_FramelessNoButtons); }).catch(error => { log('error', '预加载 BW_FramelessNoButtons 失败:', error); })); } // 根据配置决定是否预加载浏览器视图 if (this.preloadWebContentsConfig.enableBV !== false && !this.preloadedBV) { preloadPromises.push(this._createPreloadBV().then(i => { this.preloadedBV = i; log('log', 'init preloadedBV: ', !!this.preloadedBV); }).catch(error => { log('error', '预加载 BV 失败:', error); })); } if (preloadPromises.length > 0) { // 添加超时机制,默认 10 秒超时 const timeout = 10000; // 10 秒 try { await this._withTimeout(Promise.allSettled(preloadPromises), timeout, `预加载超时(${timeout}ms)`); } catch (error) { log('error', '预加载超时:', error); // 超时后继续执行,不阻塞后续流程 } } } } catch (e) { log('error', '预创建实例失败', e); } this.preloading = false; } /** * 创建预加载的窗口 * @param options 窗口选项 * @returns 预加载的窗口 */ _createPreloadBW(options = {}) { return new Promise((resolve) => { const preload = this.preload; const url = this.preloadWebContentsConfig?.url; if (this.preloadWebContentsConfig?.url) { const webPreferences = (options.webPreferences || {}); const instance = new BrowserWindow({ useContentSize: true, show: false, backgroundColor: '#ffffff', ...options, webPreferences: { ...webPreferences, sandbox: false, webviewTag: true, plugins: true, nodeIntegration: this.preloadWebContentsConfig?.nodeIntegration ?? true, contextIsolation: this.preloadWebContentsConfig?.contextIsolation ?? false, backgroundThrottling: false, webSecurity: false, preload: webPreferences.preload || preload, defaultEncoding: 'utf-8', } }); try { remote.enable(instance.webContents); } catch (error) { log('error', '预加载 BW 设置 remote 失败', error); } try { const webContentsId = instance.webContents?.id; if (webContentsId !== undefined) { // @ts-ignore instance._id = webContentsId; } } catch (error) { log('error', '预加载 BW 设置 _id 失败', error); } // @ts-ignore const webContentsId = instance.webContents?.id; log('log', '创建预BW: ', webContentsId, this.preloadWebContentsConfig?.url); // instance.webContents.once('did-finish-load', () => { // resolve(instance as BWItem); // }); // instance.webContents.once('did-fail-load', () => { // resolve(instance as BWItem); // }); // @ts-ignore instance.loadURL(url ? `${url}` : 'about:blank'); resolve(instance); } else { resolve(null); } }); } /** * 创建预加载的浏览器视图 * @returns 预加载的浏览器视图 */ _createPreloadBV(options = {}) { return new Promise((resolve) => { const preload = this.preload; const url = this.preloadWebContentsConfig?.url; if (this.preloadWebContentsConfig?.url) { const webPreferences = (options.webPreferences || {}); const instance = new BrowserView({ webPreferences: { ...webPreferences, sandbox: false, webviewTag: true, plugins: true, nodeIntegration: this.preloadWebContentsConfig?.nodeIntegration ?? true, contextIsolation: this.preloadWebContentsConfig?.contextIsolation ?? false, // backgroundThrottling: false, webSecurity: false, preload: webPreferences.preload || preload, defaultEncoding: 'utf-8', } }); try { remote.enable(instance.webContents); } catch (error) { log('error', '预加载 BV 设置 remote 失败', error); } try { const webContentsId = instance.webContents?.id; if (webContentsId !== undefined) { // @ts-ignore instance._id = webContentsId; } // 设置默认zIndex层级 instance._zIndex = 0; } catch (error) { log('error', '预加载 BV 设置 _id 失败', error); } // @ts-ignore const webContentsId = instance.webContents?.id; log('log', '创建预BV: ', webContentsId, this.preloadWebContentsConfig?.url); // instance.webContents.once('did-finish-load', () => { // resolve(instance as BVItem); // }); // instance.webContents.once('did-fail-load', () => { // resolve(instance as BVItem); // }); // @ts-ignore instance.webContents.loadURL(url || 'about:blank'); resolve(instance); } else { resolve(null); } }); } async create(options) { const queue = await getQueue(); const win = await queue.add(async () => { const window = await this._createWindow(options); return window; }); return win; } /** * 实际的窗口创建逻辑 */ async _createWindow(options) { let window; const { usePreload = true, type = 'BW', name = 'anonymous', url, loadingView = { url: undefined }, errorView = { url: undefined }, browserWindow: browserWindowOptions, openDevTools = false, preventOriginClose = false, zIndex = 0, } = options; const existingWinId = this.windowsByName.get(options.name); if (existingWinId) { window = this.windows.get(existingWinId); if (window) { return window; } // 清理无效的引用 this.windowsByName.delete(options.name); } options.type = type; // 优先复用预创建实例 let preloadWin = null; if (type === 'BW' && usePreload && this.preloadWebContentsConfig?.url) { const bwOptions = browserWindowOptions || {}; if (bwOptions.frame === false) { if (bwOptions.titleBarStyle === 'default' || !bwOptions.titleBarStyle) { if (this.preloadWebContentsConfig.enableBW_FramelessNoButtons !== false && this.preloadedBW_FramelessNoButtons) { preloadWin = this.preloadedBW_FramelessNoButtons; this.preloadedBW_FramelessNoButtons = await this._createPreloadBW({ frame: false, // transparent: true, titleBarStyle: 'default', webPreferences: { preload: bwOptions?.webPreferences?.preload || this.preload, } }); } } else { if (this.preloadWebContentsConfig.enableBW_FramelessWithButtons !== false && this.preloadedBW_FramelessWithButtons) { preloadWin = this.preloadedBW_FramelessWithButtons; this.preloadedBW_FramelessWithButtons = await this._createPreloadBW({ frame: false, // transparent: true, titleBarStyle: 'hidden', webPreferences: { preload: this.preload, } }); } } } else { if (this.preloadWebContentsConfig.enableBW !== false && this.preloadedBW) { preloadWin = this.preloadedBW; this.preloadedBW = await this._createPreloadBW({ webPreferences: { preload: bwOptions?.webPreferences?.preload || this.preload, } }); } } } if (type === 'BV' && usePreload && this.preloadWebContentsConfig?.url) { const bvOptions = browserWindowOptions || {}; if (this.preloadWebContentsConfig.enableBV !== false && this.preloadedBV) { preloadWin = this.preloadedBV; this.preloadedBV = await this._createPreloadBV({ webPreferences: { preload: bvOptions?.webPreferences?.preload || this.preload, } }); } } if (preloadWin) { const win = preloadWin; log('log', `${name} 使用预加载窗口(${type})`, this._getWebContentsId(win)); win._type = type; win._name = options.name || 'anonymous'; win._extraData = `${options?.extraData || ''}`; win._initUrl = `${options?.url || ''}`; // @ts-ignore // win?.removeAllListeners && win?.removeAllListeners?.(); // win.webContents.removeAllListeners && win.webContents.removeAllListeners(); if (type === 'BW') { // @ts-ignore this._applyBrowserWindowOptions(win, options); } if (type === 'BV') { this._applyBrowserViewOptions(win, options); } if (typeof this.preloadWebContentsConfig?.customLoadURL === 'function') { try { if (type === 'BW') { // @ts-ignore const originLoadURL = win.loadURL; // @ts-ignore win.loadURL = async (url, useNativeLoadURL = false) => { return new Promise(async (resolve, reject) => { if (useNativeLoadURL) { log('log', 'useNativeLoadURL win.loadURL'); try { await originLoadURL.call(win, url); resolve(undefined); } catch (error) { reject(error); } return; } try { log('log', 'customLoadURL win.loadURL'); // @ts-ignore await this.preloadWebContentsConfig.customLoadURL(url || 'about:blank', (url) => originLoadURL.call(win, url), win.webContents); try { win.emit('ready-to-show'); } catch (error) { log('error', 'emit ready-to-show event failed:', error); } resolve(undefined); } catch (error) { reject(error); } }); }; } const originWebContentsLoadURL = win.webContents.loadURL; // @ts-ignore win.webContents.loadURL = async (url, useNativeLoadURL = false) => { return new Promise(async (resolve, reject) => { if (useNativeLoadURL) { log('log', 'useNativeLoadURL win.webContents.loadURL'); try { await originWebContentsLoadURL.call(win.webContents, url); resolve(undefined); } catch (error) { reject(error); } return; } try { log('log', 'customLoadURL win.webContents.loadURL'); // @ts-ignore await this.preloadWebContentsConfig.customLoadURL(url || 'about:blank', (url) => originWebContentsLoadURL.call(win.webContents, url), win.webContents); try { win.webContents.emit('ready-to-show'); } catch (error) { log('error', 'emit ready-to-show event failed:', error); } resolve(undefined); } catch (error) { reject(error); } }); }; } catch (error) { log('error', 'customLoadURL error', error); } } window = win; } try { loadingView.url = `${loadingView?.url ?? this.loadingViewUrl}`; lodash.merge(options, { loadingView, }); } catch (error) { log('error', 'loadingView error:', loadingView, this.loadingViewUrl); } try { errorView.url = `${errorView?.url ?? this.errorViewUrl}`; lodash.merge(options, { errorView, }); } catch (error) { log('error', 'errorView error:', errorView, this.errorViewUrl); } try { let parentWin = undefined; if (typeof browserWindowOptions?.parent === 'number') { parentWin = BrowserWindow.fromId(browserWindowOptions?.parent) || undefined; if (parentWin) { browserWindowOptions.parent = parentWin; } else { browserWindowOptions.parent = undefined; } } log('log', 'create 1: ', options); log('log', 'create 2: ', `parentWin: ${parentWin?.id}`); const preload = browserWindowOptions?.webPreferences?.preload || this.preload; if (!window) { window = type === 'BV' ? new BrowserView(lodash.merge((browserWindowOptions || {}), { webPreferences: lodash.merge({ sandbox: false, webviewTag: true, // session: getCustomSession(), plugins: true, nodeIntegration: true, contextIsolation: false, // fix bv二次挂载会灰屏 // backgroundThrottling: false, nativeWindowOpen: true, webSecurity: false, preload: preload, defaultEncoding: 'utf-8', }, browserWindowOptions?.webPreferences || {}) })) : new BrowserWindow(lodash.merge({ acceptFirstMouse: true, }, (browserWindowOptions || {}), { parent: parentWin, webPreferences: lodash.merge({ sandbox: false, webviewTag: true, // session: getCustomSession(), plugins: true, nodeIntegration: true, contextIsolation: false, backgroundThrottling: false, nativeWindowOpen: true, webSecurity: false, preload: preload, defaultEncoding: 'utf-8', }, browserWindowOptions?.webPreferences || {}) })); log('log', `${name} 不使用 ${type === 'BV' ? 'preloadedBV' : 'preloadedBW'}`, window?.webContents?.id); try { remote.enable(window.webContents); } catch (error) { log('error', 'enable: ', error); } } // 停止加载 // window.webContents?.stop?.(); // @ts-ignore // @ts-ignore try { const webContentsId = this._getWebContentsId(window); if (webContentsId !== undefined) { window._id = Number(`${webContentsId}`); } } catch (error) { // log('error', 'set id: ', error) } window._type = type; window._name = name; window._extraData = `${options?.extraData || ''}`; window._initUrl = `${options?.url || ''}`; // 设置zIndex层级 window._zIndex = options.zIndex ?? 0; log('log', 'create 5: ', this._getWebContentsId(window), window._name); if (loadingView?.url && loadingView?.url !== 'about:blank') { if (type === 'BW') { // @ts-ignore this._setLoadingView(window, options); } } if (errorView?.url && errorView?.url !== 'about:blank') { if (type === 'BW') { const showErrorView = lodash.debounce(() => { const _url = window._initUrl; /** * 判断是否是错误视图 */ const isErrorView = `${_url}`.toUpperCase().startsWith(`${errorView?.url}`.toUpperCase()); if (!isErrorView && _url) { // @ts-ignore window.loadURL ? window.loadURL(`${errorView?.url}`) : window.webContents.loadURL(`${errorView?.url}`); window.webContents.executeJavaScript(` var key = '__ELECTRON_WINDOW_MANAGER_DID_FAIL_LOAD_URL__'; window[key] = "${_url}"; sessionStorage.setItem(key, "${_url}"); `); } }, 1000); // @ts-ignore window.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame) => { if (isMainFrame) { showErrorView(); } }); // 当开始加载时取消错误视图 window.webContents.on('did-start-loading', () => { showErrorView.cancel(); }); // 当导航开始时取消错误视图 window.webContents.on('did-start-navigation', () => { showErrorView.cancel(); }); // 当页面重新加载时取消错误视图 window.webContents.on('did-navigate', () => { showErrorView.cancel(); }); // 当页面完成加载时取消错误视图 window.webContents.on('did-finish-load', () => { showErrorView.cancel(); }); // 当窗口关闭时取消错误视图 window.webContents.on('close', () => { showErrorView.cancel(); }); // 当窗口销毁时取消错误视图 window.webContents.on('destroyed', () => { showErrorView.cancel(); }); } } window.webContents.on('did-attach-webview', (_event, webContents) => { const tryEnable = () => { try { const url = webContents.getURL(); if (!url || url === 'about:blank') { return; } if (this.webviewDomainWhiteList && this.webviewDomainWhiteList.length > 0) { try { const { hostname } = new URL(url); // 优化白名单判断,支持 .example.com 形式的子域名通配和本地/内网IP const isWhiteListed = this.webviewDomainWhiteList.some(domain => { if (domain === 'localhost' || domain === '127.0.0.1' || domain === '::1') { return this._isLocalhost(hostname); } if (domain.startsWith('.')) { // .example.com 允许所有 *.example.com return hostname === domain.slice(1) || hostname.endsWith(domain); } else { // 精确匹配 return hostname === domain; } }) || this._isLocalhost(hostname); // 允许本地回环和内网地址 if (isWhiteListed) { enable(webContents); } else { log('log', 'webview 域名未在白名单,未启用 remote', url); } } catch { log('log', 'webview url 解析失败,未启用 remote', url); } } else { enable(webContents); // 没有配置白名单则全部允许 } } catch (error) { log('error', 'tryEnable webview error:', error); } }; // 只监听一次,防止多次触发 const onDidNavigate = () => { tryEnable(); webContents.removeListener('did-navigate', onDidNavigate); webContents.removeListener('did-finish-load', onDidNavigate); }; webContents.on('did-navigate', onDidNavigate); webContents.on('did-finish-load', onDidNavigate); }); // window.webContents.on('close', () => { // this.windows.delete(window.id || window._id) // }) window.webContents.on('destroyed', () => { const winId = this._getWebContentsId(window); if (winId !== undefined) { this.windows.delete(winId); } // 同步清理名称索引 this.windowsByName.delete(window._name); }); window.webContents.on('dom-ready', () => { if (openDevTools) { window.webContents.openDevTools(); } }); if (type === 'BW') { // @ts-ignore if (options.browserWindow?.show !== false) { window.show(); } // @ts-ignore window.on('closed', () => { const winId = this._getWebContentsId(window); log('log', 'closed', winId, window._name); if (winId !== undefined) { this.windows.delete(winId); } // 同步清理名称索引 this.windowsByName.delete(window._name); }); } if (type === 'BV') { parentWin?.addBrowserView(window); log('log', 'create - addBrowserView'); } const winId = this._getWebContentsId(window); if (winId !== undefined) { this.windows.set(winId, window); // 同步更新名称索引 this.windowsByName.set(window._name, winId); } log('log', 'create', this.windows.keys()); // 初始化值 window.webContents.on('did-finish-load', () => { log('log', 'did-finish-load', this._getWebContentsId(window)); initWebContentsVal(window, `${preload || ''}`); }); window.webContents.on('did-start-loading', () => { log('log', 'did-start-loading', this._getWebContentsId(window)); initWebContentsVal(window, `${preload || ''}`); }); if (type === 'BW') { const handleBrowserViewFocus = (view) => { try { view.webContents?.focus(); view.webContents?.executeJavaScript(` try { window.dispatchEvent(new Event('focus')); } catch (error) { // 忽略错误,避免影响主流程 } `); } catch (error) { log('error', 'handleBrowserViewFocus', error); } }; const handleBrowserViewBlur = (view) => { try { view.webContents?.executeJavaScript(` try { window.dispatchEvent(new Event('blur')); } catch (error) { // 忽略错误,避免影响主流程 } `); } catch (error) { log('error', 'handleBrowserViewBlur', error); } }; window.on('focus', () => { try { if (typeof window.getBrowserViews === 'function') { const views = window.getBrowserViews() || []; for (const view of views) { handleBrowserViewFocus(view); } } } catch (error) { log('error', 'focus', error); } }); window.on('blur', () => { try { if (typeof window.getBrowserViews === 'function') { const views = window.getBrowserViews(); for (const view of views) { handleBrowserViewBlur(view); } } } catch (error) { log('error', 'focus', error); } }); try { const _addBrowserView = window.addBrowserView; window.addBrowserView = (view, isSort = false) => { _addBrowserView.call(window, view); handleBrowserViewFocus(view); // 添加BrowserView后重新排序(如果未禁用自动排序) log('log', 'addBrowserView-sort', isSort, window.getBrowserViews()); if (isSort) { this.sortBrowserViewsDebounced(window, view); } }; const _removeBrowserView = window.removeBrowserView; window.removeBrowserView = (view, isSort = false) => { _removeBrowserView.call(window, view); handleBrowserViewBlur(view); // 移除BrowserView后重新排序(如果未禁用自动排序) log('log', 'removeBrowserView-sort', isSort); if (isSort) { this.sortBrowserViewsDebounced(window, view); } }; const _setBrowserView = window.setBrowserView; window.setBrowserView = (view, isSort = false) => { const views = window.getBrowserViews() || []; for (const existingView of views) { handleBrowserViewBlur(existingView); } _setBrowserView.call(window, view); handleBrowserViewFocus(view); log('log', 'setBrowserView-sort', isSort); if (isSort) { this.sortBrowserViewsDebounced(window, view); } }; } catch (error) { log('error', 'focus', error); } } if (options.url) { // @ts-ignore window.loadURL ? window.loadURL(options.url) : window.webContents.loadURL(options.url); if (options.browserWindow?.focusable !== false) { window?.focus?.(); } window?.webContents?.focus?.(); } } catch (error) { log('error', 'create', error); throw error; } if (!window) { throw new Error(`Failed to create window: ${name}`); } return window; } _setLoadingView(window, createOptions) { if (createOptions) { const { loadingView, preventOriginNavigate = false, } = createOptions; let _loadingView = new BrowserView({ webPreferences: { sandbox: false, // session: getCustomSession(), contextIsolation: false, nodeIntegration: true, // 允许loadURL与文件路径在开发环境 webSecurity: false, } }); if (window.isDestroyed()) { return; } const loadLoadingView = () => { const [viewWidth, viewHeight] = window.getSize(); window.addBrowserView(_loadingView); _loadingView.setBounds({ x: 0, y: 0, width: viewWidth || 10, height: viewHeight || 10, }); log('log', 'loadLoadingView', window._name); _loadingView.webContents.loadURL(loadingView?.url || ''); }; const onFailure = lodash.debounce(() => { if (_loadingView.webContents && !_loadingView.webContents.isDestroyed()) { _loadingView.webContents.close(); } if (window.isDestroyed()) { return; } if (window) { window.removeBrowserView(_loadingView); } }, 300); loadLoadingView(); window.on('resize', lodash.debounce(() => { if (_loadingView.webContents && !_loadingView.webContents.isDestroyed()) { if (window.isDestroyed()) { return; } const [viewWidth, viewHeight] = window.getSize(); _loadingView.setBounds({ x: 0, y: 0, width: viewWidth || 10, height: viewHeight || 10 }); } }, 500)); window.webContents.on('will-navigate', (e) => { if (preventOriginNavigate) { e.preventDefault(); return; } if (window.isDestroyed()) { return; } if (_loadingView.webContents && !_loadingView.webContents.isDestroyed()) { window.addBrowserView(_loadingView); } else { // if loadingView has been destroyed _loadingView = new BrowserView(); loadLoadingView(); } }); window.webContents.on('dom-ready', onFailure); window.webContents.on('crashed', onFailure); window.webContents.on('unresponsive', onFailure); window.webContents.on('did-fail-load', onFailure); window.webContents.on('did-finish-load', onFailure); window.webContents.on('did-stop-loading', onFailure); } } /** * 检查窗口是否已销毁 */ _isWindowDestroyed(win) { try { return !win || !win.webContents || win.webContents.isDestroyed(); } catch { return true; } } /** * 安全地获取窗口的 webContents.id * 如果 webContents 已销毁,返回 undefined */ _getWebContentsId(win) { try { if (!win || !win.webContents) { return undefined; } if (win.webContents.isDestroyed()) { return undefined; } return win.webContents.id; } catch { return undefined; } } /** * 判断是否本地/内网IP */ _isLocalhost(hostname) { return (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || /^192\.168\./.test(hostname) || /^10\./.test(hostname) || /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname)); } /** * 清理已销毁的窗口(延迟执行,避免频繁检查) */ _cleanupDestroyedWindows() { const toDelete = []; this.windows.forEach((win, key) => { if (this._isWindowDestroyed(win)) { toDelete.push(key); // 同步清理名称索引 if (win?._name) { this.windowsByName.delete(win._name); } } }); toDelete.forEach(key => this.windows.delete(key)); } get(idOrName) { log('log', 'get', idOrName); let win; if (typeof idOrName === 'number') { // 按 ID 查找(O(1)),使用 webContents.id win = this.windows.get(idOrName); } else if (typeof idOrName === 'string') { // 按名称查找(O(1),使用索引) const winId = this.windowsByName.get(idOrName); if (winId !== undefined) { win = this.windows.get(winId); } // 如果索引中找不到,回退到遍历查找(兼容旧数据) if (!win) { this.windows.forEach(w => { if (w._name === idOrName) { win = w; // 更新索引,使用 webContents.id const webContentsId = this._getWebContentsId(w); if (webContentsId !== undefined) { this.windowsByName.set(idOrName, webContentsId); } } }); } } // 检查找到的窗口是否已销毁 if (win && !this._isWindowDestroyed(win)) { return win; } // 窗口已销毁,触发清理 if (win) { const winId = this._getWebContentsId(win); if (winId !== undefined) { this.windows.delete(winId); } if (win._name) { this.windowsByName.delete(win._name); } } // 延迟清理其他已销毁的窗口 this.cleanupDestroyedWindowsDebounced(); return undefined; } getAll(type) { log('log', 'getAll'); // 先清理已销毁的窗口 const toDelete = []; this.windows.forEach((win, key) => { if (this._isWindowDestroyed(win)) { toDelete.push(key); if (win?._name) { this.windowsByName.delete(win._name); } } }); toDelete.forEach(key => this.windows.delete(key)); if (!type) { return this.windows; } const result = new Map(); this.windows.forEach((win, key) => { if (!this._isWindowDestroyed(win) && win._type === type) { result.set(key, win); } }); // 使用类型断言,TypeScript 会通过方法重载确保类型正确 return result; } close(idOrName) { log('log', 'close', idOrName); const win = this.get(idOrName); if (!win) { return false; } const winId = this._getWebContentsId(win); if (winId !== undefined) { this.windows.delete(winId); } if (win._name) { this.windowsByName.delete(win._name); } try { if (win._type === 'BV') { // 从所有 BW 窗口中移除该 BV this.windows.forEach(i => { if (i?._type === 'BW' && !this._isWindowDestroyed(i)) { const _win = i; try { _win.removeBrowserView(win); } catch (error) { // 忽略错误,可能已经移除 } } }); } // @ts-ignore win.webContents?.destroy?.(); // @ts-ignore win.close?.(); // @ts-ignore win.destroy?.(); } catch (error) { log('error', 'close window error:', error); } return true; } closeAll() { log('log', 'closeAll'); const windowsToClose = Array.from(this.windows.values()); // 先清空 Map,避免在遍历过程中修改 this.windows.clear(); this.windowsByName.clear(); // 收集所有 BW 窗口,用于批量移除 BV const bwWindows = windowsToClose.filter(w => w._type === 'BW' && !this._isWindowDestroyed(w)); const bvWindows = windowsToClose.filter(w => w._type === 'BV'); // 先从所有 BW 窗口中移除 BV bvWindows.forEach(bv => { bwWindows.forEach(bw => { try { bw.removeBrowserView(bv); } catch (error) { // 忽略错误,可能已经移除 } }); }); // 然后销毁所有窗口 windowsToClose.forEach((win) => { try { if (this._isWindowDestroyed(win)) { return; } // @ts-ignore win.webContents?.destroy?.(); // @ts-ignore win.close?.(); // @ts-ignore win.destroy?.(); } catch (error) { log('error', 'closeAll error:', error); } }); } rename(idOrName, newName) { log('log', 'rename', idOrName, newName); const win = this.get(idOrName); if (!win) { return undefined; } // 检查新名字是否已存在(使用索引加速) const existingWinId = this.windowsByName.get(newName); if (existingWinId !== undefined) { const existingWin = this.windows.get(existingWinId); if (existingWin && existingWin !== win && !this._isWindowDestroyed(existingWin)) { // 新名字已被占用 return undefined; } } // 更新名称索引 const oldName = win._name; const winId = this._getWebContentsId(win); if (winId !== undefined) { this.windowsByName.delete(oldName); this.windowsByName.set(newName, winId); } // 修改名字并同步 webContents win._name = newName; initWebContentsVal(win, `${this.preload || ''}`); return win; } reInitUrl(idOrName, url) { log('log', 'reInitUrl', idOrName, url); const win = this.get(idOrName); if (!win) { return undefined; } win._initUrl = url; initWebContentsVal(win, `${this.preload || ''}`); return win; } getPreload() { log('log', 'getPreload'); return this.preload; } _applyBrowserWindowOptions(win, options) { const browserWindowOptions = options.browserWindow || {}; // 设置父窗口 if (typeof browserWindowOptions.parent === 'number') { const parentWin = BrowserWindow.fromId(browserWindowOptions.parent); if (parentWin) { win.setParentWindow(parentWin); } } // 设置窗口大小 if (typeof browserWindowOptions.width === 'number' && typeof browserWindowOptions.height === 'number') { win.setBounds({ width: browserWindowOptions.width, height: browserWind