Messaging API ช่วยให้คุณสื่อสารระหว่างสคริปต์ต่างๆ ที่ทำงานใน บริบทที่เชื่อมโยงกับส่วนขยายได้ ซึ่งรวมถึงการสื่อสารระหว่าง Service Worker, chrome-extension://pages และ Content Script ตัวอย่างเช่น ส่วนขยายโปรแกรมอ่าน RSS อาจใช้ Content Script เพื่อตรวจหาฟีด RSS ในหน้าเว็บ จากนั้นแจ้ง Service Worker ให้อัปเดตไอคอนการดำเนินการสำหรับหน้าเว็บนั้น
API การส่งข้อความมี 2 แบบ ได้แก่ API สำหรับคำขอแบบครั้งเดียว และ API ที่ซับซ้อนกว่าสำหรับการเชื่อมต่อที่ใช้งานได้นานซึ่งอนุญาตให้ส่งข้อความได้หลายรายการ
ดูข้อมูลเกี่ยวกับการส่งข้อความระหว่างส่วนขยายได้ที่ส่วนข้อความข้ามส่วนขยาย
คำขอแบบครั้งเดียว
หากต้องการส่งข้อความเดียวไปยังส่วนอื่นของส่วนขยาย และรับการตอบกลับหรือไม่ก็ได้ ให้เรียกใช้ runtime.sendMessage() หรือ tabs.sendMessage()
วิธีการเหล่านี้ช่วยให้คุณส่งข้อความที่แปลงเป็น JSON ได้แบบครั้งเดียวจาก Content Script ไปยัง
ส่วนขยาย หรือจากส่วนขยายไปยัง Content Script ทั้ง 2 API จะแสดงผล Promise
ซึ่งจะเปลี่ยนเป็นการตอบกลับที่ผู้รับให้ไว้
การส่งคำขอจาก Content Script จะมีลักษณะดังนี้
content-script.js:
(async () => {
const response = await chrome.runtime.sendMessage({greeting: "hello"});
// do something with response here, not outside the function
console.log(response);
})();
คำตอบ
หากต้องการฟังข้อความ ให้ใช้เหตุการณ์ chrome.runtime.onMessage
// Event listener
function handleMessages(message, sender, sendResponse) {
if (message !== 'get-status') return;
fetch('https://example.com')
.then((response) => sendResponse({statusCode: response.status}))
// Since `fetch` is asynchronous, must return an explicit `true`
return true;
}
chrome.runtime.onMessage.addListener(handleMessages);
// From the sender's context...
const {statusCode} = await chrome.runtime.sendMessage('get-status');
เมื่อมีการเรียกใช้เครื่องมือฟังเหตุการณ์ ระบบจะส่งsendResponseฟังก์ชันเป็นพารามิเตอร์ที่ 3
นี่คือฟังก์ชันที่เรียกใช้เพื่อแสดงคำตอบได้ โดย
ค่าเริ่มต้น ระบบจะเรียกใช้ Callback ของ sendResponse แบบพร้อมกัน
หากคุณเรียกใช้ sendResponse โดยไม่มีพารามิเตอร์ ระบบจะส่ง null เป็นการตอบกลับ
หากต้องการส่งการตอบกลับแบบไม่พร้อมกัน คุณมี 2 ตัวเลือก ได้แก่ การส่งคืน true หรือ
การส่งคืน Promise
กลับtrue
หากต้องการตอบกลับแบบไม่พร้อมกันโดยใช้ sendResponse() ให้ส่งคืนค่าตามตัวอักษร true
(ไม่ใช่แค่ค่าที่เป็นจริง) จากเครื่องมือฟังเหตุการณ์ การทำเช่นนี้จะทำให้ช่องข้อความเปิดอยู่กับปลายทางอีกด้านจนกว่าจะมีการเรียกใช้ sendResponse ซึ่งจะช่วยให้คุณเรียกใช้ได้ในภายหลัง
ส่งคืน Promise
ตั้งแต่ Chrome 144 เป็นต้นไป คุณสามารถส่งคืน Promise จากเครื่องมือฟังข้อความเพื่อ ตอบกลับแบบไม่พร้อมกันได้ หาก Promise แก้ไขแล้ว ระบบจะส่งค่าที่แก้ไขแล้วเป็นคำตอบ
หากสัญญาถูกปฏิเสธ sendMessage() การเรียก
ของผู้ส่งจะถูกปฏิเสธพร้อมกับข้อความของข้อผิดพลาด ดูรายละเอียดและตัวอย่างเพิ่มเติมได้ที่ส่วนการจัดการข้อผิดพลาด
ตัวอย่างที่แสดงการคืนค่า Promise ที่อาจแก้ไขหรือปฏิเสธ
// Event listener
function handleMessages(message, sender, sendResponse) {
// Return a promise that wraps fetch
// If the response is OK, resolve with the status. If it's not OK then reject
// with the network error that prevents the fetch from completing.
return new Promise((resolve, reject) => {
fetch('https://example.com')
.then(response => {
if (!response.ok) {
reject(response);
} else {
resolve(response.status);
}
})
.catch(error => {
reject(error);
});
});
}
chrome.runtime.onMessage.addListener(handleMessages);
นอกจากนี้ คุณยังประกาศ Listener เป็น async เพื่อส่งคืน Promise ได้ด้วย
chrome.runtime.onMessage.addListener(async function(message, sender) {
const response = await fetch('https://example.com');
if (!response.ok) {
// rejects the promise returned by `async function`.
throw new Error(`Fetch failed: ${response.status}`);
}
// resolves the promise returned by `async function`.
return {statusCode: response.status};
});
ส่งคืน Promise: asyncข้อควรระวังเกี่ยวกับฟังก์ชัน
โปรดทราบว่าฟังก์ชัน async ในฐานะ Listener จะแสดงผล Promise เสมอ แม้จะไม่มีคำสั่ง return ก็ตาม หาก Listener async ไม่ส่งคืนค่า สัญญาจะได้รับการแก้ไขโดยปริยายเป็น undefined และระบบจะส่ง null เป็นการตอบกลับไปยังผู้ส่ง ซึ่งอาจทำให้เกิดลักษณะการทำงานที่ไม่คาดคิดเมื่อมี Listener หลายรายการ ดังนี้
// content_script.js
function handleResponse(message) {
// The first listener promise resolves to `undefined` before the second
// listener can respond. When a listener responds with `undefined`, Chrome
// sends null as the response.
console.assert(message === null);
}
function notifyBackgroundPage() {
const sending = chrome.runtime.sendMessage('test');
sending.then(handleResponse);
}
notifyBackgroundPage();
// background.js
chrome.runtime.onMessage.addListener(async (message) => {
// This just causes the function to pause for a millisecond, but the promise
// is *not* returned from the listener so it doesn't act as a response.
await new Promise(resolve => {
setTimeout(resolve, 1, 'OK');
});
// `async` functions always return promises. So once we
// reach here there is an implicit `return undefined;`. Chrome translates
// `undefined` responses to `null`.
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return new Promise((resolve) => {
setTimeout(resolve, 1000, 'response');
});
});
การจัดการข้อผิดพลาด
ตั้งแต่ Chrome 144 เป็นต้นไป หาก Listener onMessage แสดงข้อผิดพลาด (ทั้งแบบพร้อมกันหรือแบบไม่พร้อมกันโดยการแสดงผล Promise ที่ปฏิเสธ) Promise ที่ sendMessage() แสดงผลในผู้ส่งจะปฏิเสธพร้อมกับข้อความของข้อผิดพลาด
กรณีนี้อาจเกิดขึ้นได้เช่นกันหากผู้ฟังพยายามส่งคืนการตอบกลับที่แปลงเป็น JSON ไม่ได้โดยไม่ได้ตรวจหา TypeError ที่เกิดขึ้น
หาก Listener แสดงผล Promise ที่ปฏิเสธ จะต้องปฏิเสธด้วยอินสแตนซ์ Error
เพื่อให้ผู้ส่งได้รับข้อความแสดงข้อผิดพลาดนั้น หากมีการปฏิเสธ Promise ด้วยค่าอื่น (เช่น null หรือ undefined) sendMessage()
จะถูกปฏิเสธพร้อมข้อความแสดงข้อผิดพลาดทั่วไปแทน
หากลงทะเบียน Listener หลายรายการสำหรับ onMessage จะมีเพียง Listener รายการแรกที่ตอบกลับ ปฏิเสธ หรือแสดงข้อผิดพลาดเท่านั้นที่จะส่งผลต่อผู้ส่ง ส่วน Listener อื่นๆ ทั้งหมดจะทำงาน แต่ระบบจะไม่สนใจผลลัพธ์ของ Listener เหล่านั้น
ตัวอย่าง
หาก Listener แสดงผล Promise ที่ปฏิเสธ sendMessage() จะถูกปฏิเสธ
// sender.js
try {
await chrome.runtime.sendMessage('test');
} catch (e) {
console.log(e.message); // "some error"
}
// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return Promise.reject(new Error('some error'));
});
หาก Listener ตอบกลับด้วยค่าที่ทำการซีเรียลไลซ์ไม่ได้ ระบบจะปฏิเสธ sendMessage()
// sender.js
try {
await chrome.runtime.sendMessage('test');
} catch (e) {
console.log(e.message); // "Error: Could not serialize message."
}
// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse(() => {}); // Functions are not serializable
return true; // Keep channel open for async sendResponse
});
หากผู้ฟังแสดงข้อผิดพลาดพร้อมกันก่อนที่ผู้ฟังรายอื่นจะตอบกลับ
ระบบจะปฏิเสธพรอมิสของsendMessage()ผู้ฟัง
// sender.js
try {
await chrome.runtime.sendMessage('test');
} catch (e) {
console.log(e.message); // "error!"
}
// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
throw new Error('error!');
});
อย่างไรก็ตาม หากผู้ฟังคนหนึ่งตอบกลับก่อนที่อีกคนจะส่งข้อผิดพลาด
sendMessage() จะสำเร็จ
// sender.js
const response = await chrome.runtime.sendMessage('test');
console.log(response); // "OK"
// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse('OK');
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
throw new Error('error!');
});
การเชื่อมต่อที่ใช้งานได้นาน
หากต้องการสร้างช่องการส่งข้อความแบบมีอายุยาวนานที่ใช้ซ้ำได้ ให้เรียกใช้
runtime.connect()เพื่อส่งข้อความจากสคริปต์เนื้อหา ไปยังหน้าส่วนขยายtabs.connect()เพื่อส่งข้อความจากหน้าส่วนขยายไปยัง Content Script
คุณตั้งชื่อช่องได้โดยส่งพารามิเตอร์ options ที่มีคีย์ name เพื่อ
แยกความแตกต่างระหว่างการเชื่อมต่อประเภทต่างๆ ดังนี้
const port = chrome.runtime.connect({name: "example"});
กรณีการใช้งานที่เป็นไปได้ของการเชื่อมต่อที่มีอายุการใช้งานยาวนานคือส่วนขยายการกรอกแบบฟอร์มอัตโนมัติ Content Script อาจเปิดช่องทางไปยังหน้าส่วนขยายสำหรับการเข้าสู่ระบบที่เฉพาะเจาะจง และ ส่งข้อความไปยังส่วนขยายสำหรับองค์ประกอบอินพุตแต่ละรายการในหน้าเว็บเพื่อขอข้อมูล แบบฟอร์มที่จะกรอก การเชื่อมต่อที่แชร์ช่วยให้ส่วนขยายแชร์สถานะระหว่างคอมโพเนนต์ของส่วนขยายได้
เมื่อสร้างการเชื่อมต่อ ระบบจะกำหนดออบเจ็กต์ runtime.Port ให้แต่ละปลายทางเพื่อใช้ส่งและรับข้อความผ่านการเชื่อมต่อนั้น
ใช้โค้ดต่อไปนี้เพื่อเปิดแชแนลจาก Content Script รวมถึงส่งและรับฟังข้อความ
content-script.js:
const port = chrome.runtime.connect({name: "knockknock"});
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"});
}
});
port.postMessage({joke: "Knock knock"});
หากต้องการส่งคำขอจากส่วนขยายไปยัง Content Script ให้แทนที่การเรียกใช้ runtime.connect()
ในตัวอย่างก่อนหน้าด้วย tabs.connect()
หากต้องการจัดการการเชื่อมต่อขาเข้าสำหรับ Content Script หรือหน้าส่วนขยาย ให้ตั้งค่า Listener เหตุการณ์ runtime.onConnect เมื่อส่วนอื่นของส่วนขยายเรียกใช้ connect() ระบบจะเปิดใช้งานเหตุการณ์นี้และออบเจ็กต์ runtime.Port
โค้ดสำหรับการตอบสนองต่อการเชื่อมต่อขาเข้ามีลักษณะดังนี้
service-worker.js:
chrome.runtime.onConnect.addListener(function(port) {
if (port.name !== "knockknock") {
return;
}
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."});
}
});
});
การเรียงอันดับ
ใน Chrome API การส่งข้อความจะใช้ การซีเรียลไลซ์ JSON โปรดทราบว่าวิธีนี้แตกต่างจากเบราว์เซอร์อื่นๆ ที่ใช้ API เดียวกันกับอัลกอริทึมการโคลนที่มีโครงสร้าง
ซึ่งหมายความว่าข้อความ (และการตอบกลับที่ผู้รับระบุ) อาจมีJSON.stringify()
ค่าที่ถูกต้อง
ระบบจะบังคับให้ค่าอื่นๆ เป็นค่าที่ทำให้เป็นอนุกรมได้ (โดยเฉพาะอย่างยิ่ง
undefined จะได้รับการทำให้เป็นอนุกรมเป็น null)
ขีดจำกัดขนาดข้อความ
ขนาดสูงสุดของข้อความคือ 64 MiB
อายุการใช้งานของพอร์ต
พอร์ตได้รับการออกแบบให้เป็นกลไกการสื่อสารแบบ 2 ทางระหว่างส่วนต่างๆ
ของส่วนขยาย เมื่อส่วนหนึ่งของส่วนขยายเรียกใช้
tabs.connect(), runtime.connect() หรือ
runtime.connectNative() ระบบจะสร้าง
พอร์ตที่ส่งข้อความได้ทันทีโดยใช้
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 !== allowlistedExtension) {
return; // don't allow this extension access
}
if (request.getTargetData) {
sendResponse({ targetData: targetData });
} else if (request.activateLasers) {
const 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.
const 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:
const port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);
ส่งข้อความจากหน้าเว็บ
ส่วนขยายยังรับและตอบกลับข้อความจากหน้าเว็บได้ด้วย หากต้องการส่งข้อความจากหน้าเว็บไปยังส่วนขยาย ให้ระบุใน manifest.json ว่าต้องการอนุญาตให้เว็บไซต์ใดส่งข้อความโดยใช้คีย์ไฟล์ Manifest "externally_connectable" เช่น
manifest.json
"externally_connectable": {
"matches": ["https://*.example.com/*"]
}
ซึ่งจะเปิดเผย API การรับส่งข้อความต่อหน้าเว็บใดก็ตามที่ตรงกับรูปแบบ URL ที่คุณระบุ รูปแบบ URL ต้องมีโดเมนระดับที่ 2 อย่างน้อย 1 รายการ นั่นคือระบบไม่รองรับรูปแบบชื่อโฮสต์ เช่น "*" "*.com" "*.co.uk" และ "*.appspot.com" คุณใช้
<all_urls> เพื่อเข้าถึงโดเมนทั้งหมดได้
ใช้ API runtime.sendMessage() หรือ runtime.connect() เพื่อส่ง
ข้อความไปยังส่วนขยายที่เฉพาะเจาะจง เช่น
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);
});
คุณไม่สามารถส่งข้อความจากส่วนขยาย ไปยังหน้าเว็บได้
การรับส่งข้อความแบบเดิม
ส่วนขยายแลกเปลี่ยนข้อความกับแอปพลิเคชันเนทีฟที่ลงทะเบียนเป็นโฮสต์การรับส่งข้อความเนทีฟได้ ดูข้อมูลเพิ่มเติมเกี่ยวกับฟีเจอร์นี้ได้ที่การรับส่งข้อความเนทีฟ
ข้อควรพิจารณาด้านความปลอดภัย
ข้อควรพิจารณาด้านความปลอดภัยบางประการที่เกี่ยวข้องกับการรับส่งข้อความมีดังนี้
สคริปต์เนื้อหาน่าเชื่อถือน้อยกว่า
สคริปต์เนื้อหาน่าเชื่อถือน้อยกว่า Service Worker ของส่วนขยาย ตัวอย่างเช่น หน้าเว็บที่เป็นอันตรายอาจทำให้กระบวนการแสดงผลที่เรียกใช้สคริปต์เนื้อหาถูกบุกรุก พึงระลึกว่าข้อความจาก Content Script อาจสร้างขึ้นโดยผู้โจมตี และอย่าลืมตรวจสอบและล้างข้อมูลอินพุตทั้งหมด พึงระลึกว่าข้อมูลใดก็ตามที่ส่งไปยัง Content Script อาจรั่วไหลไปยังหน้าเว็บ จำกัดขอบเขตของการดำเนินการที่มีสิทธิ์ซึ่งทริกเกอร์ได้โดยข้อความที่ได้รับจาก Content Script
Cross-site Scripting
อย่าลืมปกป้องสคริปต์ของคุณจากการเขียนสคริปต์ข้ามเว็บไซต์ เมื่อได้รับข้อมูลจากแหล่งที่มาที่ไม่น่าเชื่อถือ เช่น ข้อมูลที่ผู้ใช้ป้อน เว็บไซต์อื่นๆ ผ่าน Content Script หรือ API ให้ระมัดระวังอย่าตีความข้อมูลนี้เป็น HTML หรือใช้ในลักษณะที่อาจอนุญาตให้โค้ดที่ไม่คาดคิดทำงานได้
ใช้ API ที่ไม่เรียกใช้สคริปต์เมื่อเป็นไปได้
service-worker.js
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) { // JSON.parse doesn't evaluate the attacker's scripts. const 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! const 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; });