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

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) {
if (!url) {
return false
return true
}
if (!/^https?:\/\//i.test(url)) {
return true
}
try {
const uri = new URL(url)

View File

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

View File

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