From f9b66c12772994748f1510bf2a5117c728587996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Tue, 20 Oct 2026 14:08:58 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E9=A1=B5=E8=A2=AB=E4=B8=A2=E5=BC=83=E5=90=8E=20GM=5Fg?= =?UTF-8?q?etTab=20=E8=AF=BB=E4=B8=8D=E5=88=B0=20GM=5FsaveTab=20=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=20(#1791)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 浏览器未启用 WebContentsDiscard 时,丢弃标签页会用新 tabId 替换原标签页, 只触发 tabs.onReplaced、旧 tabId 不再触发 onRemoved。GM_saveTab 数据按 tabId 存放,此前只处理了 onRemoved,切回标签页后用新 tabId 查不到数据。 新增 onReplaced 监听:GM_saveTab 数据随标签页迁移到新 tabId,旧 tabId 的其余 标签页缓存按关闭释放。tab 释放逻辑从 init 闭包提为 PopupService.clearTabData, 便于在服务层测试。 --- src/app/service/service_worker/popup.test.ts | 32 ++++++++ src/app/service/service_worker/popup.ts | 78 ++++++++++++-------- 2 files changed, 79 insertions(+), 31 deletions(-) diff --git a/src/app/service/service_worker/popup.test.ts b/src/app/service/service_worker/popup.test.ts index 7cd26c15e..b8a3f023a 100644 --- a/src/app/service/service_worker/popup.test.ts +++ b/src/app/service/service_worker/popup.test.ts @@ -1172,3 +1172,35 @@ describe("PopupService removeDeletedScriptsFromPopupCaches 缓存清理边界情 await expect(service.getScriptMenu(-1)).resolves.toHaveLength(0); }); }); + +describe("PopupService clearTabData 标签页关闭/替换时释放 tab 数据", () => { + beforeEach(async () => { + await cacheInstance.clear(); + }); + + it("标签页被替换(如被丢弃后换了 tabId)时,GM_saveTab 数据迁移到新 tabId,旧 tabId 的缓存被清理", async () => { + await cacheInstance.set(`${CACHE_KEY_TAB_SCRIPT}${10}`, [createMenu("saved"), createMenu("unsaved")]); + await cacheInstance.set(`${CACHE_KEY_TAB_LOADED}${10}`, "https://example.com"); + await cacheInstance.set("GM_getTab:saved", { 10: { v: "tab10" }, 20: { v: "tab20" } }); + await cacheInstance.set("GM_getTab:unsaved", { 20: { v: "only-tab20" } }); + + const { service } = createService(); + await service.clearTabData(10, 11); + + await expect(cacheInstance.get("GM_getTab:saved")).resolves.toEqual({ 11: { v: "tab10" }, 20: { v: "tab20" } }); + await expect(cacheInstance.get("GM_getTab:unsaved")).resolves.toEqual({ 20: { v: "only-tab20" } }); + await expect(service.getScriptMenu(10)).resolves.toEqual([]); + await expect(cacheInstance.get(`${CACHE_KEY_TAB_LOADED}${10}`)).resolves.toBeUndefined(); + }); + + it("标签页关闭时删除该 tabId 的 GM_saveTab 数据,不影响其他标签页", async () => { + await cacheInstance.set(`${CACHE_KEY_TAB_SCRIPT}${10}`, [createMenu("saved")]); + await cacheInstance.set("GM_getTab:saved", { 10: { v: "tab10" }, 20: { v: "tab20" } }); + + const { service } = createService(); + await service.clearTabData(10); + + await expect(cacheInstance.get("GM_getTab:saved")).resolves.toEqual({ 20: { v: "tab20" } }); + await expect(service.getScriptMenu(10)).resolves.toEqual([]); + }); +}); diff --git a/src/app/service/service_worker/popup.ts b/src/app/service/service_worker/popup.ts index 33331444c..3d981a41b 100644 --- a/src/app/service/service_worker/popup.ts +++ b/src/app/service/service_worker/popup.ts @@ -808,6 +808,42 @@ export class PopupService { ); } + /** + * 释放标签页关闭时需要清理的数据。 + * @param replacedByTabId 标签页被替换时的新 tabId:对用户而言仍是同一个标签页,GM_saveTab 数据随之迁移而不是删除 + */ + clearTabData(tabId: number, replacedByTabId?: number) { + runCountMap.delete(tabId); + scriptCountMap.delete(tabId); + cacheInstance.del(`${CACHE_KEY_TAB_LOADED}${tabId}`); + const list = this.updateMenuCommands.get(tabId); + if (list) { + // 避免 menuCommand 更新在 Tab 移除后触发 + list.length = 0; + this.updateMenuCommands.delete(tabId); + } + return cacheInstance.tx(`${CACHE_KEY_TAB_SCRIPT}${tabId}`, (scripts: ScriptMenu[] | undefined, tx) => { + if (scripts) { + tx.del(); + return Promise.all( + scripts.map(({ uuid }) => { + // GM_saveTab 的数据按 tabId 存放,只能借助该 tab 运行过的脚本列表找到, + // 所以必须在这里、在删除脚本列表的同一事务里处理 + return cacheInstance.tx(`GM_getTab:${uuid}`, (tabData: { [key: number]: any } | undefined, tx) => { + if (tabData) { + if (replacedByTabId !== undefined && tabId in tabData) { + tabData[replacedByTabId] = tabData[tabId]; + } + delete tabData[tabId]; + tx.set(tabData); + } + }); + }) + ); + } + }); + } + init() { // 处理脚本菜单数据 this.mq.subscribe("registerMenuCommand", this.registerMenuCommand.bind(this)); @@ -817,35 +853,6 @@ export class PopupService { this.dealBackgroundScriptInstall(); // 监听tab开关 - const clearData = async (tabId: number) => { - runCountMap.delete(tabId); - scriptCountMap.delete(tabId); - cacheInstance.del(`${CACHE_KEY_TAB_LOADED}${tabId}`); - const list = this.updateMenuCommands.get(tabId); - if (list) { - // 避免 menuCommand 更新在 Tab 移除后触发 - list.length = 0; - this.updateMenuCommands.delete(tabId); - } - // 清理数据tab关闭需要释放的数据 - cacheInstance.tx(`${CACHE_KEY_TAB_SCRIPT}${tabId}`, (scripts: ScriptMenu[] | undefined, tx) => { - if (scripts) { - tx.del(); - return Promise.all( - scripts.map(({ uuid }) => { - // 处理GM_saveTab关闭事件, 由于需要用到tab相关的脚本数据,所以需要在这里处理 - // 避免先删除了数据获取不到 - return cacheInstance.tx(`GM_getTab:${uuid}`, (tabData: { [key: number]: any } | undefined, tx) => { - if (tabData) { - delete tabData[tabId]; - tx.set(tabData); - } - }); - }) - ); - } - }); - }; chrome.tabs.onRemoved.addListener((tabId) => { const lastError = chrome.runtime.lastError; if (lastError) { @@ -853,7 +860,16 @@ export class PopupService { // 没有 tabId 资讯,无法释放数据 return; } - clearData(tabId); + this.clearTabData(tabId); + }); + // 标签页被丢弃(discard)等情况下,浏览器会用新 tabId 替换原标签页,旧 tabId 不会再触发 onRemoved + chrome.tabs.onReplaced.addListener((addedTabId, removedTabId) => { + const lastError = chrome.runtime.lastError; + if (lastError) { + console.error("chrome.runtime.lastError in chrome.tabs.onReplaced:", lastError); + return; + } + this.clearTabData(removedTabId, addedTabId); }); /** * @param tabId 当前页面的 tabId。如 tabId 为 null, 则呼叫 getCurrentTab() 以API取当前页面的 tabId。 @@ -898,7 +914,7 @@ export class PopupService { // 没有开启脚本时不会触发pageLoad更新数据 // 这里做一次清理 if (runCountMap.has(details.tabId) && !this.runtime.isLoadScripts) { - clearData(details.tabId); + this.clearTabData(details.tabId); } });