消息传递

借助消息传递 API,您可以在与扩展程序关联的上下文中运行的不同脚本之间进行通信。这包括服务工作线程、chrome-extension://页面和内容脚本之间的通信。例如,RSS 阅读器扩展程序可以使用内容脚本来检测网页上是否存在 RSS Feed,然后通知服务工作线程更新相应网页的操作图标。

有两种消息传递 API:一种用于一次性请求,另一种用于长期连接,允许发送多条消息。

如需了解如何在扩展程序之间发送消息,请参阅跨扩展程序消息部分。

一次性请求

如需向扩展程序的另一部分发送单条消息,并选择性地获取响应,请调用 runtime.sendMessage()tabs.sendMessage()。借助这些方法,您可以从内容脚本向扩展程序发送一次性 JSON 可序列化消息,也可以从扩展程序向内容脚本发送一次性 JSON 可序列化消息。这两个 API 都会返回一个 Promise,该 Promise 会解析为收件人提供的响应。

从内容脚本发送请求如下所示:

content-script.js

(async () => {
  const response = await chrome.runtime.sendMessage({greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

响应

如需监听消息,请使用 chrome.runtime.onMessage 事件:

// Event listener
function handleMessages(message, sender, sendResponse) {
  if (message !== 'get-status') return;

  fetch('https://example.com')
    .then((response) => sendResponse({statusCode: response.status}))

  // Since `fetch` is asynchronous, must return an explicit `true`
  return true;
}

chrome.runtime.onMessage.addListener(handleMessages);

// From the sender's context...
const {statusCode} = await chrome.runtime.sendMessage('get-status');

当调用事件监听器时,系统会传递一个 sendResponse 函数作为第三个参数。这是一个可用于提供响应的函数。默认情况下,必须同步调用 sendResponse 回调。

如果您在不带任何参数的情况下调用 sendResponse,系统会发送 null 作为响应。

如需异步发送响应,您可以通过以下两种方式实现:返回 true 或返回 Promise。

返回 true

如需使用 sendResponse() 异步响应,请从事件监听器返回字面值 true(而不仅仅是真值)。这样做会使消息渠道对另一端保持开放状态,直到调用 sendResponse 为止,从而允许您稍后调用它。

返回 Promise

从 Chrome 144 开始,您可以在消息监听器中返回 promise 以进行异步响应。如果 promise 解析成功,则将其解析值作为响应发送。

如果 promise 被拒绝,则发件人的 sendMessage() 调用将被拒绝,并显示错误消息。如需了解详情和查看示例,请参阅错误处理部分。

以下示例展示了如何返回可能解析或拒绝的 promise:

// Event listener
function handleMessages(message, sender, sendResponse) {
  // Return a promise that wraps fetch
  // If the response is OK, resolve with the status. If it's not OK then reject
  // with the network error that prevents the fetch from completing.
  return new Promise((resolve, reject) => {
    fetch('https://example.com')
      .then(response => {
        if (!response.ok) {
          reject(response);
        } else {
          resolve(response.status);
        }
      })
      .catch(error => {
        reject(error);
      });
  });
}
chrome.runtime.onMessage.addListener(handleMessages);

您还可以将监听器声明为 async 以返回 Promise:

chrome.runtime.onMessage.addListener(async function(message, sender) {
  const response = await fetch('https://example.com');
  if (!response.ok) {
    // rejects the promise returned by `async function`.
    throw new Error(`Fetch failed: ${response.status}`);
  }
  // resolves the promise returned by `async function`.
  return {statusCode: response.status};
});

返回 Promise:async 函数陷阱

请注意,作为监听器的 async 函数将始终返回 promise,即使没有 return 语句也是如此。如果 async 监听器未返回值,则其 promise 会隐式解析为 undefined,并且 null 会作为响应发送给发送者。当有多个监听器时,这可能会导致意外行为:

// content_script.js
function handleResponse(message) {
    // The first listener promise resolves to `undefined` before the second
    // listener can respond. When a listener responds with `undefined`, Chrome
    // sends null as the response.
    console.assert(message === null);
}
function notifyBackgroundPage() {
    const sending = chrome.runtime.sendMessage('test');
    sending.then(handleResponse);
}
notifyBackgroundPage();

// background.js
chrome.runtime.onMessage.addListener(async (message) => {
    // This just causes the function to pause for a millisecond, but the promise
    // is *not* returned from the listener so it doesn't act as a response.
    await new Promise(resolve => {
        setTimeout(resolve, 1, 'OK');
    });
   // `async` functions always return promises. So once we
   // reach here there is an implicit `return undefined;`. Chrome translates
   // `undefined` responses to `null`.
});

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  return new Promise((resolve) => {
    setTimeout(resolve, 1000, 'response');
  });
});

错误处理

从 Chrome 144 开始,如果 onMessage 监听器抛出错误(同步或异步,通过返回拒绝的 promise),发送方中 sendMessage() 返回的 promise 将拒绝并显示错误消息。如果监听器尝试返回无法进行 JSON 序列化的响应,但未捕获生成的 TypeError,也会发生这种情况。

如果监听器返回的 Promise 被拒绝,则必须使用 Error 实例拒绝,以便发送者收到该错误消息。如果 Promise 因任何其他值(例如 nullundefined)而被拒绝,sendMessage() 将被拒绝并显示一般性错误消息。

如果为 onMessage 注册了多个监听器,则只有第一个做出响应、拒绝或抛出错误的监听器会影响发送方;所有其他监听器都会运行,但其结果会被忽略。

示例

如果监听器返回的 promise 被拒绝,则 sendMessage() 会被拒绝:

// sender.js
try {
  await chrome.runtime.sendMessage('test');
} catch (e) {
  console.log(e.message); // "some error"
}

// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  return Promise.reject(new Error('some error'));
});

