在后台上下文中的脚本之间进行通信(后台脚本、浏览器操作、页面操作、选项页面等)

Communicate between scripts in the background context (background script, browser action, page action, options page, etc.)

我 运行 遇到了将数据从后台脚本发送到 pageAction 脚本的问题。我的内容脚本添加了一个 <iframe /> 并且 <iframe /> 中的 JavaScript 正在从我的后台脚本接收数据,但它似乎没有在我的 pageAction 中检索到。

在我的后台脚本中,我有类似的东西:

chrome.tabs.sendMessage(senderTab.tab.id, 
{
   foo:bar
}); 

其中 senderTab.tab.id 是我后台脚本中 onMessage 侦听器中的 "sender"。

在由我的内容脚本注入的 <iframe /> 加载的 JavaScript 中,我有类似的内容:

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
      console.log("received in iframe:", request);
    }   
});

<iframe /> 完全按照预期收到消息。

我将相同的 JavaScript 放入我的 page_action.js,但它没有从后台脚本接收任何数据。 pageAction 在我调用 chrome.tabs.sendMessage(senderTab.tab.id ...

之前用 chrome.pageAction.show(senderTab.tab.id); 激活

附加到我的 pageAction 的 HTML 页面不是同一个选项卡的一部分吗?由于此 tabId 使我能够激活/"show" 图标,我认为 pageAction JavaScript 中的侦听器也应该从 chrome.tabs.sendMessage(senderTab.tab.id ...

接收

在我的内容脚本中,我使用以下内容将数据发送到后台脚本:

chrome.runtime.sendMessage({
  foo: bar
});  

当内容脚本发送上述消息时,pageAction JavaScript 正在拾取它。


如何让后台脚本正确地将数据发送到我的 pageAction?我不想让 pageAction request/poll,而是希望 pageAction 只听和接收。例如,如果它显示的pageAction HTML,它应该能够在后台页面发生变化时实时更新。

在后台上下文中与页面通信

在后台上下文中打开的页面包括:

使用tabs.sendMessage()(MDN) will not send a message to any of them. You would need to use runtime.sendMessage()(MDN)向他们发送消息。其中任何一个的范围,除了背景页面和事件页面,只在显示时存在。显然,当代码不存在时,您无法与代码进行通信。当作用域存在时,您可以使用以下方式与它们中的任何一个进行通信:

  • 直接
    从后台上下文中,您可以在另一个页面中直接更改变量或调用函数,该页面 也在后台上下文中 (即不是内容脚本),在获得对其全局的引用之后范围,其 Window, using extension.getViews()(MDN), extension.getBackgroundPage()(MDN), or other method(MDN).
    例如,您可以在第一个返回视图的页面中调用使用 function myFunction 创建的函数,方法如下:

    winViews = chrome.extension.getViews();
    winViews[0].myFunction(foo); 
    

    请注意,在您从 tabs.create()(MDN) or windows.create()(MDN) 的回调中,新打开的选项卡或 window 的视图可能尚不存在。您将需要使用一些方法来等待视图存在。2 请参阅下文以了解与新打开的选项卡或 windows.

    直接在其他页面的范围内操作值允许您传达您想要的任何类型的数据。

  • 消息传递
    使用 chrome.runtime.onMessage(MDN), 3 which were sent with chrome.runtime.sendMessage()(MDN) 接收消息。每次您在 runtime.onMessage 侦听器中收到一条消息时,都会提供一个 sendResponse 函数作为第三个参数,它允许您直接响应该消息。如果原始发件人在对 chrome.runtime.sendMessage() 的调用中未提供回调以接收此类响应,则响应将丢失。如果使用 Promise(例如 Firefox 中的 browser.runtime.sendMessage()),当 Promise 实现时,响应将作为参数传递。如果您想异步发送响应,您需要 return true; 来自您的 runtime.onMessage 侦听器。

    端口
    您还可以连接端口,使用 chrome.runtime.connect()(MDN) and chrome.runtime.onConnect(MDN) 进行长期消息传递。

    使用chrome.tabs.sendMessage()发送到内容脚本
    如果你想发送 from 后台上下文(例如后台脚本或弹出窗口) 你可以使用 chrome.tabs.sendMessage()/chrome.runtime.onMessage,或使用 chrome.tabs.connect()(MDN)/chrome.runtime.onConnect 连接端口。

    JSON-仅可序列化数据
    使用消息传递,您只能传递 JSON-可序列化的数据。

    消息被后台的所有脚本接收,发送者除外
    发送到后台上下文的消息会被后台上下文中所有注册了侦听器的脚本接收,发送它的脚本除外。3 那里无法指定它仅由特定脚本接收。因此,如果您有多个潜在收件人,您将需要创建一种方法来确保收到的消息是针对该脚本的。这样做的方法通常依赖于消息中存在的特定属性(例如,使用 destinationrecipient 属性 来指示接收它的脚本,或者定义一些 type 的消息总是发给一个或另一个收件人),或者根据提供给消息处理程序的 sender(MDN) 进行区分(例如,如果来自一个发件人的消息是 总是 仅针对特定收件人)。没有固定的方法来执行此操作,您必须 choose/create 一种方法才能在您的扩展程序中使用。

    关于这个问题更详细的讨论,请看:

  • 存储区中的数据
    将数据存储到StorageArea(MDN) and be notified of the change in other scripts using chrome.storage.onChanged(MDN)storage.onChanged 事件可以在后台上下文和内容脚本中监听。

    您只能将 JSON 可序列化的数据存储到 StorageArea。

在任何特定情况下哪种方法最适合使用取决于您想要交流的内容(数据类型、状态更改等),以及您想要扩展的哪个或哪些部分从和到通信。例如,如果你想传达不是 JSON-serializable 的信息,你需要直接这样做(即不发送消息或使用 StorageArea)。您可以在同一个扩展中使用多种方法。

有关弹出窗口的更多信息

None 的弹出窗口(例如浏览器操作或页面操作)直接与活动选项卡相关联。每个选项卡没有共享或单独实例的概念。但是,用户可以在每个 Chrome window 中打开一个弹出窗口。如果打开了多个弹出窗口(每个 Chrome window 最多打开一个),则每个弹出窗口都在一个单独的实例中(单独的范围;有自己的 Window),但都在相同的上下文。当弹出窗口实际可见时,它存在于背景上下文中。

每个 Chrome window 一次只能打开一个页面操作或浏览器操作弹出窗口。将打开的 HTML 文件将是为当前 window 和 用户通过单击 page/browser 操作打开的活动选项卡定义的文件按钮。通过使用 chrome.browserAction.setPopup()(MDN), or chrome.pageAction.setPopup()(MDN) 并指定 tabId,可以为不同的选项卡分配不同的 HTML 文档。弹出窗口 can/will 因多种原因被销毁,但肯定是当另一个选项卡成为弹出窗口打开的 window 中的活动选项卡时。

但是,使用的任何通信方法都只会与 is/are 当前打开的通信,而不是未打开的通信。如果弹出窗口一次打开多个 Chrome window,则它们是单独的实例,具有自己的范围(即它们自己的 Window)。您可以将此 想成 在多个选项卡中打开同一网页。

如果您有后台脚本,则后台脚本上下文在 Chrome 的整个实例中都是持久的。如果您没有后台脚本,上下文可能会在需要时创建(例如显示弹出窗口)并在不再需要时销毁。

chrome.tabs.sendMessage() 无法与 通信 弹出窗口

如前所述,即使弹出窗口确实存在,它也会存在于后台上下文中。调用 chrome.tabs.sendMessage() 将消息发送到 注入 tab/frame 的内容脚本,而不是后台上下文。因此,它不会像弹出窗口那样向非内容脚本发送消息。

操作按钮:enable/disable(浏览器操作)与 show/hide(页面操作)

调用chrome.pageAction.show()(MDN)只会导致显示页面操作按钮。它不会导致显示任何关联的 popup。如果 popup/options page/other 页面实际上没有显示(不仅仅是按钮),那么它的范围不存在。当它不存在时,显然是收不到任何消息

而不是页面操作的能力 show()(MDN) or hide()(MDN) the button, browser actions can enable()(MDN) or disable()(MDN) 按钮。

通过您的扩展程序HTML以编程方式打开选项卡或window

您可以使用 tabs.create()(MDN) or windows.create()(MDN) 从您的扩展程序中打开一个选项卡或 window 包含一个 HTML 页面。但是,这两个 API 调用的回调在页面 DOM 存在之前执行,因此在与该页面关联的任何 JavaScript 存在之前执行。因此,您不能立即访问由该页面内容创建的 DOM,也不能与该页面的 JavaScript 进行交互。非常具体:不会添加 runtime.onMessage() 个侦听器,因此新打开的页面不会收到此时发送的消息。

解决此问题的最佳方法是:

  1. 准备好数据,以便新打开的页面可以在准备就绪时获取数据。通过在开始打开页面之前执行此操作:
    1. 如果源在后台上下文中:将数据存储在发送页面的全局范围内可用的变量中。然后打开页面可以直接使用chrome.extension.getBackgroundPage()读取数据
    2. 如果数据源在后台上下文或内容脚本中:将数据放入 storage.local(MDN)。当 JavaScript 为 运行 时,打开页面可以读取它。例如,您可以使用名为 messageToNewExtensionPage.
    3. 的密钥
  2. 如果您使用的是 runtime.sendMessage(),则通过从该页面的代码向数据源发送消息(使用 runtime.sendMessage(),开始从新打开的页面传输数据)或 tabs.sendMessage() 对于内容脚本源)请求数据。然后,带有数据的脚本可以使用 runtime.onMessage() 提供的 sendResponse(MDN) 函数发回数据。
  3. 等待与新打开的页面交互,直到至少 DOM 可用,如果不是直到页面的 JavaScript 有 运行。虽然可以在新打开的页面不提供它已启动和 运行ning 的特定通知的情况下执行此操作,但这样做更复杂并且仅在某些特定情况下有用(例如,您想在 [=342] 之前执行某些操作=] 在新页面中是 运行).2

