refactor: 优化标签页加载状态管理与 URL 加载逻辑

- 新增 loadContentUrl 方法统一处理完整 URL 和相对路径的加载
  - 优化标签页加载状态,忽略 SPA 路由切换(isSameDocument),避免频繁闪烁
  - 添加定时检查器确保加载状态正确停止
  - windowClose/windowDestroy 支持识别 tab 页面发送者,仅关闭对应标签
  - 子窗口重启过程中不再意外销毁窗口
  - 微应用打开标签页时传递标题信息
  - isLocalHost 对空 URL 和相对路径返回 true
This commit is contained in:
kuaifan 2026-01-10 15:44:58 +00:00
parent ce42c2a660
commit 4929d44ce7
6 changed files with 111 additions and 43 deletions

102
electron/electron.js vendored
View File

@ -625,10 +625,10 @@ function createChildWindow(args) {
// 加载地址 // 加载地址
const hash = `${args.hash || args.path}`; const hash = `${args.hash || args.path}`;
if (/^https?:/i.test(hash)) { if (/^https?:/i.test(hash)) {
browser.loadURL(hash) // 完整 URL 直接加载
.then(_ => { }) browser.loadURL(hash).then(_ => { }).catch(_ => { })
.catch(_ => { })
} else if (isPreload) { } else if (isPreload) {
// preload 窗口尝试调用 __initializeApp失败则 loadUrl
browser browser
.webContents .webContents
.executeJavaScript(`if(typeof window.__initializeApp === 'function'){window.__initializeApp('${hash}')}else{throw new Error('no function')}`, true) .executeJavaScript(`if(typeof window.__initializeApp === 'function'){window.__initializeApp('${hash}')}else{throw new Error('no function')}`, true)
@ -636,6 +636,7 @@ function createChildWindow(args) {
utils.loadUrl(browser, serverUrl, hash) utils.loadUrl(browser, serverUrl, hash)
}); });
} else { } else {
// 相对路径使用 loadUrl
utils.loadUrl(browser, serverUrl, hash) utils.loadUrl(browser, serverUrl, hash)
} }
@ -774,7 +775,7 @@ function createWebTabWindow(args) {
// force=true 时重新加载 // force=true 时重新加载
if (args.force === true && args.url) { if (args.force === true && args.url) {
viewItem.view.webContents.loadURL(args.url).catch(_ => {}); utils.loadContentUrl(viewItem.view.webContents, serverUrl, args.url);
} }
return existing.windowId; return existing.windowId;
} }
@ -1050,10 +1051,13 @@ function createWebTabView(windowId, args) {
} }
browserView.webContents.on('destroyed', () => { browserView.webContents.on('destroyed', () => {
// 清理 name 映射
if (browserView.tabName) { if (browserView.tabName) {
webTabNameMap.delete(browserView.tabName); webTabNameMap.delete(browserView.tabName);
} }
if (browserView._loadingChecker) {
clearInterval(browserView._loadingChecker);
browserView._loadingChecker = null;
}
closeWebTabInWindow(windowId, browserView.webContents.id); closeWebTabInWindow(windowId, browserView.webContents.id);
}); });
browserView.webContents.setWindowOpenHandler(({url}) => { browserView.webContents.setWindowOpenHandler(({url}) => {
@ -1134,27 +1138,46 @@ function createWebTabView(windowId, args) {
favicon: base64Favicon || '' favicon: base64Favicon || ''
}).then(_ => { }); }).then(_ => { });
}); });
browserView.webContents.on('did-start-loading', _ => { // 页面加载状态管理忽略SPA路由切换(isSameDocument)
// 使用动态窗口ID支持标签在窗口间转移 browserView._loadingActive = false;
const currentWindowId = browserView.webTabWindowId; browserView._loadingChecker = null;
const wd = webTabWindows.get(currentWindowId); const dispatchLoading = (event) => {
if (!wd || !wd.window) return; const wd = webTabWindows.get(browserView.webTabWindowId);
utils.onDispatchEvent(wd.window.webContents, { if (wd && wd.window) {
event: 'start-loading', utils.onDispatchEvent(wd.window.webContents, {
id: browserView.webContents.id, event,
}).then(_ => { }); id: browserView.webContents.id,
}).then(_ => { });
}
};
const startLoading = () => {
if (browserView._loadingActive) return;
browserView._loadingActive = true;
dispatchLoading('start-loading');
if (!browserView._loadingChecker) {
browserView._loadingChecker = setInterval(() => {
if (browserView.webContents.isDestroyed() || !browserView.webContents.isLoading()) {
stopLoading();
}
}, 3000);
}
};
const stopLoading = () => {
if (browserView._loadingChecker) {
clearInterval(browserView._loadingChecker);
browserView._loadingChecker = null;
}
if (!browserView._loadingActive) return;
browserView._loadingActive = false;
dispatchLoading('stop-loading');
};
browserView.webContents.on('did-start-navigation', (_, _url, _isInPlace, isMainFrame, _frameProcessId, _frameRoutingId, _navigationId, isSameDocument) => {
if (isMainFrame && !isSameDocument) {
startLoading();
}
}); });
browserView.webContents.on('did-stop-loading', _ => { browserView.webContents.on('did-stop-loading', _ => {
// 使用动态窗口ID支持标签在窗口间转移 stopLoading();
const currentWindowId = browserView.webTabWindowId;
const wd = webTabWindows.get(currentWindowId);
if (!wd || !wd.window) return;
utils.onDispatchEvent(wd.window.webContents, {
event: 'stop-loading',
id: browserView.webContents.id,
}).then(_ => { });
// 加载完成暗黑模式下把窗口背景色改成白色,避免透明网站背景色穿透
if (nativeTheme.shouldUseDarkColors) { if (nativeTheme.shouldUseDarkColors) {
browserView.setBackgroundColor('#FFFFFF'); browserView.setBackgroundColor('#FFFFFF');
} }
@ -1179,7 +1202,8 @@ function createWebTabView(windowId, args) {
electronMenu.webContentsMenu(browserView.webContents, true); electronMenu.webContentsMenu(browserView.webContents, true);
browserView.webContents.loadURL(args.url).then(_ => { }).catch(_ => { }); // 加载地址
utils.loadContentUrl(browserView.webContents, serverUrl, args.url);
browserView.setVisible(true); browserView.setVisible(true);
@ -2128,20 +2152,36 @@ ipcMain.on('windowHidden', (event) => {
}) })
/** /**
* 关闭窗口 * 关闭窗口或关闭 tab如果发送者是 tab 中的页面
*/ */
ipcMain.on('windowClose', (event) => { ipcMain.on('windowClose', (event) => {
const win = BrowserWindow.fromWebContents(event.sender); const tabId = event.sender.id;
win.close() const windowId = findWindowIdByTabId(tabId);
if (windowId !== null) {
// 发送者是 tab 中的页面,只关闭这个 tab
closeWebTabInWindow(windowId, tabId);
} else {
// 发送者是独立窗口,关闭整个窗口
const win = BrowserWindow.fromWebContents(event.sender);
win?.close()
}
event.returnValue = "ok" event.returnValue = "ok"
}) })
/** /**
* 销毁窗口 * 销毁窗口或销毁 tab如果发送者是 tab 中的页面
*/ */
ipcMain.on('windowDestroy', (event) => { ipcMain.on('windowDestroy', (event) => {
const win = BrowserWindow.fromWebContents(event.sender); const tabId = event.sender.id;
win.destroy() const windowId = findWindowIdByTabId(tabId);
if (windowId !== null) {
// 发送者是 tab 中的页面,只关闭这个 tab
closeWebTabInWindow(windowId, tabId);
} else {
// 发送者是独立窗口,销毁整个窗口
const win = BrowserWindow.fromWebContents(event.sender);
win?.destroy()
}
event.returnValue = "ok" event.returnValue = "ok"
}) })

17
electron/lib/utils.js vendored
View File

@ -768,6 +768,23 @@ const utils = {
} }
}, },
/**
* 加载内容 URL自动判断完整 URL 或相对路径
* @param webContents - BrowserWindow WebContents 对象
* @param serverUrl - 服务器地址
* @param url - 要加载的 URL完整 URL 或相对路径
*/
loadContentUrl(webContents, serverUrl, url) {
if (!url) return;
if (/^https?:/i.test(url)) {
// 完整 URL 直接加载
webContents.loadURL(url).then(_ => { }).catch(_ => { })
} else {
// 相对路径使用 loadUrl 处理
utils.loadUrl(webContents, serverUrl, url)
}
},
/** /**
* 获取主题名称 * 获取主题名称
* @returns {string|*} * @returns {string|*}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 32 32"><g fill="none"><path d="M9.5 16a2.5 2.5 0 1 1-5 0a2.5 2.5 0 0 1 5 0zm9 0a2.5 2.5 0 1 1-5 0a2.5 2.5 0 0 1 5 0zm6.5 2.5a2.5 2.5 0 1 0 0-5a2.5 2.5 0 0 0 0 5z" fill="currentColor"></path></g></svg>

After

Width:  |  Height:  |  Size: 296 B

View File

@ -617,7 +617,10 @@
*/ */
isLocalHost(url) { isLocalHost(url) {
if (!url) { if (!url) {
return false return true
}
if (!/^https?:\/\//i.test(url)) {
return true
} }
try { try {
const uri = new URL(url) const uri = new URL(url)

View File

@ -119,6 +119,7 @@ export default {
data() { data() {
return { return {
assistShow: false, assistShow: false,
isRestarting: false,
userSelectOptions: {value: [], config: {}}, userSelectOptions: {value: [], config: {}},
backupConfigs: {}, backupConfigs: {},
@ -160,8 +161,8 @@ export default {
this.unmountAllMicroApp() this.unmountAllMicroApp()
}, },
assistShow(show) { assistShow(show) {
if (!show && $A.isSubElectron) { if (!show && $A.isSubElectron && !this.isRestarting) {
// Electron // Electron
$A.Electron.sendMessage('windowDestroy'); $A.Electron.sendMessage('windowDestroy');
} }
}, },
@ -486,7 +487,7 @@ export default {
path: path, path: path,
force: false, force: false,
config: Object.assign({ config: Object.assign({
title: ' ', title: appConfig.title || ' ',
parent: null, parent: null,
width: Math.min(window.screen.availWidth, 1440), width: Math.min(window.screen.availWidth, 1440),
height: Math.min(window.screen.availHeight, 900), height: Math.min(window.screen.availHeight, 900),
@ -518,7 +519,7 @@ export default {
path: config.url, path: config.url,
force: false, force: false,
config: { config: {
title: ' ', title: config.title || ' ',
parent: null, parent: null,
width: Math.min(window.screen.availWidth, 1440), width: Math.min(window.screen.availWidth, 1440),
height: Math.min(window.screen.availHeight, 900), height: Math.min(window.screen.availHeight, 900),
@ -706,15 +707,20 @@ export default {
* @param name * @param name
*/ */
async onRestartApp(name) { async onRestartApp(name) {
this.closeMicroApp(name, true) this.isRestarting = true
await new Promise(resolve => setTimeout(resolve, 300)); try {
this.closeMicroApp(name, true)
await new Promise(resolve => setTimeout(resolve, 300));
const app = this.backupConfigs[name]; const app = this.backupConfigs[name];
if (!app) { if (!app) {
$A.modalError("应用不存在"); $A.modalError("应用不存在");
return return
}
await this.onOpen(app)
} finally {
this.isRestarting = false
} }
await this.onOpen(app)
}, },
/** /**

View File

@ -5120,6 +5120,7 @@ export default {
const config = { const config = {
id: microAppId, id: microAppId,
name: data.name, name: data.name,
title: data.label || data.title || data.name,
url: $A.mainUrl(url), url: $A.mainUrl(url),
type: data.type || data.url_type, type: data.type || data.url_type,
background: data.background || null, background: data.background || null,