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

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

การสื่อสารระหว่างส่วนขยายกับสคริปต์เนื้อหาจะทำงานโดยใช้การส่งข้อความ ทั้ง 2 ฝ่ายสามารถรอฟังข้อความที่ส่งจากอีกฝ่าย และตอบกลับในช่องทางเดียวกันได้ ข้อความมีออบเจ็กต์ JSON ที่ถูกต้อง (null, บูลีน, ตัวเลข, สตริง, อาร์เรย์ หรือออบเจ็กต์) ได้ มี API แบบง่ายสำหรับคำขอแบบครั้งเดียว และ API ที่ซับซ้อนกว่าซึ่งช่วยให้คุณมีการเชื่อมต่อที่ใช้งานได้นานสำหรับการแลกเปลี่ยนข้อความหลายรายการที่มีบริบทที่ใช้ร่วมกัน นอกจากนี้ คุณยังส่งข้อความไปยังส่วนขยายอื่นได้หากทราบรหัสของส่วนขยายนั้น ซึ่งจะกล่าวถึงในส่วนข้อความข้ามส่วนขยาย

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

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

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

chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
  console.log(response.farewell);
});

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

chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
  chrome.tabs.sendMessage(tabs[0].id, {greeting: "hello"}, function(response) {
    console.log(response.farewell);
  });
});

ในฝั่งที่รับ คุณต้องตั้งค่า Listener เหตุการณ์ runtime.onMessage เพื่อจัดการข้อความ ซึ่งจะดูเหมือนกันจากสคริปต์เนื้อหาหรือหน้าส่วนขยาย

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

ในตัวอย่างด้านบน ระบบจะเรียกใช้ sendResponse แบบพร้อมกัน หากต้องการใช้ sendResponseแบบอะซิงโครนัส ให้เพิ่ม return true; ลงในเครื่องจัดการเหตุการณ์ onMessage

หมายเหตุ: หากมีหลายหน้าที่รอรับเหตุการณ์ onMessage เฉพาะหน้าที่เรียกใช้ sendResponse() เป็นหน้าแรกเท่านั้นที่จะส่งการตอบกลับได้สำเร็จ ระบบจะไม่สนใจการตอบกลับอื่นๆ ทั้งหมดต่อเหตุการณ์นั้น
หมายเหตุ: sendResponse Callback จะใช้ได้ก็ต่อเมื่อใช้แบบซิงโครนัส หรือหากตัวแฮนเดิลเหตุการณ์แสดงผล true เพื่อระบุว่าจะตอบกลับแบบไม่พร้อมกัน ระบบจะเรียกใช้ Callback ของฟังก์ชัน sendMessage โดยอัตโนมัติหากไม่มีแฮนเดิลใดแสดงผลเป็นจริง หรือหากมีการเก็บขยะใน Callback ของ sendResponse

การเชื่อมต่อที่มีอายุยาวนาน

บางครั้งการสนทนาที่ยาวนานกว่าคำขอและการตอบกลับครั้งเดียวก็มีประโยชน์ ในกรณีนี้ คุณสามารถเปิดแชแนลที่มีอายุการใช้งานยาวนานจากสคริปต์เนื้อหาไปยังหน้าส่วนขยาย หรือในทางกลับกันได้โดยใช้ runtime.connect หรือ tabs.connect ตามลำดับ ช่องอาจมีชื่อ (ไม่บังคับ) ซึ่งช่วยให้คุณแยกความแตกต่างระหว่างการเชื่อมต่อประเภทต่างๆ ได้

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

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

วิธีเปิดแชแนลจากสคริปต์เนื้อหา รวมถึงวิธีส่งและฟังข้อความมีดังนี้

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

การส่งคำขอจากส่วนขยายไปยัง Content Script จะมีลักษณะคล้ายกันมาก เพียงแต่คุณต้อง ระบุแท็บที่จะเชื่อมต่อ เพียงแทนที่การเรียกใช้เพื่อเชื่อมต่อในตัวอย่างด้านบนด้วย tabs.connect

หากต้องการจัดการการเชื่อมต่อขาเข้า คุณต้องตั้งค่า Listener เหตุการณ์ runtime.onConnect ซึ่งจะดูเหมือนกันจากสคริปต์เนื้อหาหรือหน้าส่วนขยาย เมื่อส่วนอื่นของส่วนขยายเรียกใช้ "connect()" ระบบจะทริกเกอร์เหตุการณ์นี้พร้อมกับออบเจ็กต์ runtime.Port ที่คุณใช้เพื่อส่งและรับข้อความผ่านการเชื่อมต่อได้ การตอบกลับการเชื่อมต่อขาเข้ามีลักษณะดังนี้

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

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

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

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

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

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

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

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

การรอรับคำขอและการเชื่อมต่อขาเข้าจะคล้ายกับกรณีภายใน ยกเว้นว่าคุณจะใช้วิธี runtime.onMessageExternal หรือ runtime.onConnectExternal ตัวอย่างของแต่ละรายการมีดังนี้

// For simple requests:
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.
  });
});

ในทำนองเดียวกัน การส่งข้อความไปยังส่วนขยายอื่นก็คล้ายกับการส่งข้อความภายในส่วนขยายของคุณ ความแตกต่างเพียงอย่างเดียวคือคุณต้องส่งรหัสของส่วนขยายที่ต้องการสื่อสารด้วย เช่น

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

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

// Start a long-running conversation:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

การส่งข้อความจากหน้าเว็บ

แอปหรือส่วนขยายจะรับและตอบกลับข้อความจากหน้าเว็บปกติได้เช่นเดียวกับการรับส่งข้อความข้ามส่วนขยาย หากต้องการใช้ฟีเจอร์นี้ คุณต้องระบุใน manifest.json ก่อน ว่าต้องการสื่อสารกับเว็บไซต์ใด เช่น

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

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

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

จากแอปหรือส่วนขยาย คุณอาจฟังข้อความจากหน้าเว็บผ่าน API runtime.onMessageExternal มีเพียงหน้าเว็บเท่านั้นที่เริ่มการเชื่อมต่อได้ มีตัวอย่างดังต่อไปนี้

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

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

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

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

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

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

Cross-site Scripting

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

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

แต่ให้ใช้ API ที่ปลอดภัยกว่าซึ่งไม่เรียกใช้สคริปต์แทน

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // JSON.parse does not evaluate the attacker's scripts.
  var resp = JSON.parse(response.farewell);
});
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // innerText does not let the attacker inject HTML elements.
  document.getElementById("resp").innerText = response.farewell;
});

ตัวอย่าง

คุณดูตัวอย่างการสื่อสารผ่านข้อความอย่างง่ายได้ในไดเรกทอรี examples/api/messaging ตัวอย่างการรับส่งข้อความดั้งเดิมแสดงให้เห็นว่าแอป Chrome สื่อสารกับแอปที่มาพร้อมเครื่องได้อย่างไร ดูตัวอย่างเพิ่มเติมและรับความช่วยเหลือในการดูซอร์สโค้ดได้ที่ตัวอย่าง