如果监听器响应的值无法序列化,则 sendMessage() 会被拒绝:

// sender.js
try {
  await chrome.runtime.sendMessage('test');
} catch (e) {
  console.log(e.message); // "Error: Could not serialize message."
}

// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  sendResponse(() => {}); // Functions are not serializable
  return true; // Keep channel open for async sendResponse
});

如果某个监听器在任何其他监听器做出响应之前同步抛出错误,则该监听器的 sendMessage() promise 会被拒绝:

// sender.js
try {
  await chrome.runtime.sendMessage('test');
} catch (e) {
  console.log(e.message); // "error!"
}

// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  throw new Error('error!');
});

不过,如果一个监听器在另一个监听器抛出错误之前做出响应,则 sendMessage() 会成功:

// sender.js
const response = await chrome.runtime.sendMessage('test');
console.log(response); // "OK"

// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  sendResponse('OK');
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  throw new Error('error!');
});

长期连接

如需创建可重复使用的长期消息传递渠道,请调用:

  • runtime.connect(),用于将消息从内容脚本传递到扩展程序页面
  • tabs.connect(),用于将消息从扩展程序页面传递到内容脚本。

您可以通过传递带有 name 键的 options 参数来命名频道,以区分不同类型的连接:

const port = chrome.runtime.connect({name: "example"});

长效连接的一个潜在用例是自动表单填充扩展程序。内容脚本可能会为特定登录会话打开与扩展程序页面的通道,并为页面上的每个输入元素向扩展程序发送消息,以请求要填充的表单数据。共享连接允许扩展程序在扩展程序组件之间共享状态。

建立连接时,系统会为每个端点分配一个 runtime.Port 对象,用于通过该连接发送和接收消息。

使用以下代码从内容脚本打开渠道,并发送和监听消息:

content-script.js

const port = chrome.runtime.connect({name: "knockknock"});
port.onMessage.addListener(function(msg) {
  if (msg.question === "Who's there?") {
    port.postMessage({answer: "Madame"});
  } else if (msg.question === "Madame who?") {
    port.postMessage({answer: "Madame... Bovary"});
  }
});
port.postMessage({joke: "Knock knock"});

如需从扩展程序向内容脚本发送请求,请将上例中对 runtime.connect() 的调用替换为 tabs.connect()

若要处理内容脚本或扩展程序网页的传入连接,请设置 runtime.onConnect 事件监听器。当扩展程序的其他部分调用 connect() 时,它会激活此事件和 runtime.Port 对象。用于响应传入连接的代码如下所示:

service-worker.js

chrome.runtime.onConnect.addListener(function(port) {
  if (port.name !== "knockknock") {
    return;
  }
  port.onMessage.addListener(function(msg) {
    if (msg.joke === "Knock knock") {
      port.postMessage({question: "Who's there?"});
    } else if (msg.answer === "Madame") {
      port.postMessage({question: "Madame who?"});
    } else if (msg.answer === "Madame... Bovary") {
      port.postMessage({question: "I don't get it."});
    }
  });
});

序列化

在 Chrome 中,消息传递 API 使用 JSON 序列化。值得注意的是,这与其他使用结构化克隆算法实现相同 API 的浏览器不同。

这意味着消息(以及接收者提供的回答)可以包含任何有效的 JSON.stringify() 值。其他值将被强制转换为可序列化的值(尤其是 undefined 将序列化为 null);

邮件大小限制

消息的最大大小为 64 MiB。

端口生命周期

端口旨在作为扩展程序不同部分之间的双向通信机制。当扩展程序的一部分调用 tabs.connect()runtime.connect()runtime.connectNative() 时,它会创建一个 Port,该 Port 可以立即使用 postMessage() 发送消息。

如果某个标签页中有多个框架,则调用 tabs.connect() 会针对该标签页中的每个框架调用一次 runtime.onConnect 事件。同样,如果调用 runtime.connect(),则 onConnect 事件可以在扩展程序进程中针对每个帧触发一次。

