如何避免在扩展更新后发送消息时出现"Extension context invalidated"错误?



在发布更新后,我正努力为Chrome扩展的用户创造一种流畅的体验。

我在应用程序更新时重新插入我的内容脚本,即使用户继续在扩展更新后未刷新的页面上使用我的扩展,我的功能也能正常工作。刷新页面是理想的,但我不想强迫我的用户这样做。

然而,当我的代码试图从内容脚本/页面向后台页面发送消息时,在扩展更新之后,但在页面刷新之前,我在内容脚本控制台(插入内容脚本的页面)中收到以下错误:

未捕获错误:扩展上下文无效。

有办法"重建"连接吗?我尝试了一个长寿命的端口和定期的消息传递——更新Extension也有同样的效果。

任何想法/方向都值得赞赏。

以下是我的简单消息传递代码(在内容脚本中),它抛出了错误。。。但IS能够与后台脚本进行通信:

chrome.runtime.sendMessage({ msg: "recordFeedback", obj: commentObj, source: source}, function(response){
console.log('Sent to DB...response was: ', response, response.recordFeedbackResponse);
});

卸载扩展时,现有的内容脚本将失去与扩展其余部分的连接,即端口关闭,它们将无法使用runtime.sendMessage(),但内容脚本本身仍然可以继续工作,因为它们已经被注入到页面中。

当重新加载扩展时也是一样的:那些现有的内容脚本仍然存在,但无法发送消息;尝试这样做会导致类似您遇到的错误。此外,由于扩展在加载时将其内容脚本注入现有选项卡中是很常见的(在Chrome上,Firefox会自动执行),因此您最终会在给定的选项卡中运行多个内容脚本副本:原始的、现在已断开连接的和当前已连接的。

如果出现以下情况,可能会出现问题:(1)您的原始内容脚本仍在尝试与扩展的其余部分通信,或者(2)您的初始内容脚本执行了修改DOM之类的操作,因为您可能会多次执行这些更改。

这可能就是为什么即使在重新注入内容脚本后仍然会出现错误的原因:错误来自仍然存在但已断开连接的原始内容脚本

这转述了我之前研究这个问题时发现的一些有用的背景信息。请注意,Firefox会自动卸载内容脚本(稍后会详细介绍)

解决方案

如果你没有自动重新注入内容脚本,那么根据wOxxOm上面的评论,你可以尝试让现有脚本在升级时重新连接。

以下是一些进一步的信息和建议,如果您注入新的内容脚本,并且希望在注入新脚本时禁用原始的、断开连接的脚本,则这些信息和建议特别有用。

无论您是想尝试使用现有内容脚本重新连接,还是想停止原始内容脚本进行进一步更改并注入新内容脚本,都需要检测到已发生卸载。您可以使用端口的disconnect处理程序在卸载扩展时进行捕获。如果您的扩展只使用简单的一次性消息传递,而不使用端口,那么该代码就像在内容脚本启动时运行以下代码一样简单(我第一次在lydell的一些代码中看到这种技术)。

browser.runtime.connect().onDisconnect.addListener(function() {
// clean up when content script gets disconnected
})

在Firefox上,端口的disconnect不会被触发,因为内容脚本在收到之前就被删除了。这样做的优点是不需要进行检测(手动内容脚本注入也不需要,因为内容剧本也是自动注入的),但这确实意味着在内容脚本运行时,没有机会重置内容脚本对DOM所做的任何更改。

我建议访问chrome.runtime.id来判断扩展上下文是否无效。

// use null-safe operator since chrome.runtime
// is lazy inited and might return undefined
//
if (chrome.runtime?.id) {

}
if(typeof chrome.app.isInstalled!=='undefined'){
chrome.runtime.sendMessage()
}

我花了几个小时阅读文档、SO、chromium错误报告,最终修复了所有与此相关的错误"在扩展重新加载后重新连接到运行时";问题

此解决方案在更新/重新加载扩展后重新安装内容脚本,并确保旧的内容脚本不再与无效的运行时通信。

以下是整个相关代码:

manifest.json

content_scripts.matches中的所有URL也必须包含在permissions数组中。这是chrome.tabs.executeScript()工作所必需的
实际上,您可以删除content_scripts对象(因为它的唯一原因是自动注入内容脚本),并自己处理background.js中的所有(请参阅文档:"注入脚本")。但我保留并使用了它,以便更好地概述和";兼容性";原因。

{
"permissions": [
"tabs",
"http://example.org",
],
"background": {
"scripts": ["background.js"],
"persistent": false
},
"content_scripts": [
{
"matches": ["http://example.org/*"],
"js": ["contentScript.js"]
}
]
}

注意,content_scriptscontent_scripts[n]['js']一样是一个阵列。

background.js

const manifest = chrome.runtime.getManifest();
function installContentScript() {
// iterate over all content_script definitions from manifest
// and install all their js files to the corresponding hosts.
let contentScripts = manifest.content_scripts;
for (let i = 0; i < contentScripts.length; i++) {
let contScript = contentScripts[i];
chrome.tabs.query({ url: contScript.matches }, function(foundTabs) {
for (let j = 0; j < foundTabs.length; j++) {
let javaScripts = contScript.js;
for (let k = 0; k < javaScripts.length; k++) {
chrome.tabs.executeScript(foundTabs[j].id, {
file: javaScripts[k]
});          
}
}
});
}
}
chrome.runtime.onInstalled.addListener(installContentScript);

contentScript.js

var chromeRuntimePort = chrome.runtime.connect();
chromeRuntimePort.onDisconnect.addListener(() => {
chromeRuntimePort = undefined;
});
// when using the port, always check if valid/connected
function postToPort(msg) {
if (chromeRuntimePort) {
chromeRuntimePort.postMessage(msg);
}
}
// or
chromeRuntimePort?.postMessage('Hey, finally no errors');

请在后台脚本中尝试此操作。许多旧方法现在已经被弃用,所以我重构了代码。对于我的使用,我只安装了一个content_script文件。如果需要,您可以在chrome.runtime.getManifest().content_scripts数组上迭代以获得所有.js文件。

chrome.runtime.onInstalled.addListener(installScript);
function installScript(details){
// console.log('Installing content script in all tabs.');
let params = {
currentWindow: true
};
chrome.tabs.query(params, function gotTabs(tabs){
let contentjsFile = chrome.runtime.getManifest().content_scripts[0].js[0];
for (let index = 0; index < tabs.length; index++) {
chrome.tabs.executeScript(tabs[index].id, {
file: contentjsFile
},
result => {
const lastErr = chrome.runtime.lastError;
if (lastErr) {
console.error('tab: ' + tabs[index].id + ' lastError: ' + JSON.stringify(lastErr));
}
})
}
});    
}

在Chrome中,最重要的答案对我不起作用。它不是在重新加载扩展时运行,而是运行每个页面,甚至在刷新时也是如此。

以下是我决定实现的(在Chrome和Firefox中测试)。这将检查是否由于在开发中重新加载扩展而导致错误(英文字符串)。如果这是真的,那么它会通过全局变量禁止函数在扩展中执行导致错误的代码。它会记录所有其他错误。

function isReloadExtErr(err) {
if (err.toString().includes("Extension context invalidated")) {
console.log("Extension has been reloaded, set global var");
extensionActive = false;
return true;
}
return false;
}

然后在刷新后会导致错误的内部函数(使用浏览器API的函数)

function someFunction() {
try { 
if (!extensionActive) return;
/* code */ 
} catch (err) {
if (!isReloadExtErr(err)) console.error(err);
}
}

关闭浏览器选项卡并重新开始。为我工作。

相关内容

最新更新