Chrome扩展Manifest V3中“Extension context invalidated”错误解决求助
问题:Chrome扩展Manifest V3出现"Uncaught Error: Extension context invalidated."
开发首个Manifest V3版本的Chrome扩展时,遇到Uncaught Error: Extension context invalidated.错误,推测原因是扩展更新或HTTPS页面硬刷新时,contentScript被多次注入,旧脚本仍在执行DOM操作和Chrome API调用,但此时脚本上下文已失效,导致报错。尝试过相关社区方案未解决,需代码修改方案。
原代码如下:
Manifest文件
{ "name": "Focuser", "description": "Focuses on a page during a specified time frame", "version": "0.1.0", "manifest_version": 3, "background": { "service_worker": "background.js" }, "permissions": [ "storage", "scripting", "alarms" ], "host_permissions": [ "<all_urls>" ] }
Background脚本
try { chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { if ('undefined' !== typeof tab.url) { // skip urls like "chrome://" to avoid extension error if (tab.url?.startsWith("chrome://")) return undefined; if(changeInfo.status == 'complete') { chrome.scripting.executeScript({ files: ['contentScript.js'], target: {tabId: tab.id} }) } } }) } catch(e) { console.log(e) }
ContentScript(已移除部分类)
/** * Foozle Focuser - a class that injects a temporary DOM element * into the site and then focus and click on it. * * The idea is that by maintaining the focus while running in the background a * Chrome tab can stream radio stations and other media content. This could be used * for anything a user wants to click. */ class FoozleFocuser { /** * private {string} target * the dom element used as the target for the inject DOM */ #target = null; /** * private {string} injectElement * the DOM element used as the target for the injected DOM */ #injectElement = ''; /** * private {string} injectElementClassName * the CSS class to be added to the injected DOM element */ #injectElementClassName = ''; /** * Constructor - set the target and injectElementClass - optionally no params passed * will set a target as document.body and the injectClassName will be foozle-focuser * @param {string} target - the passed target DOM element that will be used as the target for the injected DOM * @param {string} injectElementClassName - the CSS class to be added to the injected DOM element * @return Void */ constructor(target=null, injectElementClassName = 'foozle-focuser') { this.#target = this.#getTarget(target); this.#injectElementClassName = injectElementClassName; } /** * private SetInjectedElement * Creates the injected DOM element with a class that will be used as a target for the focus * @param {string} domElement * @return Void */ #setInjectedElement(domElement = 'div') { this.#injectElement = document.createElement(domElement); this.#injectElement.className = this.#injectElementClassName; } /** * private getTarget - queries the passed dom string. If null set the document.body as target * @param {string || null} target - The dom target element where the injection will be done * @return string - the target */ #getTarget(target=null) { if ( target == null ) { target = document.body; } else { target = document.querySelector(target); if ( target == null || 'undefined' == typeof target ) { target = document.body; } } return target; } /** * private focus - appends, focuses on, and clicks the injected DOM Element * @return Void */ #focus() { if (this.#target) { this.#target.appendChild(this.#injectElement); this.#target.focus(); this.#target.click(); let newDiv = document.querySelector('.' + this.#injectElementClassName) newDiv.parentNode.removeChild(newDiv); } } /** * private run - runs the setup for the target and injected element and then focuses on the target * @return Void */ run() { this.#setInjectedElement(); this.#focus(); } } class FoozleTypes { typeOf(value) { var s = typeof value; if (s === 'object') { if (value) { if (Object.prototype.toString.call(value) == '[object Array]') { s = 'array'; } } else { s = 'null'; } } return s; } checkTypes(argList, typeList) { for (var i = 0; i < typeList.length; i++) { if (typeOf(argList[i]) !== typeList[i]) { throw 'wrong type: expecting ' + typeList[i] + ", found " + typeOf(argList[i]); } } } } class FoozleCounter { getStoredCount(key='focuserCount') { this.item = localStorage.getItem(key); if ( null == this.item || 'undefined' == this.item ) { localStorage.setItem(key, 0); } this.item = parseInt(this.item) let count = this.item; count++; localStorage.setItem(key, count); return count; } } let FC = new FoozleCounter(); let FF = new FoozleFocuser(); if ('undefined' == typeof intervalId) { var intervalId = setInterval(() => { if (!chrome.runtime?.id) { // The extension was reloaded and this script is orphaned clearInterval(intervalId); return; } FF.run(); // Get the updated count let count = FC.getStoredCount(); // Store and report the count // @TODO - change the console log to the popup page chrome.storage.local.set({key: count}, function() { console.log('Count is set to ' + count); }); }, 45000); } if('undefined' == typeof init ) { var init = () => { if (!chrome.runtime?.id) { // The extension was reloaded and this script is orphaned clearInterval(init); return; } FF.run(); console.log('count', FC.getStoredCount()) } init(); }
核心问题分析
- 重复注入content script:background脚本在
tabs.onUpdated的status=complete时每次都注入脚本,页面刷新、前进后退等操作都会触发,导致多个独立的content script上下文同时运行。 - 旧脚本未及时清理:旧脚本的定时器仍在执行,当尝试调用Chrome API(如
chrome.storage.local.set)时,因上下文已失效抛出错误。 - 原代码存在小bug:如
FoozleTypes类中调用typeOf未绑定实例,导致方法调用失败;init函数中错误地对函数对象执行clearInterval。
代码修改方案
1. Background脚本:避免重复注入content script
修改注入逻辑,仅在页面URL变化且加载完成时注入,同时检查是否已注入脚本,避免重复执行:
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { // 跳过chrome://协议页面 if (!tab.url || tab.url.startsWith("chrome://")) return; // 仅在页面加载完成且URL发生变化时执行 if (changeInfo.status === 'complete' && changeInfo.url) { try { // 检查当前标签页是否已注入过content script const existingScripts = await chrome.scripting.getScripts({ target: { tabId: tabId }, ids: ['focuser-content-script'] }); if (existingScripts.length === 0) { await chrome.scripting.executeScript({ files: ['contentScript.js'], target: { tabId: tabId }, id: 'focuser-content-script' // 给脚本设置唯一ID,方便后续检查 }); } } catch (e) { console.error('注入content script失败:', e); } } });
2. Content Script:完善上下文失效处理与资源清理
添加chrome.runtime.onInvalidated监听,在上下文失效时立即清理定时器;修复原代码bug,给Chrome API调用添加错误捕获:
/** * Foozle Focuser - 注入临时DOM元素并保持焦点,用于后台页面保持媒体播放 */ class FoozleFocuser { #target = null; #injectElement = ''; #injectElementClassName = ''; constructor(target=null, injectElementClassName = 'foozle-focuser') { this.#target = this.#getTarget(target); this.#injectElementClassName = injectElementClassName; } #setInjectedElement(domElement = 'div') { this.#injectElement = document.createElement(domElement); this.#injectElement.className = this.#injectElementClassName; } #getTarget(target=null) { if (target == null) { target = document.body; } else { target = document.querySelector(target); if (!target) { target = document.body; } } return target; } #focus() { if (this.#target) { this.#target.appendChild(this.#injectElement); this.#target.focus(); this.#target.click(); const newDiv = document.querySelector('.' + this.#injectElementClassName); if (newDiv && newDiv.parentNode) { newDiv.parentNode.removeChild(newDiv); } } } run() { this.#setInjectedElement(); this.#focus(); } } class FoozleTypes { typeOf(value) { let s = typeof value; if (s === 'object') { if (value) { if (Object.prototype.toString.call(value) === '[object Array]') { s = 'array'; } } else { s = 'null'; } } return s; } checkTypes(argList, typeList) { for (let i = 0; i < typeList.length; i++) { if (this.typeOf(argList[i]) !== typeList[i]) { throw `wrong type: expecting ${typeList[i]}, found ${this.typeOf(argList[i])}`; } } } } class FoozleCounter { getStoredCount(key='focuserCount') { let item = localStorage.getItem(key); if (!item) { localStorage.setItem(key, '0'); item = '0'; } let count = parseInt(item, 10); count++; localStorage.setItem(key, count.toString()); return count; } } let intervalId = null; const FC = new FoozleCounter(); const FF = new FoozleFocuser(); // 初始化执行 function init() { if (!chrome.runtime?.id) return; FF.run(); console.log('count', FC.getStoredCount()); } // 定时执行逻辑 function startInterval() { intervalId = setInterval(async () => { if (!chrome.runtime?.id) { clearInterval(intervalId); return; } FF.run(); const count = FC.getStoredCount(); try { await chrome.storage.local.set({ key: count }); console.log('Count is set to ' + count); } catch (e) { // 捕获上下文失效导致的API调用错误 console.error('存储计数失败:', e); clearInterval(intervalId); } }, 45000); } // 监听上下文失效事件,立即清理定时器 chrome.runtime.onInvalidated.addListener(() => { if (intervalId) { clearInterval(intervalId); intervalId = null; } }); // 启动初始化和定时任务 init(); startInterval();
3. Manifest文件:可选优化(声明式注入)
若无需动态注入逻辑,可改用Manifest声明式注入,Chrome会自动管理注入时机和重复注入问题:
{ "name": "Focuser", "description": "Focuses on a page during a specified time frame", "version": "0.1.0", "manifest_version": 3, "background": { "service_worker": "background.js" }, "permissions": [ "storage", "alarms" ], "host_permissions": [ "<all_urls>" ], "content_scripts": [ { "matches": ["<all_urls>"], "exclude_matches": ["chrome://*/*"], "js": ["contentScript.js"], "run_at": "document_idle" } ] }
使用此配置后,可删除background中的注入逻辑,进一步简化代码。
内容的提问来源于stack exchange,提问作者ServerStorm
相关产品推荐
相关产品推荐

