You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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();
}

核心问题分析

  1. 重复注入content script:background脚本在tabs.onUpdated的status=complete时每次都注入脚本,页面刷新、前进后退等操作都会触发,导致多个独立的content script上下文同时运行。
  2. 旧脚本未及时清理:旧脚本的定时器仍在执行,当尝试调用Chrome API(如chrome.storage.local.set)时,因上下文已失效抛出错误。
  3. 原代码存在小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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 13:05:29