การส่งข้อความ

Messaging API ช่วยให้คุณสื่อสารระหว่างสคริปต์ต่างๆ ที่ทำงานใน บริบทที่เชื่อมโยงกับส่วนขยายได้ ซึ่งรวมถึงการสื่อสารระหว่าง Service Worker, chrome-extension://pages และ Content Script ตัวอย่างเช่น ส่วนขยายโปรแกรมอ่าน RSS อาจใช้ Content Script เพื่อตรวจหาฟีด RSS ในหน้าเว็บ จากนั้นแจ้ง Service Worker ให้อัปเดตไอคอนการดำเนินการสำหรับหน้าเว็บนั้น

API การส่งผ่านข้อความมี 2 แบบ ได้แก่ แบบสำหรับคำขอแบบครั้งเดียว และแบบที่ซับซ้อนกว่าสำหรับการเชื่อมต่อที่ใช้งานได้นานซึ่งอนุญาตให้ส่งข้อความได้หลายรายการ

ดูข้อมูลเกี่ยวกับการส่งข้อความระหว่างส่วนขยายได้ที่ส่วนข้อความข้ามส่วนขยาย

คำขอแบบครั้งเดียว

หากต้องการส่งข้อความเดียวไปยังส่วนอื่นของส่วนขยาย และรับการตอบกลับหรือไม่ก็ได้ ให้เรียกใช้ runtime.sendMessage() หรือ tabs.sendMessage() วิธีการเหล่านี้ช่วยให้คุณส่งข้อความที่แปลงเป็น JSON ได้แบบครั้งเดียวจาก Content Script ไปยัง ส่วนขยาย หรือจากส่วนขยายไปยัง Content Script ทั้ง 2 API จะแสดงผล Promise ซึ่งจะเปลี่ยนเป็นการตอบกลับที่ผู้รับให้ไว้

