Chuyển tin nhắn

Vì tập lệnh nội dung chạy trong ngữ cảnh của một trang web, chứ không phải là tiện ích chạy các tập lệnh đó, nên chúng thường cần các cách để giao tiếp với phần còn lại của tiện ích. Ví dụ: tiện ích trình đọc RSS có thể sử dụng tập lệnh nội dung để phát hiện sự hiện diện của nguồn cấp dữ liệu RSS trên một trang, sau đó thông báo cho trình chạy dịch vụ để hiển thị một biểu tượng hành động cho trang đó.

Hoạt động giao tiếp này sử dụng tính năng truyền thông báo, cho phép cả tiện ích và tập lệnh nội dung nghe tin nhắn của nhau và phản hồi trên cùng một kênh. Một thông báo có thể chứa bất kỳ đối tượng JSON hợp lệ nào (null, boolean, số, chuỗi, mảng hoặc đối tượng). Có 2 API truyền thông báo: một API dành cho các yêu cầu một lần và một API phức tạp hơn dành cho kết nối dài hạn cho phép gửi nhiều thông báo. Để biết thông tin về cách gửi thư giữa các tiện ích, hãy xem phần thông báo trên nhiều tiện ích.

Yêu cầu một lần

Để gửi một tin nhắn đến một phần khác trong tiện ích và nhận phản hồi (không bắt buộc), hãy gọi runtime.sendMessage() hoặc tabs.sendMessage(). Các phương thức này cho phép bạn gửi thông báo chuyển đổi tuần tự JSON dùng một lần từ một tập lệnh nội dung đến tiện ích hoặc từ tiện ích đến một tập lệnh nội dung. Để xử lý phản hồi, hãy sử dụng lời hứa được trả về. Để tương thích ngược với các tiện ích cũ, bạn có thể chuyển một lệnh gọi lại làm đối số cuối cùng. Bạn không thể sử dụng lời hứa và lệnh gọi lại trong cùng một lệnh gọi.

Để biết thông tin về cách chuyển đổi lệnh gọi lại thành lời hứa và cách sử dụng lệnh gọi lại đó trong tiện ích, hãy xem hướng dẫn di chuyển Manifest V3.

Việc gửi yêu cầu từ một tập lệnh nội dung sẽ có dạng như sau:

content-script.js:

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

Để gửi yêu cầu đến một tập lệnh nội dung, hãy chỉ định thẻ mà yêu cầu sẽ áp dụng như minh hoạ bên dưới. Ví dụ này hoạt động trong các trình chạy dịch vụ, cửa sổ bật lên và các trang chrome-extension:// được mở dưới dạng thẻ.

(async () => {
  const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
  const response = await chrome.tabs.sendMessage(tab.id, {greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

Để nhận thông báo, hãy thiết lập trình nghe sự kiện runtime.onMessage. Các tệp này sử dụng cùng một mã trong cả tiện ích và tập lệnh nội dung:

content-script.js hoặc service-worker.js:

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

Trong ví dụ trước, sendResponse() được gọi đồng bộ. Để sử dụng sendResponse() một cách không đồng bộ, hãy thêm return true; vào trình xử lý sự kiện onMessage.

Nếu nhiều trang đang theo dõi các sự kiện onMessage, thì chỉ trang đầu tiên gọi sendResponse() cho một sự kiện cụ thể mới gửi thành công phản hồi. Tất cả các phản hồi khác cho sự kiện đó sẽ bị bỏ qua.

Kết nối lâu dài

Để tạo kênh truyền thông báo dài hạn có thể sử dụng lại, hãy gọi runtime.connect() để truyền thông báo từ tập lệnh nội dung đến trang tiện ích hoặc tabs.connect() để truyền thông báo từ trang tiện ích sang tập lệnh nội dung. Bạn có thể đặt tên cho kênh để phân biệt giữa các loại kết nối.

Một trường hợp sử dụng tiềm năng cho kết nối lâu dài là tiện ích tự động điền biểu mẫu. Tập lệnh nội dung có thể mở một kênh đến trang tiện ích cho một lần đăng nhập cụ thể và gửi thông báo tới tiện ích cho từng thành phần đầu vào trên trang để yêu cầu dữ liệu biểu mẫu cần điền. Kết nối dùng chung cho phép tiện ích chia sẻ trạng thái giữa các thành phần của tiện ích.

Khi thiết lập kết nối, mỗi đầu được gán một đối tượng runtime.Port để gửi và nhận thông báo qua kết nối đó.

Sử dụng mã sau để mở một kênh từ một tập lệnh nội dung, đồng thời gửi và nghe thông báo:

content-script.js:

var port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
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"});
});

Để gửi yêu cầu từ tiện ích đến một tập lệnh nội dung, hãy thay thế lệnh gọi đến runtime.connect() trong ví dụ trước bằng tabs.connect().

Để xử lý các kết nối đến cho tập lệnh nội dung hoặc trang tiện ích, hãy thiết lập trình nghe sự kiện runtime.onConnect. Khi một phần khác của tiện ích gọi connect(), sự kiện này và đối tượng runtime.Port sẽ kích hoạt. Mã để phản hồi các kết nối đến sẽ có dạng như sau:

service-worker.js:

chrome.runtime.onConnect.addListener(function(port) {
  console.assert(port.name === "knockknock");
  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."});
  });
});

