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, nativeImage,
globalShortcut, globalShortcut,
nativeTheme, nativeTheme,
screen,
Tray, Tray,
Menu, Menu,
WebContentsView,
BrowserWindow BrowserWindow
} = require('electron') } = require('electron')
@ -44,7 +42,7 @@ const electronMenu = require("./electron-menu");
const { startMCPServer, stopMCPServer } = require("./lib/mcp"); const { startMCPServer, stopMCPServer } = require("./lib/mcp");
const {onRenderer, renderer} = require("./lib/renderer"); const {onRenderer, renderer} = require("./lib/renderer");
const {onExport} = require("./lib/pdf-export"); 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 webTabManager = require("./lib/web-tab-manager");
const faviconCache = require("./lib/favicon-cache"); const faviconCache = require("./lib/favicon-cache");
@ -299,7 +297,7 @@ function createMainWindow() {
mainWindow.on('close', event => { mainWindow.on('close', event => {
if (!willQuitApp) { if (!willQuitApp) {
utils.onBeforeUnload(event, mainWindow).then(() => { webTabManager.onBeforeUnload(event, mainWindow).then(() => {
if (['darwin', 'win32'].includes(process.platform)) { if (['darwin', 'win32'].includes(process.platform)) {
if (mainWindow.isFullScreen()) { if (mainWindow.isFullScreen()) {
mainWindow.once('leave-full-screen', () => { 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 http = require('http')
const https = require('https') const https = require('https')
const crypto = require('crypto') 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 loger = require("electron-log");
const Store = require("electron-store"); const Store = require("electron-store");
const store = new Store(); const store = new Store();
@ -291,6 +291,7 @@ const utils = {
/** /**
* 正则提取域名 * 正则提取域名
* @param weburl * @param weburl
* @param toLowerCase
* @returns {string|string} * @returns {string|string}
*/ */
getDomain(weburl, toLowerCase = true) { 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 * @param webContents

View File

@ -15,7 +15,8 @@ const {
screen, screen,
Menu, Menu,
WebContentsView, WebContentsView,
BrowserWindow BrowserWindow,
dialog
} = require('electron') } = require('electron')
const utils = require('./utils') const utils = require('./utils')
@ -42,6 +43,9 @@ const webTabHeight = 40
// 快捷键关闭状态 Map<windowId, boolean> // 快捷键关闭状态 Map<windowId, boolean>
let webTabClosedByShortcut = new Map() let webTabClosedByShortcut = new Map()
// 存储已声明关闭拦截的 webContents id
const closeInterceptors = new Set()
// ============================================================ // ============================================================
// 预加载池 // 预加载池
// ============================================================ // ============================================================
@ -560,13 +564,19 @@ function createWebTabWindowInstance(windowId, position, mode = 'tab') {
// 检查页面是否有未保存数据 // 检查页面是否有未保存数据
if (isShortcut) { if (isShortcut) {
// 快捷键关闭:只检查当前激活的标签 // 快捷键关闭:只检查当前激活的标签
event.preventDefault()
// 获取当前激活标签
const activeTab = windowData.views.find(v => v.id === windowData.activeTabId) const activeTab = windowData.views.find(v => v.id === windowData.activeTabId)
if (!activeTab) return if (!activeTab) return
// 构造代理 window 对象
const proxyWindow = Object.create(webTabWindow, { const proxyWindow = Object.create(webTabWindow, {
webContents: { get: () => activeTab.view.webContents } webContents: { get: () => activeTab.view.webContents }
}) })
utils.onBeforeUnload(event, proxyWindow).then(() => {
// 检查并等待用户确认
onBeforeUnload(event, proxyWindow).then(() => {
closeWebTabInWindow(windowId, 0) closeWebTabInWindow(windowId, 0)
}) })
return return
@ -583,7 +593,7 @@ function createWebTabWindowInstance(windowId, position, mode = 'tab') {
webContents: { get: () => tab.view.webContents } webContents: { get: () => tab.view.webContents }
}) })
// 检查并等待用户确认(用户取消则 Promise 不 resolve中断循环 // 检查并等待用户确认(用户取消则 Promise 不 resolve中断循环
await utils.onBeforeUnload(event, proxyWindow) await onBeforeUnload(event, proxyWindow)
// 确认后关闭这个标签(最后一个标签关闭时窗口会自动销毁) // 确认后关闭这个标签(最后一个标签关闭时窗口会自动销毁)
closeWebTabInWindow(windowId, tab.id) closeWebTabInWindow(windowId, tab.id)
} }
@ -736,7 +746,7 @@ function createWebTabView(windowId, args) {
}) })
} else if (url && url !== 'about:blank') { } else if (url && url !== 'about:blank') {
// tab 模式下创建新标签 // tab 模式下创建新标签
createWebTabWindow({ url, afterId: browserView.webContents.id, windowId }) createWebTabWindow({ url, afterId: browserView.webContents.id, windowId: browserView.webTabWindowId })
} }
return { action: 'deny' } return { action: 'deny' }
}) })
@ -1136,7 +1146,7 @@ function safeCloseWebTab(windowId, tabId) {
const proxyWindow = Object.create(windowData.window, { const proxyWindow = Object.create(windowData.window, {
webContents: { get: () => tab.view.webContents } webContents: { get: () => tab.view.webContents }
}) })
utils.onBeforeUnload({ preventDefault: () => {} }, proxyWindow).then(() => { onBeforeUnload({ preventDefault: () => {} }, proxyWindow).then(() => {
closeWebTabInWindow(windowId, tabId) 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 注册 // IPC 注册
// ============================================================ // ============================================================
@ -1923,6 +2008,21 @@ function registerIPC() {
event.returnValue = "ok" 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, destroyAll,
closeAllWindowMode, closeAllWindowMode,
destroyAllWindowMode, destroyAllWindowMode,
// 关闭拦截管理
registerCloseInterceptor,
unregisterCloseInterceptor,
hasCloseInterceptor,
onBeforeUnload,
} }

View File

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