เนื่องจากสคริปต์เนื้อหาจะทำงานในบริบทของหน้าเว็บ ไม่ใช่ส่วนขยายที่เรียกใช้สคริปต์ บ่อยครั้งที่สคริปต์เหล่านี้จึงต้องมีวิธีสื่อสารกับส่วนขยายที่เหลือ ตัวอย่างเช่น ส่วนขยายโปรแกรมอ่าน RSS อาจใช้สคริปต์เนื้อหาเพื่อตรวจหาการมีอยู่ของฟีด RSS ในหน้าเว็บ แล้วแจ้งให้โปรแกรมทำงานของบริการแสดงไอคอนการดำเนินการสำหรับหน้านั้น
การสื่อสารนี้ใช้การส่งข้อความ ซึ่งช่วยให้ทั้งส่วนขยายและสคริปต์เนื้อหาสามารถรับฟังข้อความของกันและกันและตอบกลับในช่องทางเดียวกัน ข้อความอาจมีออบเจ็กต์ JSON ที่ถูกต้อง (ค่านัล บูลีน ตัวเลข สตริง ชุดลำดับ หรือออบเจ็กต์) API การส่งข้อความมี 2 รายการ ได้แก่ รายการสำหรับคำขอแบบครั้งเดียว และรายการที่ซับซ้อนกว่าสำหรับการเชื่อมต่อแบบคงที่ที่อนุญาตให้ส่งข้อความได้หลายรายการ ดูข้อมูลเกี่ยวกับการส่งข้อความระหว่างส่วนขยายได้ที่ส่วนข้อความส่วนขยายแบบข้ามแพลตฟอร์ม
คำขอแบบครั้งเดียว
หากต้องการส่งข้อความเดียวไปยังอีกส่วนของส่วนขยาย และเลือกที่จะรับคำตอบได้ โปรดโทร runtime.sendMessage()
หรือ tabs.sendMessage()
วิธีการเหล่านี้ช่วยให้คุณส่งข้อความ JSON แบบอนุกรมครั้งเดียวจากสคริปต์เนื้อหาไปยังส่วนขยาย หรือจากส่วนขยายไปยังสคริปต์เนื้อหาได้ ในการจัดการการตอบสนอง ให้ใช้
คำมั่นสัญญาที่ส่งคืน สำหรับความเข้ากันได้แบบย้อนหลังกับส่วนขยายที่เก่ากว่า คุณสามารถส่ง Callback เป็นอาร์กิวเมนต์สุดท้ายแทนได้ คุณไม่สามารถใช้สัญญาและการเรียกกลับในการโทรเดียวกันได้
ดูข้อมูลเกี่ยวกับการแปลง Callback เป็น Conversion และวิธีใช้ในส่วนขยายได้ในคำแนะนำในการย้ายข้อมูล 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
ไว้ใช้งานจนกว่าคุณจะพร้อมใช้งาน ระบบไม่รองรับฟังก์ชันแบบไม่พร้อมกันเนื่องจากฟังก์ชันดังกล่าวจะแสดง 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
จะทริกเกอร์ที่พอร์ตฝั่งส่งเท่านั้น ไม่ใช่ที่พอร์ตอื่นๆ
การรับส่งข้อความข้ามส่วนขยาย
นอกจากการส่งข้อความระหว่างคอมโพเนนต์ต่างๆ ในส่วนขยายแล้ว คุณยังใช้ 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 === 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/*"]
}
ซึ่งจะแสดง 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.
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; });