jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Build Chrome Extensions · Part 6 — Messaging Across Contexts

How isolated pieces talk: one-shot sendMessage, long-lived connect ports, the return-true async trap, broadcasting to tabs, and externally_connectable. With an interactive messaging playground.

Mọi ngữ cảnh đều cô lập, nên cách duy nhất để chúng hợp tác là truyền tin nhắn. Làm đúng messaging thì extension như một chương trình; làm sai thì bạn sẽ rượt lỗi “popup chạy nhưng content script không bao giờ phản hồi” hàng giờ.

Thử cả hai kiểu messaging bên dưới — bắn gói tin và đọc code mỗi kiểu sinh ra:


1. Tin nhắn một lần

Con ngựa kéo: gửi tin nhắn, nhận về một phản hồi. Từ popup, options, hay content script tới worker:

// sender (popup.js / content.js)
const response = await chrome.runtime.sendMessage({ action: "getData" });
console.log(response.items);

// receiver (background.js)
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.action === "getData") {
    sendResponse({ items: [1, 2, 3] });
  }
});

sender cho biết ai gửi — sender.tab được đặt khi đến từ content script, giúp bạn trả về đúng tab.


2. Bẫy async return true

Đây là lỗi messaging phổ biến nhất. Nếu listener phản hồi bất đồng bộ, bạn phải return true để giữ kênh mở:

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.action === "fetch") {
    fetch(msg.url)
      .then((r) => r.json())
      .then((data) => sendResponse(data)); // runs later
    return true; // ← without this, the channel closes and sendResponse is a no-op
  }
});

Không có return true, Chrome đóng kênh ngay khi listener return, và sendResponse sau đó âm thầm không làm gì. await của bên gửi sẽ resolve thành undefined.


3. Worker → content script

chrome.runtime.sendMessage tới worker; để tới content script của một tab cụ thể dùng chrome.tabs.sendMessage:

// background.js — send to the active tab
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const res = await chrome.tabs.sendMessage(tab.id, { action: "highlight", term: "todo" });

Nếu không có content script nào lắng nghe trong tab đó, lời gọi reject với “Could not establish connection” — bọc try/catch.


4. Kết nối lâu dài (port)

Với tin nhắn lặp lại hoặc streaming, mở kênh một-lần mỗi lần là lãng phí. Mở một port một lần và tái dùng:

// popup.js
const port = chrome.runtime.connect({ name: "live" });
port.postMessage({ type: "subscribe" });
port.onMessage.addListener((msg) => render(msg));

// background.js
chrome.runtime.onConnect.addListener((port) => {
  if (port.name !== "live") return;
  const id = setInterval(() => port.postMessage({ tick: Date.now() }), 1000);
  port.onDisconnect.addListener(() => clearInterval(id)); // clean up!
});

Port hoàn hảo cho dashboard trực tiếp, luồng tiến độ, hay bất kỳ qua-lại nào. Luôn xử lý onDisconnect để giải phóng tài nguyên.

Một port lâu dài giữ service worker sống khi còn mở. Hữu ích cho tác vụ đang chạy, nhưng đừng giữ port mở mãi chỉ để né vòng đời.


5. Phát sóng

Không có sẵn “gửi tới tất cả”. Để báo tất cả tab, truy vấn rồi lặp:

const tabs = await chrome.tabs.query({});
for (const tab of tabs) {
  chrome.tabs.sendMessage(tab.id, { action: "themeChanged" }).catch(() => {});
}

Thường mẫu sạch hơn là ghi vào chrome.storage và để mọi ngữ cảnh phản ứng qua chrome.storage.onChanged (Phần 7).


6. Nói chuyện với trang web & extension khác

Mặc định trang web không nhắn extension của bạn được. Bật bằng externally_connectable trong manifest, rồi trang dùng chrome.runtime.sendMessage(extensionId, msg):

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

Xử lý trên chrome.runtime.onMessageExternalxác thực origin của bên gửi — đây là bề mặt tấn công.


7. Vệ sinh tin nhắn

  • Dùng hình dạng { action, payload } nhất quán để listener switch gọn.
  • Tin nhắn được serialize JSON — không hàm, node DOM, hay instance class.
  • Luôn xử lý rejection khi không có bên nhận.
  • Xác thực input không tin cậy, nhất là từ content script và bên gửi ngoài.

8. Bài tập

1. sendMessage trong popup resolve thành undefined dù listener của worker gọi sendResponse sau một fetch. Sửa đi.

Lời giải

Thêm return true trong listener để kênh mở chờ sendResponse bất đồng bộ.

2. Bạn cần đẩy thanh tiến độ trực tiếp từ worker tới popup mỗi 500ms. One-shot hay port?

Lời giải

Một port — mở một lần, stream nhiều cập nhật postMessage, dọn ở onDisconnect.

3. chrome.tabs.sendMessage(tabId, ...) ném “Could not establish connection”. Nguyên nhân khả dĩ?

Lời giải

Không có content script nào tải/lắng nghe trong tab đó (sai URL match, hoặc trang chưa tiêm). Bọc try/catch và kiểm tra matches.

Nâng cao:trong playground, chuyển sang port mode, mở port, gửi vài gói, rồi disconnect — để ý không có bắt-tay-lại mỗi tin nhắn.


Điểm chính

  • sendMessage cho yêu cầu/phản hồi một lần; port cho lặp lại/streaming.
  • return true giữ kênh mở cho sendResponse bất đồng bộ.
  • Tới content script của tab bằng chrome.tabs.sendMessage.
  • Ưu tiên chrome.storage.onChanged hơn phát sóng thủ công cho state chung.
  • Xác thực tin nhắn bên ngoài — chúng là bề mặt tấn công.

Tiếp theo

Phần 7 — Storage: local vs sync vs session, hạn mức, sự kiện onChanged cho UI phản ứng, storage làm nguồn sự thật duy nhất, và di trú cấu trúc giữa các phiên bản.