Thời gian tồn tại của cổng

Cổng được thiết kế như phương thức liên lạc hai chiều giữa các phần khác nhau của tiện ích. Khung cấp cao nhất là phần nhỏ nhất của tiện ích có thể sử dụng cổng. Khi một tiện ích gọi tabs.connect(), runtime.connect() hoặc runtime.connectNative(), tiện ích sẽ tạo một Cổng có thể gửi tin nhắn ngay lập tức bằng postMessage().

Nếu có nhiều khung trong một thẻ, việc gọi tabs.connect() sẽ gọi sự kiện runtime.onConnect một lần cho mỗi khung trong thẻ. Tương tự, nếu runtime.connect() được gọi, sự kiện onConnect có thể kích hoạt một lần cho mỗi khung hình trong quá trình tiện ích.

Bạn nên tìm hiểu thời điểm kết nối bị đóng, chẳng hạn như bạn đang duy trì các trạng thái riêng biệt cho mỗi cổng đang mở. Để thực hiện việc này, hãy theo dõi sự kiện runtime.Port.onDisconnect. Sự kiện này sẽ kích hoạt khi không có cổng hợp lệ ở đầu bên kia của kênh. Điều này có thể do bất kỳ nguyên nhân nào sau đây:

  • Không có trình nghe cho runtime.onConnect ở đầu bên kia.
  • Thẻ chứa cổng đã bị huỷ tải (ví dụ: nếu thẻ đang được điều hướng).
  • Khung nơi connect() được gọi đã bị huỷ tải.
  • Tất cả khung hình nhận được cổng (thông qua runtime.onConnect) đều đã huỷ tải.
  • runtime.Port.disconnect() được gọi bằng đầu bên kia. Nếu lệnh gọi connect() dẫn đến nhiều cổng ở phía nhận và disconnect() được gọi trên bất kỳ cổng nào trong số này, thì sự kiện onDisconnect chỉ kích hoạt ở cổng gửi, chứ không kích hoạt ở các cổng khác.

Nhắn tin trên nhiều tiện ích

Ngoài việc gửi thông báo giữa các thành phần trong tiện ích của mình, bạn có thể sử dụng API nhắn tin để giao tiếp với các tiện ích khác. Điều này cho phép bạn hiển thị API công khai cho các tiện ích khác sử dụng.

Để theo dõi các yêu cầu đến và kết nối từ các tiện ích khác, hãy sử dụng phương thức runtime.onMessageExternal hoặc runtime.onConnectExternal. Dưới đây là ví dụ về từng ví dụ:

service-worker.js

// For a single request:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id === blocklistedExtension)
      return;  // don't allow this extension access
    else if (request.getTargetData)
      sendResponse({targetData: targetData});
    else if (request.activateLasers) {
      var 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.
  });
});

