@lynker-desktop/electron-window-manager
Version:
electron-window-manager
1,228 lines (1,227 loc) • 70.7 kB
JavaScript
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