Messaging API ช่วยให้คุณสื่อสารระหว่างสคริปต์ต่างๆ ที่ทำงานในบริบทที่เชื่อมโยงกับส่วนขยายได้ ซึ่งรวมถึงการสื่อสารระหว่าง Service Worker, chrome-extension://pages และสคริปต์เนื้อหา ตัวอย่างเช่น ส่วนขยายโปรแกรมอ่าน RSS อาจใช้สคริปต์เนื้อหาเพื่อตรวจหาฟีด RSS ในหน้าเว็บ จากนั้นแจ้งให้ Service Worker อัปเดตไอคอนการทำงานสำหรับหน้านั้น
Messaging API มี 2 แบบ ได้แก่ API สำหรับ คำขอแบบครั้งเดียว และ API ที่ซับซ้อนกว่าสำหรับการ เชื่อมต่อที่ใช้งานได้นานซึ่งอนุญาตให้ส่งข้อความได้หลายรายการ
ดูข้อมูลเกี่ยวกับการส่งข้อความระหว่างส่วนขยายได้ที่ส่วนข้อความระหว่างส่วนขยาย
คำขอแบบครั้งเดียว
หากต้องการส่งข้อความเดียวไปยังส่วนอื่นของส่วนขยาย และเลือกรับการตอบกลับ ให้เรียกใช้ runtime.sendMessage() หรือ tabs.sendMessage()
เมธอดเหล่านี้ช่วยให้คุณส่งข้อความที่แปลงเป็น JSON ได้แบบครั้งเดียวจากสคริปต์เนื้อหาไปยังส่วนขยาย หรือจากส่วนขยายไปยังสคริปต์เนื้อหา ทั้ง 2 API จะแสดงผล Promise ซึ่งจะแสดงผลการตอบกลับที่ผู้รับให้ไว้
การส่งคำขอจากสคริปต์เนื้อหาจะมีลักษณะดังนี้
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');
เมื่อมีการเรียกใช้ Listener เหตุการณ์ ระบบจะส่งฟังก์ชัน sendResponse เป็นพารามิเตอร์ที่ 3 ซึ่งเป็นฟังก์ชันที่เรียกใช้เพื่อแสดงการตอบกลับได้ โดยค่าเริ่มต้น การเรียกกลับ sendResponse ต้องเรียกใช้แบบซิงโครนัส
หากคุณเรียกใช้ sendResponse โดยไม่มีพารามิเตอร์ ระบบจะส่ง null เป็นการตอบกลับ
หากต้องการส่งการตอบกลับแบบอะซิงโครนัส คุณมี 2 ตัวเลือก ได้แก่ แสดงผล true หรือแสดงผล Promise
แสดงผล true
หากต้องการตอบกลับแบบอะซิงโครนัสโดยใช้ sendResponse() ให้แสดงผล true แบบ Literal (ไม่ใช่แค่ค่า Truthy) จาก Listener เหตุการณ์ การดำเนินการดังกล่าวจะทำให้ช่องข้อความเปิดอยู่สำหรับปลายทางอื่นจนกว่าจะมีการเรียกใช้ sendResponse ซึ่งช่วยให้คุณเรียกใช้ได้ในภายหลัง
แสดงผล Promise
ตั้งแต่ Chrome 148 เป็นต้นไป คุณสามารถแสดงผล Promise จาก Listener ข้อความเพื่อตอบกลับแบบอะซิงโครนัสได้ การอัปเดตนี้จะทยอยเปิดตัว คุณจึงอาจพบว่าการอัปเดตนี้ยังไม่พร้อมใช้งานในเบราว์เซอร์ของผู้ใช้บางราย คุณควรตรวจสอบว่าส่วนขยายจัดการได้ว่าความสามารถนี้เปิดใช้อยู่หรือไม่ การใช้ return true; จะยังคงใช้งานได้กับการตอบกลับแบบอะซิงโครนัสไม่ว่าความสามารถนี้จะเปิดใช้อยู่หรือไม่ก็ตาม
หาก 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};
});
ข้อควรระวังเกี่ยวกับฟังก์ชัน async ที่แสดงผล Promise
โปรดทราบว่าฟังก์ชัน async ที่เป็น Listener จะแสดงผล Promise เสมอ แม้ว่าจะไม่มีคำสั่ง return ก็ตาม หาก Listener async ไม่แสดงผลค่า Promise ของ Listener จะแสดงผล 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 146 เป็นต้นไป หาก Listener onMessage แสดงข้อผิดพลาด (ทั้งแบบซิงโครนัสหรือแบบอะซิงโครนัสโดยการแสดงผล Promise ที่ถูกปฏิเสธ) Promise ที่แสดงผลโดย sendMessage() ในผู้ส่งจะถูกปฏิเสธพร้อมข้อความแสดงข้อผิดพลาด การอัปเดตนี้จะทยอยเปิดตัว คุณจึงอาจพบว่าการเปลี่ยนแปลงนี้ยังไม่มีผลในเบราว์เซอร์ของผู้ใช้บางราย คุณควรตรวจสอบว่าส่วนขยายจัดการได้ว่าการเปลี่ยนแปลงนี้เปิดใช้อยู่หรือไม่
หาก Listener พยายามแสดงผลการตอบกลับที่แปลงเป็นอนุกรมไม่ได้
(โดยไม่ดักจับ TypeError) ระบบ
จะถือว่า Listener แสดงข้อผิดพลาดด้วย
หาก Listener แสดงผล Promise ที่ถูกปฏิเสธ Listener จะต้องถูกปฏิเสธด้วยอินสแตนซ์ Error เพื่อให้ผู้ส่งได้รับข้อความแสดงข้อผิดพลาดนั้น หาก Promise ถูกปฏิเสธด้วยค่าอื่น (เช่น null หรือ undefined) ระบบจะปฏิเสธ sendMessage() พร้อมข้อความแสดงข้อผิดพลาดทั่วไปแทน
หากมีการลงทะเบียน Listener หลายรายการสำหรับ onMessage เฉพาะ 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
});
หาก Listener แสดงข้อผิดพลาดแบบซิงโครนัสก่อนที่ Listener อื่นจะตอบกลับ ระบบจะปฏิเสธ Promise sendMessage() ของ Listener
// 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!');
});
อย่างไรก็ตาม หาก Listener รายการหนึ่งตอบกลับก่อนที่อีกรายการจะแสดงข้อผิดพลาด 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()เพื่อส่งข้อความจากหน้าส่วนขยายไปยังสคริปต์เนื้อหา
คุณสามารถตั้งชื่อช่องได้โดยส่งพารามิเตอร์ตัวเลือกที่มีคีย์ name เพื่อแยกความแตกต่างระหว่างการเชื่อมต่อประเภทต่างๆ
const port = chrome.runtime.connect({name: "example"});
กรณีการใช้งานที่เป็นไปได้สำหรับการเชื่อมต่อที่ใช้งานได้นานคือส่วนขยายการกรอกแบบฟอร์มอัตโนมัติ สคริปต์เนื้อหาอาจเปิดช่องไปยังหน้าส่วนขยายสำหรับการเข้าสู่ระบบที่เฉพาะเจาะจง และส่งข้อความไปยังส่วนขยายสำหรับองค์ประกอบอินพุตแต่ละรายการในหน้าเว็บเพื่อขอข้อมูลแบบฟอร์มที่จะกรอก การเชื่อมต่อที่แชร์ช่วยให้ส่วนขยายแชร์สถานะระหว่างคอมโพเนนต์ส่วนขยายได้
เมื่อสร้างการเชื่อมต่อ ระบบจะกำหนดออบเจ็กต์ runtime.Port ให้แต่ละปลายทางเพื่อ
ส่งและรับข้อความผ่านการเชื่อมต่อนั้น
ใช้โค้ดต่อไปนี้เพื่อเปิดช่องจากสคริปต์เนื้อหา รวมถึงส่งและรับฟังข้อความ
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"});
หากต้องการส่งคำขอจากส่วนขยายไปยังสคริปต์เนื้อหา ให้แทนที่การเรียกใช้ runtime.connect()
ในตัวอย่างก่อนหน้าด้วย tabs.connect()
หากต้องการจัดการการเชื่อมต่อขาเข้าสำหรับสคริปต์เนื้อหาหรือหน้าส่วนขยาย ให้ตั้ง
ค่า runtime.onConnect Listener เหตุการณ์ เมื่อส่วนอื่นของส่วนขยายเรียกใช้ 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 Messaging 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 เหตุการณ์นี้จะทริกเกอร์เมื่อไม่มีพอร์ตที่ถูกต้องที่ปลายทางอื่นของช่อง ซึ่งอาจเกิดจากสาเหตุต่อไปนี้
- ไม่มี Listener สำหรับ
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/*"]
}
การดำเนินการนี้จะเปิดเผย Messaging API ให้กับทุกหน้าเว็บที่ตรงกับ รูปแบบการจับคู่ที่คุณระบุ
ใช้ 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);
}
);
}
จากส่วนขยาย ให้รับฟังข้อความจากหน้าเว็บโดยใช้
runtime.onMessageExternal หรือ runtime.onConnectExternal
API เช่นเดียวกับการรับส่งข้อความระหว่างส่วนขยาย ตัวอย่าง
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 ของส่วนขยาย ตัวอย่างเช่น หน้าเว็บที่เป็นอันตรายอาจสามารถบุกรุกกระบวนการแสดงผลที่เรียกใช้สคริปต์เนื้อหาได้ ให้ถือว่าข้อความจากสคริปต์เนื้อหาอาจถูกสร้างขึ้นโดยผู้โจมตี และตรวจสอบและล้างข้อมูลอินพุตทั้งหมด ให้ถือว่าข้อมูลใดก็ตามที่ส่งไปยังสคริปต์เนื้อหาอาจรั่วไหลไปยังหน้าเว็บ จำกัดขอบเขตของการดำเนินการที่มีสิทธิ์ซึ่งอาจทริกเกอร์โดยข้อความที่ได้รับจากสคริปต์เนื้อหา
Cross-site Scripting
ตรวจสอบว่าได้ปกป้องสคริปต์ของคุณจากการโจมตีแบบ 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. 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; });