Để gửi tin nhắn đến một tiện ích khác, hãy chuyển mã của tiện ích mà bạn muốn giao tiếp như sau:

service-worker.js

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

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

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

Gửi tin nhắn từ trang web

Tiện ích cũng có thể nhận và trả lời tin nhắn từ các trang web khác nhưng không thể gửi tin nhắn đến các trang web. Để gửi thông báo từ một trang web đến một tiện ích, hãy chỉ định trang web bạn muốn giao tiếp trong manifest.json bằng cách sử dụng khoá tệp kê khai "externally_connectable". Ví dụ:

manifest.json

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

Thao tác này sẽ hiển thị API nhắn tin cho mọi trang khớp với mẫu URL bạn chỉ định. Mẫu URL phải chứa ít nhất một miền cấp hai; tức là các mẫu tên máy chủ như "*", "*.com", "*.co.uk" và "*.appspot.com" không được hỗ trợ. Kể từ Chrome 107, bạn có thể dùng <all_urls> để truy cập vào tất cả các miền. Xin lưu ý rằng vì vấn đề này ảnh hưởng đến tất cả máy chủ lưu trữ nên quá trình xem xét trên Cửa hàng Chrome trực tuyến có thể mất nhiều thời gian hơn.

Sử dụng API runtime.sendMessage() hoặc runtime.connect() để gửi thông báo đến một ứng dụng hoặc tiện ích cụ thể. Ví dụ:

webpage.js

// The ID of the extension we want to talk to.
var editorExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// Make a simple request:
chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
  function(response) {
    if (!response.success)
      handleError(url);
  });

Từ tiện ích của bạn, hãy nghe thông báo trên các trang web bằng cách sử dụng API runtime.onMessageExternal hoặc runtime.onConnectExternal như trong tính năng nhắn tin trên nhiều tiện ích. Ví dụ:

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);
  });

Nhắn tin gốc

Các tiện ích có thể trao đổi tin nhắn với các ứng dụng gốc được đăng ký làm máy chủ nhắn tin gốc. Để tìm hiểu thêm về tính năng này, hãy xem bài viết Nhắn tin gốc.

Lưu ý về bảo mật

Dưới đây là một số điểm cần cân nhắc về bảo mật liên quan đến hoạt động nhắn tin.

Tập lệnh nội dung không đáng tin cậy

Tập lệnh nội dung không đáng tin cậy hơn trình chạy dịch vụ tiện ích. Ví dụ: một trang web độc hại có thể xâm phạm quá trình hiển thị đang chạy tập lệnh nội dung. Giả sử rằng các thông báo từ một tập lệnh nội dung có thể đã do kẻ tấn công tạo ra và hãy nhớ xác thực và dọn dẹp tất cả dữ liệu đầu vào. Giả định rằng bất kỳ dữ liệu nào được gửi đến tập lệnh nội dung có thể bị rò rỉ đến trang web. Giới hạn phạm vi các thao tác đặc quyền có thể được kích hoạt bằng các thông báo nhận được từ tập lệnh nội dung.

Tập lệnh trên nhiều trang web

Đảm bảo bảo vệ tập lệnh của bạn khỏi tập lệnh trên nhiều trang web. Khi nhận dữ liệu từ một nguồn không đáng tin cậy (chẳng hạn như hoạt động đầu vào của người dùng, các trang web khác thông qua tập lệnh nội dung hoặc API), hãy cẩn thận để tránh diễn giải dữ liệu này dưới dạng HTML hoặc sử dụng dữ liệu theo cách có thể khiến mã không mong muốn chạy.

Phương pháp an toàn hơn

Sử dụng các API không chạy tập lệnh bất cứ khi nào có thể:

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // JSON.parse doesn't evaluate the attacker's scripts.
  var 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;
});
Phương thức không an toàn

Tránh sử dụng các phương thức sau khiến tiện ích của bạn dễ bị tấn công:

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be evaluating a malicious script!
  var 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;
});