refactor: 优化窗口关闭拦截机制,采用声明式注册

- 将 onBeforeUnload 从 utils.js 移至 web-tab-manager.js
- 新增声明式拦截注册机制,前端通过 registerCloseInterceptor 声明需要拦截
- 仅对已声明拦截的页面执行 JS 检查,未声明的直接关闭
- 添加 5 秒超时保护,防止网页卡死导致无法关闭窗口
- 修复 command+w 快捷键关闭整个窗口而非当前 tab 的问题
This commit is contained in:
kuaifan 2026-01-14 22:29:36 +08:00
parent 1c27719ac4
commit cb414b48f6
4 changed files with 116 additions and 39 deletions

View File

@ -17,10 +17,8 @@ const {
nativeImage,
globalShortcut,
nativeTheme,
screen,
Tray,
Menu,
WebContentsView,
BrowserWindow
} = require('electron')
@ -44,7 +42,7 @@ const electronMenu = require("./electron-menu");
const { startMCPServer, stopMCPServer } = require("./lib/mcp");
const {onRenderer, renderer} = require("./lib/renderer");
const {onExport} = require("./lib/pdf-export");
const {allowedCalls, isWin, isMac} = require("./lib/other");
const {allowedCalls, isWin} = require("./lib/other");
const webTabManager = require("./lib/web-tab-manager");
const faviconCache = require("./lib/favicon-cache");
@ -299,7 +297,7 @@ function createMainWindow() {
mainWindow.on('close', event => {
if (!willQuitApp) {
utils.onBeforeUnload(event, mainWindow).then(() => {
webTabManager.onBeforeUnload(event, mainWindow).then(() => {
if (['darwin', 'win32'].includes(process.platform)) {
if (mainWindow.isFullScreen()) {
mainWindow.once('leave-full-screen', () => {

32
electron/lib/utils.js vendored
View File

@ -5,7 +5,7 @@ const dayjs = require("dayjs");
const http = require('http')
const https = require('https')
const crypto = require('crypto')
const {shell, dialog, session, net, Notification, nativeTheme} = require("electron");
const {shell, session, net, Notification, nativeTheme} = require("electron");
const loger = require("electron-log");
const Store = require("electron-store");
const store = new Store();
@ -291,6 +291,7 @@ const utils = {
/**
* 正则提取域名
* @param weburl
* @param toLowerCase
* @returns {string|string}
*/
getDomain(weburl, toLowerCase = true) {
@ -327,35 +328,6 @@ const utils = {
}
},
/**
* 窗口关闭事件
* @param event
* @param app
*/
onBeforeUnload(event, app) {
return new Promise(resolve => {
const contents = app.webContents
if (contents != null) {
contents.executeJavaScript(`if(typeof window.__onBeforeUnload === 'function'){window.__onBeforeUnload()}`, true).then(options => {
if (utils.isJson(options)) {
let choice = dialog.showMessageBoxSync(app, options)
if (choice === 1) {
contents.executeJavaScript(`if(typeof window.__removeBeforeUnload === 'function'){window.__removeBeforeUnload()}`, true).catch(() => {});
resolve()
}
} else if (options !== true) {
resolve()
}
}).catch(_ => {
resolve()
})
event.preventDefault()
} else {
resolve()
}
})
},
/**
* 新窗口打开事件
* @param webContents

View File

@ -15,7 +15,8 @@ const {
screen,
Menu,
WebContentsView,
BrowserWindow
BrowserWindow,
dialog
} = require('electron')
const utils = require('./utils')
@ -42,6 +43,9 @@ const webTabHeight = 40
// 快捷键关闭状态 Map<windowId, boolean>
let webTabClosedByShortcut = new Map()
// 存储已声明关闭拦截的 webContents id
const closeInterceptors = new Set()
// ============================================================
// 预加载池
// ============================================================
@ -560,13 +564,19 @@ function createWebTabWindowInstance(windowId, position, mode = 'tab') {
// 检查页面是否有未保存数据
if (isShortcut) {
// 快捷键关闭:只检查当前激活的标签
event.preventDefault()
// 获取当前激活标签
const activeTab = windowData.views.find(v => v.id === windowData.activeTabId)
if (!activeTab) return
// 构造代理 window 对象
const proxyWindow = Object.create(webTabWindow, {
webContents: { get: () => activeTab.view.webContents }
})
utils.onBeforeUnload(event, proxyWindow).then(() => {
// 检查并等待用户确认
onBeforeUnload(event, proxyWindow).then(() => {
closeWebTabInWindow(windowId, 0)
})
return
@ -583,7 +593,7 @@ function createWebTabWindowInstance(windowId, position, mode = 'tab') {
webContents: { get: () => tab.view.webContents }
})
// 检查并等待用户确认(用户取消则 Promise 不 resolve中断循环
await utils.onBeforeUnload(event, proxyWindow)
await onBeforeUnload(event, proxyWindow)
// 确认后关闭这个标签(最后一个标签关闭时窗口会自动销毁)
closeWebTabInWindow(windowId, tab.id)
}
@ -736,7 +746,7 @@ function createWebTabView(windowId, args) {
})
} else if (url && url !== 'about:blank') {
// tab 模式下创建新标签
createWebTabWindow({ url, afterId: browserView.webContents.id, windowId })
createWebTabWindow({ url, afterId: browserView.webContents.id, windowId: browserView.webTabWindowId })
}
return { action: 'deny' }
})
@ -1136,7 +1146,7 @@ function safeCloseWebTab(windowId, tabId) {
const proxyWindow = Object.create(windowData.window, {
webContents: { get: () => tab.view.webContents }
})
utils.onBeforeUnload({ preventDefault: () => {} }, proxyWindow).then(() => {
onBeforeUnload({ preventDefault: () => {} }, proxyWindow).then(() => {
closeWebTabInWindow(windowId, tabId)
})
}
@ -1483,6 +1493,81 @@ function destroyAllWindowMode() {
}
}
// ============================================================
// 关闭拦截管理
// ============================================================
/**
* 注册关闭拦截前端声明需要拦截关闭事件
* @param webContentsId
*/
function registerCloseInterceptor(webContentsId) {
closeInterceptors.add(webContentsId)
}
/**
* 取消关闭拦截
* @param webContentsId
*/
function unregisterCloseInterceptor(webContentsId) {
closeInterceptors.delete(webContentsId)
}
/**
* 检查是否有关闭拦截
* @param webContentsId
* @returns {boolean}
*/
function hasCloseInterceptor(webContentsId) {
return closeInterceptors.has(webContentsId)
}
/**
* 窗口关闭事件
* @param event
* @param app
* @param timeout
*/
function onBeforeUnload(event, app, timeout = 5000) {
return new Promise(resolve => {
const contents = app.webContents
if (contents != null && !contents.isDestroyed()) {
// 检查是否有声明拦截,没有声明则直接关闭,不执行 JS
if (!hasCloseInterceptor(contents.id)) {
resolve()
return
}
// 有声明拦截,执行 JS带超时保护
const timeoutPromise = new Promise(r => setTimeout(() => r({ __timeout: true }), timeout))
const jsPromise = contents.executeJavaScript(`if(typeof window.__onBeforeUnload === 'function'){window.__onBeforeUnload()}`, true)
Promise.race([jsPromise, timeoutPromise]).then(options => {
if (utils.isJson(options)) {
// 超时,直接允许关闭
if (options.__timeout) {
resolve()
return
}
// 显示确认对话框
let choice = dialog.showMessageBoxSync(app, options)
if (choice === 1) {
contents.executeJavaScript(`if(typeof window.__removeBeforeUnload === 'function'){window.__removeBeforeUnload()}`, true).catch(() => {});
resolve()
}
} else if (options !== true) {
resolve()
}
}).catch(_ => {
resolve()
})
event.preventDefault()
} else {
resolve()
}
})
}
// ============================================================
// IPC 注册
// ============================================================
@ -1923,6 +2008,21 @@ function registerIPC() {
event.returnValue = "ok"
})
/**
* 注册关闭拦截前端声明需要拦截关闭事件
*/
ipcMain.on('registerCloseInterceptor', (event) => {
registerCloseInterceptor(event.sender.id)
event.returnValue = "ok"
})
/**
* 取消关闭拦截
*/
ipcMain.on('unregisterCloseInterceptor', (event) => {
unregisterCloseInterceptor(event.sender.id)
event.returnValue = "ok"
})
}
// ============================================================
@ -1950,4 +2050,10 @@ module.exports = {
destroyAll,
closeAllWindowMode,
destroyAllWindowMode,
// 关闭拦截管理
registerCloseInterceptor,
unregisterCloseInterceptor,
hasCloseInterceptor,
onBeforeUnload,
}

View File

@ -594,6 +594,7 @@ export default {
return true;
}
}
this.$Electron.sendMessage('registerCloseInterceptor')
window.__onBeforeOpenWindow = ({url}) => {
const urlType = this.getUrlMethodType(url)
if (urlType === 2) {