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

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

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

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

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

การส่งคำขอจากสคริปต์เนื้อหามีลักษณะดังนี้

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

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

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 จะใช้ได้เมื่อใช้แบบซิงโครนัสเท่านั้น หรือหากเครื่องจัดการเหตุการณ์แสดง true เพื่อบ่งชี้ว่าจะตอบสนองแบบไม่พร้อมกัน ระบบจะเรียกใช้โค้ดเรียกกลับของฟังก์ชัน sendMessage โดยอัตโนมัติหากตัวแฮนเดิลไม่มีผลลัพธ์ที่มีค่า "จริง" หรือหากมีการรวบรวมการเรียกกลับ sendResponse

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

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

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

เมื่อทำการเชื่อมต่อ แต่ละปลายทางจะได้รับออบเจ็กต์ 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"});
});

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

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

นอกจากการส่งข้อความระหว่างคอมโพเนนต์ต่างๆ ในส่วนขยายแล้ว คุณยังสามารถใช้ 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 เป็นอย่างน้อย กล่าวคือ รูปแบบชื่อโฮสต์ เช่น "*", "*.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 หรือ runtime.onConnectExternal ซึ่งคล้ายกับการรับส่งข้อความข้ามส่วนขยาย มีเพียงหน้าเว็บเท่านั้นที่เริ่มต้นการเชื่อมต่อได้ มีตัวอย่างดังต่อไปนี้

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

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

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

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

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

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

Cross-site Scripting

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