การส่งคำขอจาก Content Script จะมีลักษณะดังนี้

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) {
  fetch(message.url)
    .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({
  url: 'https://example.com'
});

เมื่อมีการเรียก Listener เหตุการณ์ ระบบจะส่งsendResponseฟังก์ชันเป็นพารามิเตอร์ที่สาม นี่คือฟังก์ชันที่เรียกใช้เพื่อแสดงคำตอบได้ โดย ค่าเริ่มต้น ระบบจะเรียกใช้การเรียกกลับ sendResponse แบบพร้อมกัน หากต้องการ ทำงานแบบไม่พร้อมกันเพื่อรับค่าที่ส่งไปยัง sendResponse คุณต้อง ส่งคืนค่าตามตัวอักษร true (ไม่ใช่แค่ค่าที่เป็นจริง) จากเครื่องมือฟังเหตุการณ์ การทำเช่นนี้จะทำให้ช่องข้อความเปิดอยู่กับปลายทางอีกด้านจนกว่าจะมีการเรียกใช้ sendResponse

หากคุณเรียกใช้ sendResponse โดยไม่มีพารามิเตอร์ ระบบจะส่ง null เป็นการตอบกลับ

หากหลายหน้าเว็บกำลังรอรับฟังเหตุการณ์ onMessage จะมีเพียงหน้าเว็บแรกที่เรียกใช้ sendResponse() สำหรับ เหตุการณ์หนึ่งๆ เท่านั้นที่จะส่งการตอบกลับได้สำเร็จ ระบบจะเพิกเฉยต่อการตอบกลับอื่นๆ ทั้งหมดสำหรับกิจกรรมนั้น

การเชื่อมต่อที่ใช้งานได้นาน

หากต้องการสร้างช่องการส่งข้อความแบบมีอายุยาวนานที่ใช้ซ้ำได้ ให้เรียกใช้

  • runtime.connect() เพื่อส่งข้อความจากสคริปต์เนื้อหา ไปยังหน้าส่วนขยาย
  • tabs.connect() เพื่อส่งข้อความจากหน้าส่วนขยายไปยัง Content Script

คุณตั้งชื่อช่องได้โดยส่งพารามิเตอร์ options ที่มีคีย์ name เพื่อ แยกความแตกต่างระหว่างการเชื่อมต่อประเภทต่างๆ ดังนี้

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

กรณีการใช้งานที่เป็นไปได้ของการเชื่อมต่อที่มีอายุการใช้งานยาวนานคือส่วนขยายการกรอกแบบฟอร์มอัตโนมัติ Content Script อาจเปิดช่องทางไปยังหน้าส่วนขยายสำหรับการเข้าสู่ระบบที่เฉพาะเจาะจง และ ส่งข้อความไปยังส่วนขยายสำหรับองค์ประกอบอินพุตแต่ละรายการในหน้าเว็บเพื่อขอข้อมูล แบบฟอร์มที่จะกรอก การเชื่อมต่อที่แชร์ช่วยให้ส่วนขยายแชร์สถานะระหว่างคอมโพเนนต์ของส่วนขยายได้

เมื่อสร้างการเชื่อมต่อ ระบบจะกำหนดออบเจ็กต์ runtime.Port ให้แต่ละปลายทางเพื่อ ส่งและรับข้อความผ่านการเชื่อมต่อนั้น

ใช้โค้ดต่อไปนี้เพื่อเปิดแชแนลจาก Content Script รวมถึงส่งและรับฟังข้อความ

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

หากต้องการส่งคำขอจากส่วนขยายไปยัง Content Script ให้แทนที่การเรียกใช้ runtime.connect() ในตัวอย่างก่อนหน้าด้วย tabs.connect()

หากต้องการจัดการการเชื่อมต่อขาเข้าสำหรับ Content Script หรือหน้าส่วนขยาย ให้ตั้งค่า Listener เหตุการณ์ 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 ซึ่งหมายความว่าข้อความ (และการตอบกลับที่ผู้รับระบุ) สามารถมีค่า JSON ที่ถูกต้อง (null บูลีน ตัวเลข สตริง อาร์เรย์ หรือออบเจ็กต์) ระบบจะบังคับให้ค่าอื่นๆ เป็นค่าที่ทำให้เป็นอนุกรมได้

โปรดทราบว่าการดำเนินการนี้แตกต่างจากเบราว์เซอร์อื่นๆ ที่ใช้ API เดียวกันกับอัลกอริทึมการโคลนที่มีโครงสร้าง

อายุการใช้งานของพอร์ต

พอร์ตได้รับการออกแบบให้เป็นกลไกการสื่อสารแบบ 2 ทางระหว่างส่วนต่างๆ ของส่วนขยาย เมื่อส่วนหนึ่งของส่วนขยายเรียกใช้ tabs.connect(), runtime.connect() หรือ runtime.connectNative() ระบบจะสร้าง พอร์ตที่ส่งข้อความได้ทันทีโดยใช้ postMessage()

หากมีหลายเฟรมในแท็บ การเรียก tabs.connect() จะเรียกใช้เหตุการณ์ runtime.onConnect 1 ครั้งสำหรับแต่ละเฟรมในแท็บ ในทํานองเดียวกัน หากมีการเรียกใช้ runtime.connect() เหตุการณ์ onConnect จะทํางานได้ 1 ครั้งต่อเฟรมในกระบวนการของส่วนขยาย

คุณอาจต้องการทราบเมื่อการเชื่อมต่อปิดลง เช่น หากคุณรักษาสถานะแยกต่างหากสำหรับแต่ละพอร์ตที่เปิดอยู่ โดยให้ฟังเหตุการณ์ runtime.Port.onDisconnect เหตุการณ์นี้จะทริกเกอร์เมื่อไม่มีพอร์ตที่ถูกต้องที่ปลายอีกด้านของแชแนล ซึ่งอาจเกิดจากสาเหตุต่อไปนี้

  • ไม่มีผู้ฟังสำหรับ runtime.onConnect ที่ปลายสาย
  • ระบบจะยกเลิกการโหลดแท็บที่มีพอร์ต (เช่น หากมีการไปยังส่วนต่างๆ ในแท็บ)
  • เฟรมที่เรียกใช้ connect() ได้เลิกโหลดแล้ว
  • เฟรมทั้งหมดที่ได้รับพอร์ต (ผ่าน runtime.onConnect) ได้ยกเลิกการโหลดแล้ว
  • runtime.Port.disconnect() จะถูกเรียกใช้โดยปลายทางอีกฝั่ง หากการโทรผ่าน connect() ทำให้มีการโอนหลายครั้งที่ฝั่งผู้รับ และมีการโทรหา disconnect() ในพอร์ตใดพอร์ตหนึ่งเหล่านี้ เหตุการณ์ onDisconnect จะทริกเกอร์เฉพาะที่พอร์ตส่งเท่านั้น ไม่ใช่ที่พอร์ตอื่นๆ

การรับส่งข้อความข้ามส่วนขยาย

นอกเหนือจากการส่งข้อความระหว่างคอมโพเนนต์ต่างๆ ในส่วนขยายแล้ว คุณยังใช้ Messaging API เพื่อสื่อสารกับส่วนขยายอื่นๆ ได้ด้วย ซึ่งจะช่วยให้คุณเปิดเผย API สาธารณะเพื่อให้ส่วนขยายอื่นๆ ใช้ได้

หากต้องการฟังคำขอและการเชื่อมต่อขาเข้าจากส่วนขยายอื่นๆ ให้ใช้วิธี runtime.onMessageExternal หรือ runtime.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.
  });
});

