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 tiện ích chạy các tập lệnh đó, nên các tập lệnh này thường cần có cách thức để giao tiếp với phần còn lại của tiện ích. Ví dụ: một 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 worker dịch vụ để hiển thị biểu tượng hành động cho trang đó.

Quá trình giao tiếp này sử dụng tính năng truyền tin nhắn, cho phép cả tiện ích và tập lệnh nội dung lắng 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 (rỗng, boolean, số, chuỗi, mảng hoặc đối tượng). Có hai API truyền tin nhắn: một API cho yêu cầu một lần và một API phức tạp hơn cho kết nối dài hạn cho phép gửi nhiều tin nhắn. Để biết thông tin về cách gửi thông báo giữa các tiện ích, hãy xem phần thông báo giữa các 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 của tiện ích và tuỳ ý nhận phản hồi, 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 một thông báo JSON tuần tự một lần từ tập lệnh nội dung đến tiện ích hoặc từ tiện ích đến 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ể truyền lệnh gọi lại làm đối số cuối cùng. Bạn không thể sử dụng một lời hứa và một lệnh gọi lại trong cùng một lệnh gọi.

Khi bạn gửi một thông báo, trình nghe sự kiện xử lý thông báo đó sẽ được truyền một đối số thứ ba không bắt buộc là sendResponse. Đây là một hàm lấy một đối tượng có thể chuyển đổi tuần tự JSON được dùng làm giá trị trả về cho hàm đã gửi thông báo. Theo mặc định, lệnh gọi lại sendResponse phải được gọi đồng bộ. Nếu muốn thực hiện công việc không đồng bộ để truyền giá trị đến sendResponse, bạn phải trả về một giá trị cố định true (không chỉ là giá trị đúng) từ trình nghe sự kiện. Việc này sẽ giữ cho kênh thông báo mở cho đầu kia cho đến khi sendResponse được gọi.

// Event listener
function handleMessages(message, sender, sendResponse) {

  fetch(message.url)
    .then((response) => sendResponse({statusCode: response.status}))

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

// Message sender
  const {statusCode} = await chrome.runtime.sendMessage({
    url: 'https://example.com'
  });

Để 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 các lệnh gọi lại đó trong tiện ích, hãy xem hướng dẫn di chuyển sang Manifest V3.

Cách gửi yêu cầu từ tập lệnh nội dung 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);
})();

Nếu muốn phản hồi đồng bộ một tin nhắn, bạn chỉ cần gọi sendResponse sau khi có phản hồi và trả về false để cho biết đã hoàn tất. Để phản hồi không đồng bộ, hãy trả về true để duy trì lệnh gọi lại sendResponse cho đến khi bạn sẵn sàng sử dụng lệnh gọi lại đó. Các hàm không đồng bộ không được hỗ trợ vì các hàm này trả về một Lời hứa không được hỗ trợ.

Để 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 áp dụng như sau. Ví dụ này hoạt động trong 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 lệnh 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 được 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 tồn tại lâu dài

Để tạo một kênh truyền tin dài hạn có thể sử dụng lại, hãy gọi runtime.connect() để truyền tin nhắn từ tập lệnh nội dung đến trang tiện ích hoặc tabs.connect() để truyền tin nhắn từ trang tiện ích đến 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 mối kết nối.

Một trường hợp sử dụng tiềm năng của 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ượt đăng nhập cụ thể và gửi thông báo đến tiện ích cho từng phần tử đầu vào trên trang để yêu cầu dữ liệu biểu mẫu điền vào. 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 tiện ích.

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

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

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 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(), phần đó sẽ kích hoạt sự kiện này và đối tượng runtime.Port. 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ế dưới dạng phương thức giao tiếp hai chiều giữa các phần của tiện ích. Khung cấp cao nhất là phần nhỏ nhất của một tiện ích có thể sử dụng cổng. Khi một phần của tiện ích gọi tabs.connect(), runtime.connect() hoặc runtime.connectNative(), phần đó 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ẻ, 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, thì sự kiện onConnect có thể kích hoạt một lần cho mỗi khung trong quá trình mở rộng.

Bạn có thể muốn tìm hiểu thời điểm một kết nối bị đóng, chẳng hạn như nếu 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ể là do một trong những nguyên nhân sau:

  • Không có trình nghe nào cho runtime.onConnect ở đầu bên kia.
  • Thẻ chứa cổng sẽ bị huỷ tải (ví dụ: nếu thẻ được điều hướng).
  • Khung nơi connect() được gọi đã được tải xuống.
  • Tất cả các khung đã nhận cổng (thông qua runtime.onConnect) đều đã tải xuống.
  • runtime.Port.disconnect() được đầu bên kia gọi. Nếu lệnh gọi connect() dẫn đến nhiều cổng ở đầu máy 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 phải ở các cổng khác.

Nhắn tin giữa các 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, bạn có thể sử dụng API nhắn tin để giao tiếp với các tiện ích khác. Thao tác này cho phép bạn hiển thị một API công khai để các tiện ích khác sử dụng.

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

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 truyền mã nhận dạng 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 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:
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à phản hồ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 trong manifest.json những trang web mà bạn muốn giao tiếp 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 mà 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ể sử dụng <all_urls> để truy cập vào tất cả các miền. Xin lưu ý rằng vì ảnh hưởng đến tất cả máy chủ lưu trữ, nên quá trình xem xét của Cửa hàng Chrome trực tuyến đối với các tiện ích sử dụng tính năng này 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.
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);
    }
  );
}

Từ tiện ích, hãy nghe thông báo từ các trang web bằng cách sử dụng API runtime.onMessageExternal hoặc runtime.onConnectExternal như trong phần nhắn tin giữa các 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 bằng ứng dụng gốc

Tiện ích có thể trao đổi thông báo 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 phần Tin nhắn gốc.

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

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

Tập lệnh nội dung ít đáng tin cậy hơn

Tập lệnh nội dung kém tin cậy hơn so với worker dịch vụ tiện ích. Ví dụ: một trang web độc hại có thể xâm phạm quy trình hiển thị chạy các tập lệnh nội dung. Giả sử rằng kẻ tấn công có thể đã tạo thông báo từ tập lệnh nội dung và nhớ xác thực và dọn dẹp tất cả dữ liệu đầu vào. Giả sử mọi dữ liệu được gửi đến tập lệnh nội dung đều có thể bị rò rỉ sang trang web. Giới hạn phạm vi của các hành động đặ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

Hãy nhớ 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ư dữ liệu đầ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 này theo cách có thể cho phép mã không mong muốn chạy.

Các phương thức 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 đây 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;
});