Chrome 扩展中的持久服务工作者
Persistent Service Worker in Chrome Extension
我需要在我的 Chrome 扩展中将我的 Service Worker 定义为持久的,因为我正在使用 webRequest API 拦截以特定请求的形式传递的一些数据,但我不不知道我该怎么做。我已经尝试了所有方法,但我的 Service Worker 一直在卸载。
如何保持加载并等待请求被拦截?
这是由 ManifestV3 中的这些问题引起的:
crbug.com/1024211,工作人员不会因 webRequest 事件而醒来。
下面列出了解决方法。
crbug.com/1271154,工人更新后随机损坏
主要固定在 Chrome 101.
根据 service worker (SW) 规范,它不能持久化,浏览器必须在一段时间后强制终止所有 SW 连接,例如网络请求或端口,这在 Chrome 是 5 分钟。 Chromium 团队 currently considers 这种行为是有意的并且对扩展有利,因为该团队从未调查过这种 SW 行为不好的现实情况,以防扩展必须观察频繁发生的事件:
- chrome.tabs.onUpdated/onActivated,
- chrome.webNavigation 如果没有限定到罕见 url,
- chrome.webRequest 如果没有限定到罕见的 url 或类型,
- chrome.runtime.onMessage/onConnect 所有选项卡中来自内容脚本的消息。
此类事件是响应用户操作而生成的,因此会自然暂停几分钟,在此期间软件会终止。然后它再次开始一个新事件,这至少需要 50 毫秒来创建进程加上加载和编译代码的时间,平均约 50 毫秒,即它比调用简单 JS 事件所需的约 1 毫秒重约 100 倍听众。对于一个活跃的在线用户来说,它可能每天重启数百次,从而耗尽 CPU/disk/battery 并且经常引入扩展反应的频繁可察觉的滞后。
解决方法
对于未唤醒的 webRequest
另外订阅一个 API,如 chrome.webNavigation
,如其他答案所示。
这适用于观察偶发事件的扩展程序,例如您为 webRequest/webNavigation 指定了 urls
过滤器,仅针对一个很少访问的站点。可以重新设计此类扩展以避免需要持久的后台脚本,因此它一天只会启动几次,这将有利于内存占用,同时不会给 CPU 带来太多压力。您将通过 chrome.storage.session
(临时,最大 1MB)或 chrome.storage.local
或 IndexedDB 在每个侦听器中 variables/state variables/state,这对于 [=146] 来说要快得多=] 数据.
但是,如果您必须观察频繁发生的事件(列在本答案的开头),则必须使用以下解决方法人为地延长后台脚本的生命周期。
存在可连接选项卡时的“持久”服务工作者
如果您不使用在所有选项卡中运行的内容脚本的端口(在下面的另一个解决方法中显示),这里有一个从任何选项卡的内容脚本或另一个打开 runtime 端口的示例像弹出页面一样的扩展页面,并在 5 分钟过去之前重新连接它。
缺点:
- 需要打开网页选项卡或打开扩展程序tab/popup。
- 内容脚本的广泛主机权限(如
<all_urls>
或 *://*/*
),这会将大多数扩展放入网上商店的慢审队列。
警告!如果您使用 sendMessage,还要实施 sendMessage 的解决方法(如下)。
警告!如果您已经使用 chrome.runtime.connect(下方)的变通方法和在所有选项卡中运行的内容脚本,则不需要此操作。
manifest.json,相关部分:
"permissions": ["scripting"],
"host_permissions": ["<all_urls>"],
"background": {"service_worker": "bg.js"}
后台服务工作者bg.js:
let lifeline;
keepAlive();
chrome.runtime.onConnect.addListener(port => {
if (port.name === 'keepAlive') {
lifeline = port;
setTimeout(keepAliveForced, 295e3); // 5 minutes minus 5 seconds
port.onDisconnect.addListener(keepAliveForced);
}
});
function keepAliveForced() {
lifeline?.disconnect();
lifeline = null;
keepAlive();
}
async function keepAlive() {
if (lifeline) return;
for (const tab of await chrome.tabs.query({ url: '*://*/*' })) {
try {
await chrome.scripting.executeScript({
target: { tabId: tab.id },
function: () => chrome.runtime.connect({ name: 'keepAlive' }),
// `function` will become `func` in Chrome 93+
});
chrome.tabs.onUpdated.removeListener(retryOnTabUpdate);
return;
} catch (e) {}
}
chrome.tabs.onUpdated.addListener(retryOnTabUpdate);
}
async function retryOnTabUpdate(tabId, info, tab) {
if (info.url && /^(file|https?):/.test(info.url)) {
keepAlive();
}
}
如果你也使用sendMessage
始终在 chrome.runtime.onMessage 侦听器中调用 sendResponse(),即使您不需要响应。这是 MV3 中的错误。另外,确保你在不到 5 分钟的时间内完成,否则立即调用 sendResponse 并通过 chrome.tabs.sendMessage(到选项卡)或 chrome.runtime.sendMessage(到弹出窗口)发回新消息工作完成后。
如果您已经使用端口,例如chrome.runtime.连接
在 5 分钟过去之前重新连接每个端口。
后台脚本示例:
chrome.runtime.onConnect.addListener(port => {
if (port.name !== 'foo') return;
port.onMessage.addListener(onMessage);
port.onDisconnect.addListener(deleteTimer);
port._timer = setTimeout(forceReconnect, 250e3, port);
});
function onMessage(msg, port) {
console.log('received', msg, 'from', port.sender);
}
function forceReconnect(port) {
deleteTimer(port);
port.disconnect();
}
function deleteTimer(port) {
if (port._timer) {
clearTimeout(port._timer);
delete port._timer;
}
}
客户端脚本示例,例如内容脚本:
let port;
function connect() {
port = chrome.runtime.connect({name: 'foo'});
port.onDisconnect.addListener(connect);
port.onMessage.addListener(msg => {
console.log('received', msg, 'from bg');
});
}
connect();
“永远”,通过专用选项卡打开该选项卡
打开一个包含扩展页面的新标签页,例如chrome.tabs.create({url: 'bg.html'})
.
它将具有与 ManifestV2 的永久后台页面相同的功能,但 a) 它是可见的并且 b) 无法通过 chrome.extension.getBackgroundPage
访问(可以用 chrome.extension.getViews 代替)。
缺点:
- 消耗更多内存,
- 在标签条中浪费 space,
- 分散用户注意力,
- 当多个扩展程序打开这样一个选项卡时,不利因素会滚雪球并成为真正的 PITA。
您可以通过向页面添加 info/logs/charts/dashboard 并添加 beforeunload
侦听器来防止标签被意外关闭,从而使您的用户更容易接受它。
ManifestV3 的未来
希望 Chromium 能够提供 API 来控制这种行为,而无需求助于这种肮脏的黑客攻击和可悲的解决方法。同时在 crbug.com/1152255 中描述您的用例(如果此处尚未描述),以帮助 Chromium 团队意识到一个既定事实,即许多扩展可能需要一个持久的后台脚本,持续任意时间段,并且至少有一个这样的大多数扩展用户可能会安装扩展。
WebSocket
从我的扩展 service worker 的 chrome.runtime
侦听器注册中注册的回调不会被调用,这听起来几乎是同一个问题。
我通过向其添加以下代码来确保我的 service worker 永不结束来解决这个问题:
function keepServiceRunning() {
setTimeout(keepServiceRunning, 2000);
}
keepServiceRunning()
在此之后,我的回调现在会按预期被调用。
如果我理解正确,您可以通过警报唤醒服务人员 (background.js)。看下面的例子:
- 清单 v3
"permissions": [
"alarms"
],
- 服务工作者background.js:
chrome.alarms.create({ periodInMinutes: 4.9 })
chrome.alarms.onAlarm.addListener(() => {
console.log('log for debug')
});
很遗憾,这不是我的问题,可能你也有不同的问题。当我刷新开发扩展或停止和 运行 产品扩展时,某个时间服务人员会死掉。当我关闭和打开浏览器时,worker 不会 运行 并且 worker 中的任何侦听器也不会 运行。它尝试手动注册工人。例如:
// override.html
<!DOCTYPE html>
<html lang="en">
<head>...<head>
<body>
...
<script defer src="override.js"></script>
<body>
<html>
// override.js - this code is running in new tab page
navigator.serviceWorker.getRegistrations().then((res) => {
for (let worker of res) {
console.log(worker)
if (worker.active.scriptURL.includes('background.js')) {
return
}
}
navigator.serviceWorker
.register(chrome.runtime.getURL('background.js'))
.then((registration) => {
console.log('Service worker success:', registration)
}).catch((error) => {
console.log('Error service:', error)
})
})
这个解决方案对我有部分帮助,但这并不重要,因为我必须在不同的选项卡上注册工作人员。可能有人知道决定。我会很高兴的。
与 chrome.webRequest API 不同,chrome.webNavigation API 完美运行因为 chrome.webNavigation API 可以唤醒 service worker,现在你可以尝试把 chrome.webRequestAPIapi里面chrome.webNavigation.
chrome.webNavigation.onBeforeNavigate.addListener(function(){
chrome.webRequest.onResponseStarted.addListener(function(details){
//.............
//.............
},{urls: ["*://domain/*"],types: ["main_frame"]});
},{
url: [{hostContains:"domain"}]
});
由于 that chrome.webNavigation 可以唤醒 MV3 中的 service worker,以下是我的解决方法:
// manifest.json
...
"background": {
"service_worker": "background.js"
},
"host_permissions": ["https://example.com/api/*"],
"permissions": ["webRequest", "webNavigation"]
...
在我的例子中,它监听 onHistoryStateUpdated 事件来唤醒 service worker:
// background.js
chrome.webNavigation.onHistoryStateUpdated.addListener((details) => {
console.log('wake me up');
});
chrome.webRequest.onSendHeaders.addListener(
(details) => {
// code here
},
{
urls: ['https://example.com/api/*'],
types: ['xmlhttprequest'],
},
['requestHeaders']
);
我找到了使扩展程序保持活动状态的不同解决方案。它通过使用辅助扩展打开到我们的主扩展的连接端口来改进 wOxxOm 的答案。然后这两个扩展尝试在任何断开连接的情况下相互通信,从而保持两个活动。
之所以需要这个,是因为根据我公司的另一个团队的说法,wOxxOm 的回答被证明是不可靠的。据报道,他们的软件最终会以不确定的方式失败。
话又说回来,我的解决方案适用于我的公司,因为我们正在部署企业安全软件,我们将强制安装扩展。在其他 use-cases.
中,让用户安装 2 个扩展可能仍然是不可取的
我需要在我的 Chrome 扩展中将我的 Service Worker 定义为持久的,因为我正在使用 webRequest API 拦截以特定请求的形式传递的一些数据,但我不不知道我该怎么做。我已经尝试了所有方法,但我的 Service Worker 一直在卸载。
如何保持加载并等待请求被拦截?
这是由 ManifestV3 中的这些问题引起的:
crbug.com/1024211,工作人员不会因 webRequest 事件而醒来。
下面列出了解决方法。crbug.com/1271154,工人更新后随机损坏
主要固定在 Chrome 101.根据 service worker (SW) 规范,它不能持久化,浏览器必须在一段时间后强制终止所有 SW 连接,例如网络请求或端口,这在 Chrome 是 5 分钟。 Chromium 团队 currently considers 这种行为是有意的并且对扩展有利,因为该团队从未调查过这种 SW 行为不好的现实情况,以防扩展必须观察频繁发生的事件:
- chrome.tabs.onUpdated/onActivated,
- chrome.webNavigation 如果没有限定到罕见 url,
- chrome.webRequest 如果没有限定到罕见的 url 或类型,
- chrome.runtime.onMessage/onConnect 所有选项卡中来自内容脚本的消息。
此类事件是响应用户操作而生成的,因此会自然暂停几分钟,在此期间软件会终止。然后它再次开始一个新事件,这至少需要 50 毫秒来创建进程加上加载和编译代码的时间,平均约 50 毫秒,即它比调用简单 JS 事件所需的约 1 毫秒重约 100 倍听众。对于一个活跃的在线用户来说,它可能每天重启数百次,从而耗尽 CPU/disk/battery 并且经常引入扩展反应的频繁可察觉的滞后。
解决方法
对于未唤醒的 webRequest
另外订阅一个 API,如 chrome.webNavigation
,如其他答案所示。
这适用于观察偶发事件的扩展程序,例如您为 webRequest/webNavigation 指定了 urls
过滤器,仅针对一个很少访问的站点。可以重新设计此类扩展以避免需要持久的后台脚本,因此它一天只会启动几次,这将有利于内存占用,同时不会给 CPU 带来太多压力。您将通过 chrome.storage.session
(临时,最大 1MB)或 chrome.storage.local
或 IndexedDB 在每个侦听器中 variables/state variables/state,这对于 [=146] 来说要快得多=] 数据.
但是,如果您必须观察频繁发生的事件(列在本答案的开头),则必须使用以下解决方法人为地延长后台脚本的生命周期。
存在可连接选项卡时的“持久”服务工作者
如果您不使用在所有选项卡中运行的内容脚本的端口(在下面的另一个解决方法中显示),这里有一个从任何选项卡的内容脚本或另一个打开 runtime 端口的示例像弹出页面一样的扩展页面,并在 5 分钟过去之前重新连接它。
缺点:
- 需要打开网页选项卡或打开扩展程序tab/popup。
- 内容脚本的广泛主机权限(如
<all_urls>
或*://*/*
),这会将大多数扩展放入网上商店的慢审队列。
警告!如果您使用 sendMessage,还要实施 sendMessage 的解决方法(如下)。
警告!如果您已经使用 chrome.runtime.connect(下方)的变通方法和在所有选项卡中运行的内容脚本,则不需要此操作。
manifest.json,相关部分:
"permissions": ["scripting"], "host_permissions": ["<all_urls>"], "background": {"service_worker": "bg.js"}
后台服务工作者bg.js:
let lifeline; keepAlive(); chrome.runtime.onConnect.addListener(port => { if (port.name === 'keepAlive') { lifeline = port; setTimeout(keepAliveForced, 295e3); // 5 minutes minus 5 seconds port.onDisconnect.addListener(keepAliveForced); } }); function keepAliveForced() { lifeline?.disconnect(); lifeline = null; keepAlive(); } async function keepAlive() { if (lifeline) return; for (const tab of await chrome.tabs.query({ url: '*://*/*' })) { try { await chrome.scripting.executeScript({ target: { tabId: tab.id }, function: () => chrome.runtime.connect({ name: 'keepAlive' }), // `function` will become `func` in Chrome 93+ }); chrome.tabs.onUpdated.removeListener(retryOnTabUpdate); return; } catch (e) {} } chrome.tabs.onUpdated.addListener(retryOnTabUpdate); } async function retryOnTabUpdate(tabId, info, tab) { if (info.url && /^(file|https?):/.test(info.url)) { keepAlive(); } }
如果你也使用sendMessage
始终在 chrome.runtime.onMessage 侦听器中调用 sendResponse(),即使您不需要响应。这是 MV3 中的错误。另外,确保你在不到 5 分钟的时间内完成,否则立即调用 sendResponse 并通过 chrome.tabs.sendMessage(到选项卡)或 chrome.runtime.sendMessage(到弹出窗口)发回新消息工作完成后。
如果您已经使用端口,例如chrome.runtime.连接
在 5 分钟过去之前重新连接每个端口。
后台脚本示例:
chrome.runtime.onConnect.addListener(port => { if (port.name !== 'foo') return; port.onMessage.addListener(onMessage); port.onDisconnect.addListener(deleteTimer); port._timer = setTimeout(forceReconnect, 250e3, port); }); function onMessage(msg, port) { console.log('received', msg, 'from', port.sender); } function forceReconnect(port) { deleteTimer(port); port.disconnect(); } function deleteTimer(port) { if (port._timer) { clearTimeout(port._timer); delete port._timer; } }
客户端脚本示例,例如内容脚本:
let port; function connect() { port = chrome.runtime.connect({name: 'foo'}); port.onDisconnect.addListener(connect); port.onMessage.addListener(msg => { console.log('received', msg, 'from bg'); }); } connect();
“永远”,通过专用选项卡打开该选项卡
打开一个包含扩展页面的新标签页,例如chrome.tabs.create({url: 'bg.html'})
.
它将具有与 ManifestV2 的永久后台页面相同的功能,但 a) 它是可见的并且 b) 无法通过 chrome.extension.getBackgroundPage
访问(可以用 chrome.extension.getViews 代替)。
缺点:
- 消耗更多内存,
- 在标签条中浪费 space,
- 分散用户注意力,
- 当多个扩展程序打开这样一个选项卡时,不利因素会滚雪球并成为真正的 PITA。
您可以通过向页面添加 info/logs/charts/dashboard 并添加 beforeunload
侦听器来防止标签被意外关闭,从而使您的用户更容易接受它。
ManifestV3 的未来
希望 Chromium 能够提供 API 来控制这种行为,而无需求助于这种肮脏的黑客攻击和可悲的解决方法。同时在 crbug.com/1152255 中描述您的用例(如果此处尚未描述),以帮助 Chromium 团队意识到一个既定事实,即许多扩展可能需要一个持久的后台脚本,持续任意时间段,并且至少有一个这样的大多数扩展用户可能会安装扩展。
WebSocket
从我的扩展 service worker 的 chrome.runtime
侦听器注册中注册的回调不会被调用,这听起来几乎是同一个问题。
我通过向其添加以下代码来确保我的 service worker 永不结束来解决这个问题:
function keepServiceRunning() {
setTimeout(keepServiceRunning, 2000);
}
keepServiceRunning()
在此之后,我的回调现在会按预期被调用。
如果我理解正确,您可以通过警报唤醒服务人员 (background.js)。看下面的例子:
- 清单 v3
"permissions": [
"alarms"
],
- 服务工作者background.js:
chrome.alarms.create({ periodInMinutes: 4.9 })
chrome.alarms.onAlarm.addListener(() => {
console.log('log for debug')
});
很遗憾,这不是我的问题,可能你也有不同的问题。当我刷新开发扩展或停止和 运行 产品扩展时,某个时间服务人员会死掉。当我关闭和打开浏览器时,worker 不会 运行 并且 worker 中的任何侦听器也不会 运行。它尝试手动注册工人。例如:
// override.html
<!DOCTYPE html>
<html lang="en">
<head>...<head>
<body>
...
<script defer src="override.js"></script>
<body>
<html>
// override.js - this code is running in new tab page
navigator.serviceWorker.getRegistrations().then((res) => {
for (let worker of res) {
console.log(worker)
if (worker.active.scriptURL.includes('background.js')) {
return
}
}
navigator.serviceWorker
.register(chrome.runtime.getURL('background.js'))
.then((registration) => {
console.log('Service worker success:', registration)
}).catch((error) => {
console.log('Error service:', error)
})
})
这个解决方案对我有部分帮助,但这并不重要,因为我必须在不同的选项卡上注册工作人员。可能有人知道决定。我会很高兴的。
与 chrome.webRequest API 不同,chrome.webNavigation API 完美运行因为 chrome.webNavigation API 可以唤醒 service worker,现在你可以尝试把 chrome.webRequestAPIapi里面chrome.webNavigation.
chrome.webNavigation.onBeforeNavigate.addListener(function(){
chrome.webRequest.onResponseStarted.addListener(function(details){
//.............
//.............
},{urls: ["*://domain/*"],types: ["main_frame"]});
},{
url: [{hostContains:"domain"}]
});
由于
// manifest.json
...
"background": {
"service_worker": "background.js"
},
"host_permissions": ["https://example.com/api/*"],
"permissions": ["webRequest", "webNavigation"]
...
在我的例子中,它监听 onHistoryStateUpdated 事件来唤醒 service worker:
// background.js
chrome.webNavigation.onHistoryStateUpdated.addListener((details) => {
console.log('wake me up');
});
chrome.webRequest.onSendHeaders.addListener(
(details) => {
// code here
},
{
urls: ['https://example.com/api/*'],
types: ['xmlhttprequest'],
},
['requestHeaders']
);
我找到了使扩展程序保持活动状态的不同解决方案。它通过使用辅助扩展打开到我们的主扩展的连接端口来改进 wOxxOm 的答案。然后这两个扩展尝试在任何断开连接的情况下相互通信,从而保持两个活动。
之所以需要这个,是因为根据我公司的另一个团队的说法,wOxxOm 的回答被证明是不可靠的。据报道,他们的软件最终会以不确定的方式失败。
话又说回来,我的解决方案适用于我的公司,因为我们正在部署企业安全软件,我们将强制安装扩展。在其他 use-cases.
中,让用户安装 2 个扩展可能仍然是不可取的