หากต้องการส่งข้อความไปยังส่วนขยายอื่น ให้ส่งรหัสของส่วนขยายที่ต้องการสื่อสารด้วยดังนี้

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 ว่าต้องการอนุญาตให้เว็บไซต์ใดส่งข้อความโดยใช้คีย์ไฟล์ Manifest "externally_connectable" เช่น

manifest.json

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

ซึ่งจะเปิดเผย API การรับส่งข้อความไปยังหน้าเว็บใดก็ตามที่ตรงกับรูปแบบ URL ที่คุณระบุ รูปแบบ URL ต้องมีโดเมนระดับที่ 2 อย่างน้อย 1 รายการ นั่นคือระบบไม่รองรับรูปแบบชื่อโฮสต์ เช่น "*" "*.com" "*.co.uk" และ "*.appspot.com" คุณใช้ <all_urls> เพื่อเข้าถึงโดเมนทั้งหมดได้

ใช้ API runtime.sendMessage() หรือ runtime.connect() เพื่อส่ง ข้อความไปยังส่วนขยายที่เฉพาะเจาะจง เช่น

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

จากส่วนขยาย ให้รับฟังข้อความจากหน้าเว็บโดยใช้ API runtime.onMessageExternal หรือ runtime.onConnectExternal ตามที่ระบุไว้ในการรับส่งข้อความระหว่างส่วนขยาย เช่น

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

คุณไม่สามารถส่งข้อความจากส่วนขยาย ไปยังหน้าเว็บได้

การรับส่งข้อความแบบเดิม

ส่วนขยายแลกเปลี่ยนข้อความกับแอปพลิเคชันเนทีฟที่ลงทะเบียนเป็นโฮสต์การรับส่งข้อความเนทีฟได้ ดูข้อมูลเพิ่มเติมเกี่ยวกับฟีเจอร์นี้ได้ที่การรับส่งข้อความเนทีฟ

ข้อควรพิจารณาด้านความปลอดภัย

ข้อควรพิจารณาด้านความปลอดภัยบางประการที่เกี่ยวข้องกับการรับส่งข้อความมีดังนี้

สคริปต์เนื้อหาน่าเชื่อถือน้อยกว่า

สคริปต์เนื้อหาน่าเชื่อถือน้อยกว่า Service Worker ของส่วนขยาย ตัวอย่างเช่น หน้าเว็บที่เป็นอันตรายอาจทำให้กระบวนการแสดงผลที่เรียกใช้สคริปต์เนื้อหาถูกบุกรุก พึงระลึกว่าข้อความจาก Content Script อาจถูกสร้างขึ้นโดยผู้โจมตี และอย่าลืมตรวจสอบและล้างข้อมูลอินพุตทั้งหมด พึงระลึกว่าข้อมูลใดก็ตามที่ส่งไปยัง Content Script อาจรั่วไหลไปยังหน้าเว็บ จำกัดขอบเขตของการดำเนินการที่มีสิทธิ์ซึ่งทริกเกอร์ได้โดยข้อความที่ได้รับจาก Content Scripts

Cross-site Scripting

อย่าลืมปกป้องสคริปต์จากการเขียนสคริปต์ข้ามเว็บไซต์ เมื่อได้รับข้อมูลจากแหล่งที่ไม่น่าเชื่อถือ เช่น ข้อมูลที่ผู้ใช้ป้อน เว็บไซต์อื่นๆ ผ่าน Content Script หรือ 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;
});