您可能需要了解连接何时关闭,例如,如果您要为每个打开的端口维护单独的状态。为此,请监听 runtime.Port.onDisconnect 事件。当渠道另一端没有有效端口时,系统会触发此事件,原因可能是以下任一情况:

  • 另一端没有 runtime.onConnect 的监听器。
  • 包含该端口的标签页已卸载(例如,如果该标签页被导航)。
  • 调用 connect() 的帧已卸载。
  • 接收到端口的所有帧(通过 runtime.onConnect)都已卸载。
  • runtime.Port.disconnect()另一端调用。如果 connect() 调用在接收端产生多个端口,并且在其中任何一个端口上调用了 disconnect(),则 onDisconnect 事件仅在发送端口触发,而不会在其他端口触发。

跨扩展程序的消息传递

除了在扩展程序中的不同组件之间发送消息之外,您还可以使用 Messaging API 与其他扩展程序进行通信。这样一来,您就可以公开一个 API,供其他扩展程序使用。

如需侦听来自其他扩展程序的传入请求和连接,请使用 runtime.onMessageExternalruntime.onConnectExternal 方法。以下是每种类型的示例:

service-worker.js

// For a single request:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id !== allowlistedExtension) {
      return; // don't allow this extension access
    }
    if (request.getTargetData) {
      sendResponse({ targetData: targetData });
    } else if (request.activateLasers) {
      const success = activateLasers();
      sendResponse({ activateLasers: success });
    }
  }
);

// For long-lived connections:
chrome.runtime.onConnectExternal.addListener(function(port) {
  port.onMessage.addListener(function(msg) {
    // See other examples for sample onMessage handlers.
  });
});

如需向其他扩展程序发送消息,请传递要与之通信的扩展程序的 ID,如下所示:

service-worker.js

// The ID of the extension we want to talk to.
const laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// For a minimal request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
  }
);

// For a long-lived connection:
const port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

通过网页发送消息

扩展程序还可以接收和回复来自网页的消息。如需从网页向扩展程序发送消息,请在 manifest.json 中使用 "externally_connectable" 清单键指定您要允许哪些网站发送消息。例如:

manifest.json

"externally_connectable": {
  "matches": ["https://*.example.com/*"]
}

这会将消息传递 API 公开给与您指定的网址模式匹配的任何网页。网址模式必须至少包含一个二级网域;也就是说,不支持“*”“*.com”“*.co.uk”和“*.appspot.com”等主机名模式。您可以使用 <all_urls> 访问所有网域。

使用 runtime.sendMessage()runtime.connect() API 向特定扩展程序发送消息。例如:

webpage.js

// The ID of the extension we want to talk to.
const editorExtensionId = 'abcdefghijklmnoabcdefhijklmnoabc';

// Check if extension is installed
if (chrome && chrome.runtime) {
  // Make a request:
  chrome.runtime.sendMessage(
    editorExtensionId,
    {
      openUrlInEditor: url
    },
    (response) => {
      if (!response.success) handleError(url);
    }
  );
}

在扩展程序中,使用 runtime.onMessageExternalruntime.onConnectExternal API 监听来自网页的消息,如跨扩展程序的消息传递中所述。示例如下:

service-worker.js

chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.url === blocklistedWebsite)
      return;  // don't allow this web page access
    if (request.openUrlInEditor)
      openUrl(request.openUrlInEditor);
  });

无法从扩展程序 向网页 发送消息。

原生消息传递

扩展程序可以与注册为原生消息传递主机的原生应用交换消息。如需详细了解此功能,请参阅原生消息传递

安全注意事项

以下是与消息传递相关的一些安全注意事项。

内容脚本的可信度较低

与扩展程序服务工作线程相比,内容脚本的可信度较低。例如,恶意网页可能能够入侵运行内容脚本的渲染进程。假设来自内容脚本的消息可能由攻击者精心制作,并确保验证和清理所有输入。假设发送到内容脚本的任何数据都可能会泄露到网页。 限制可由从内容脚本接收的消息触发的特权操作的范围。

跨站点脚本攻击

请务必保护您的脚本免受跨站脚本攻击。从不可信的来源(例如用户输入、通过内容脚本访问的其他网站或 API)接收数据时,请注意避免将这些数据解读为 HTML 或以可能导致意外代码运行的方式使用这些数据。

更安全的方法

尽可能使用不运行脚本的 API:

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // JSON.parse doesn't evaluate the attacker's scripts.
  const resp = JSON.parse(response.farewell);
});

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // innerText does not let the attacker inject HTML elements.
  document.getElementById("resp").innerText = response.farewell;
});
不安全的方法

避免使用以下会使扩展程序容易受到攻击的方法:

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be evaluating a malicious script!
  const resp = eval(`(${response.farewell})`);
});

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be injecting a malicious script!
  document.getElementById("resp").innerHTML = response.farewell;
});