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.localIndexedDB 在每个侦听器中 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 workerchrome.runtime 侦听器注册中注册的回调不会被调用,这听起来几乎是同一个问题。

我通过向其添加以下代码来确保我的 service worker 永不结束来解决这个问题:

function keepServiceRunning() {
    setTimeout(keepServiceRunning, 2000);
  }

keepServiceRunning()

在此之后,我的回调现在会按预期被调用。

如果我理解正确,您可以通过警报唤醒服务人员 (background.js)。看下面的例子:

  1. 清单 v3
"permissions": [
    "alarms"
],
  1. 服务工作者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 个扩展可能仍然是不可取的