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

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

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

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

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

โปรดดูข้อมูลเกี่ยวกับการแปลงโค้ดเรียกกลับให้เป็นคำสัญญาและการนำไปใช้ในส่วนขยายที่หัวข้อคำแนะนำในการย้ายข้อมูลไฟล์ Manifest V3

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

content-script.js:

(async () => {
  const response = await chrome.runtime.sendMessage({greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

หากต้องการส่งคำขอไปยังสคริปต์เนื้อหา ให้ระบุแท็บที่จะนำคำขอไปใช้ตามที่แสดงในข้อมูลต่อไปนี้ ตัวอย่างนี้ใช้งานได้ในหน้า Service Worker, ป๊อปอัป และ chrome-extension:// ที่เปิดเป็นแท็บ

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

หากต้องการรับข้อความ ให้ตั้งค่า Listener เหตุการณ์ runtime.onMessage เครื่องมือเหล่านี้ใช้โค้ดเดียวกันทั้งในส่วนขยายและสคริปต์เนื้อหา

content-script.js หรือ 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"});
  }
);

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

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

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

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

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

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

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

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

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

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

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

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

พอร์ตออกแบบมาเพื่อเป็นวิธีการสื่อสารแบบ 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 นี่คือตัวอย่างของแต่ละไซต์

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

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

service-worker.js

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

// For a simple 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(...);

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

ส่วนขยายยังสามารถรับและตอบกลับข้อความจากหน้าเว็บอื่นๆ ได้ แต่ไม่สามารถส่งข้อความถึงหน้าเว็บได้ หากต้องการส่งข้อความจากหน้าเว็บไปยังส่วนขยาย ให้ระบุเว็บไซต์ที่ต้องการสื่อสารด้วยใน manifest.json โดยใช้คีย์ไฟล์ Manifest "externally_connectable" เช่น

manifest.json

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

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

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

webpage.js

// 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 จากส่วนขยาย เช่นเดียวกับในการรับส่งข้อความข้ามส่วนขยาย ตัวอย่าง

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

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

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

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

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

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

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

Cross-site Scripting

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

วิธีที่ปลอดภัยยิ่งขึ้น

ใช้ API ที่ไม่เรียกใช้สคริปต์ทุกครั้งที่ทำได้ โดยทำดังนี้

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;
});
วิธีการที่ไม่ปลอดภัย

หลีกเลี่ยงการใช้วิธีต่อไปนี้ที่ทำให้ส่วนขยายมีความเสี่ยง

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