其他参考文献

Chrome

火狐


  1. 除了一些小的例外:例如使用内容脚本将内容插入页面上下文。
  2. 您可以使用多种方法。哪种方式最好取决于你在做什么(例如,当你需要访问与视图中正在执行的代码相关的视图时)。一个简单的方法就是轮询等待视图存在。以下代码用于打开 window:

    chrome.windows.create({url: myUrl},function(win){
        //Poll for the view of the window ID. Poll every 50ms for a
        //  maximum of 20 times (1 second). Then do a second set of polling to
        //  accommodate slower machines. Testing on a single moderately fast machine
        //  indicated the view was available after, at most, the second 50ms delay.
        waitForWindowId(win.id,50,20,actOnViewFound,do2ndWaitForWinId);
    });
    function waitForWindowId(id,delay,maxTries,foundCallback,notFoundCallback) {
        if(maxTries--<=0){
            if(typeof notFoundCallback === 'function'){
                notFoundCallback(id,foundCallback);
            }
            return;
        }
        let views = chrome.extension.getViews({windowId:id});
        if(views.length > 0){
            if(typeof foundCallback === 'function'){
                foundCallback(views[0]);
            }
        } else {
            setTimeout(waitForWindowId,delay,id,delay,maxTries,foundCallback
                       ,notFoundCallback);
        }
    }
    function do2ndWaitForWinId(winId,foundCallback){
        //Poll for the view of the window ID. Poll every 500ms for max 40 times (20s).
        waitForWindowId(winId,500,40,foundCallback,windowViewNotFound);
    }
    function windowViewNotFound(winId,foundCallback){
        //Did not find the view for the window. Do what you want here.
        //  Currently fail quietly.
    }
    function actOnViewFound(view){
        //What you desire to happen with the view, when it exists.
    }
    
  3. From MDN:

    In Firefox versions prior to version 51, the runtime.onMessage listener will be called for messages sent from the same script (e.g. messages sent by the background script will also be received by the background script). In those versions of Firefox, if you unconditionally call runtime.sendMessage() from within a runtime.onMessage listener, you will set up an infinite loop which will max-out the CPU and lock-up Firefox. If you need to call runtime.sendMessage() from within a runtime.onMessage, you will need to check the sender.url property to verify you are not sending a message in response to a message which was sent from the same script. This bug was resolved as of Firefox 51.