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

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

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

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

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

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

// Event listener
function handleMessages(message, sender, sendResponse) {

  fetch(message.url)
    .then((response) => sendResponse({statusCode: response.status}))

  // Since `fetch` is asynchronous, must send an explicit `true`
  return true;
}

// Message sender
  const {statusCode} = await chrome.runtime.sendMessage({
    url: 'https://example.com'
  });

ดูข้อมูลเกี่ยวกับการแปลงการเรียกกลับเป็นสัญญาและการใช้สัญญาในส่วนขยายได้ที่คู่มือการย้ายข้อมูล 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);
})();

หากต้องการตอบกลับข้อความแบบซิงค์กัน ให้เรียกใช้ sendResponse เมื่อคุณมีคำตอบแล้ว และส่งกลับ false เพื่อระบุว่าดำเนินการเสร็จแล้ว หากต้องการตอบกลับแบบไม่พร้อมกัน ให้ส่งกลับ true เพื่อเก็บการเรียกกลับ sendResponse ไว้จนกว่าคุณจะพร้อมใช้งาน ระบบไม่รองรับฟังก์ชัน Async เนื่องจากจะแสดงผล Promise ซึ่งระบบไม่รองรับ

หากต้องการส่งคําขอไปยังสคริปต์เนื้อหา ให้ระบุแท็บที่จะใช้คําขอ ดังที่แสดงต่อไปนี้ ตัวอย่างนี้ใช้ได้กับ 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()

หากต้องการจัดการการเชื่อมต่อขาเข้าสำหรับสคริปต์เนื้อหาหรือหน้าส่วนขยาย ให้ตั้งค่า runtime.onConnect Listener เหตุการณ์ เมื่อส่วนอื่นของชิ้นงานเรียก 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() ระบบจะสร้าง Port ที่ส่งข้อความได้ทันทีโดยใช้ postMessage()

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

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

  • ไม่มีผู้ฟัง 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 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:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

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

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

manifest.json

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

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

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

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

การรับส่งข้อความในเครื่อง

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

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

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

สคริปต์เนื้อหาไม่น่าเชื่อถือ

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

Cross-site Scripting

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