一、描述

由于内容脚本在网页的上下文中运行而不是在扩展程序中运行,因此它们通常需要某种方式与扩展程序的其余部分(比如后端脚本)进行通信,例如,RSS 阅读器扩展可能使用内容脚本来检测页面上 RSS 提要的存在,然后通知后台页面以显示该页面的页面操作图标。

扩展程序与其内容脚本之间的通信通过使用消息传递来工作。

任何一方都可以侦听从另一端发送的消息,并在同一频道上进行响应。

消息可以包含任何有效的JSON对象(null,boolean,number,string,array或object)。

二、简单的双向通信

如果只需要将单个消息发送到扩展程序的另一部分(并且可选地获得响应),应该使用简化的 runtime.sendMessagetabs.sendMessage

这两个 API 可以分别从内容脚本向扩展发送一次性 JSON 可序列化消息,反之亦然。可选的回调参数允许处理来自另一方的响应(如果有响应的话)。

从内容脚本发送一个消息:

chrome.runtime.sendMessage({greeting: 'Hello'}, (response) => {
        console.log(response);
    });

如果需要指定向那个选项卡的扩展程序的内容脚本发送消息可以通过如下方式:

下面代码是向当前窗口的 activeTab 发送消息

chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
  chrome.tabs.sendMessage(tabs[0].id, {greeting: "hello"}, function(response) {
    console.log(response);
  });
});

消息的监听与响应:

下面是我在 background.js 做的消息的监听和响应处理

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
    console.log(sender.tab ? "from a content script:" + sender.tab.url : "from the extension");
    if (request.greeting == "hello")
        sendResponse({farewell: "goodbye"});
});

可以发现,当消息发出来之后,能够在双方继续拧简单的响应:

1.jpg

两端通信的消息结构其实是一样的

2.jpg

注意:如果多个页面正在侦听 onMessage 事件,则只有第一个调用特定事件的 sendResponse()才能成功发送响应。对该事件的所有其他响应都将被忽略。

注意: sendResponse 回调仅在同步使用时有效,或者如果事件处理程序返回 true 以指示它将异步响应。如果没有处理程序返回 true 或者 sendResponse 回调是垃圾收集的,则将自动调用 sendMessage 函数的回调。

三、长连接通信

某些场景下,我们可能需要长连接,可以通过 runtime.connecttabs.connect 打开从内容脚本到扩展程序 page 的通道。通道可以选择使用名称,以便区分不同类型的连接。

建立连接时,每个终端都有一个 runtime.Port 对象,用于通过该连接发送和接收消息。

比如我在内容脚本中通过下面的方式开启长连接:

button2.onclick = () => {
    const port = chrome.runtime.connect({ name: 'postbird' });
    port.postMessage({ name: 'ptbird' });
    port.onMessage.addListener((msg) => {
        if (msg.question === "what's your name?") {
            port.postMessage({ answer: 'My name is postbird' });
        } else if (msg.question === 'How are you?') {
            port.postMessage({ answer: "I am fine, thank U!" });
        }
    });
    console.log('Successed!');
}

将扩展程序中的请求发送到内容脚本写法非常相似,只是需要指定要连接的选项卡。 用 tabs.connect 替换上面的 connect 连接。

为了处理传入的连接,需要设置 runtime.onConnect 事件侦听器。这从内容脚本或扩展页面看起来是相同的.当扩展的另一部分调用 "connec()" 时,将触发此事件以及可用于通过连接发送和接收消息的 runtime.Port 对象。

比如下面的方式处理:

// background.js
chrome.runtime.onConnect.addListener((port) => {
    console.log(port);

    port.onMessage.addListener((msg) => {
        if (msg.name == "ptbird")
            port.postMessage({ question: "what's your name?" });
        else if (msg.answer == "My name is postbird")
            port.postMessage({ question: "How are you?" });
        else if (msg.answer == "I am fine, thank U!")
            port.postMessage({ question: "I don't get it." });
    })
});

建立连接的时候在 onConnect 中拿到 port 的信息如下:

3.jpg

content scripts 和 background.js 的交互历史如下:

4.jpg

5.jpg

端口生命时长

调用 tabs.connectruntime.connectruntime.connectNative 时,会创建一个 Port。此端口可以立即用于通过 postMessage 向另一端发送消息。

如果选项卡中有多个框架,则调用 tabs.connect 会导致对 runtime.onConnect 事件进行多次调用(对于选项卡中的每个框架一次)。

类似地,如果使用 runtime.connect ,则可以多次触发 onConnect 事件(对于扩展进程中的每一进程,一次)。

如果想知道何时关闭连接,可以通过 runtime.Port.onDisconnect 事件监听。当通道另一侧没有有效端口时会触发此事件。

这种情况发生在以下情况:

  • 另一端没有 runtime.onConnect 的监听器。
  • 卸载包含端口的选项卡
  • 调用 connect 的框架已卸载。
  • 接收端口的所有进程(通过 runtime.onConnect )都已卸载。
  • runtime.Port.disconnect is called by the other end
  • runtime.Port.disconnect 由另一端调用,请注意,如果连接调用在接收方端产生多个端口,并且在任何这些端口上调用 disconnect,则 onDisconnect 事件仅在发送方的端口上触发,而不在其他端口上触发。

代码

完整代码仓库:https://github.com/postbird/chrome-extensions-demo/tree/master/demo11-messagePassing