ממשקי API להעברת הודעות מאפשרים תקשורת בין סקריפטים שונים שפועלים בהקשרים שמשויכים לתוסף. התקשורת הזו כוללת את העובד של השירות, chrome-extension://pages וסקריפטים של תוכן. לדוגמה, תוסף לקריאת RSS עשוי להשתמש בסקריפטים של תוכן כדי לזהות את הנוכחות של פיד RSS בדף, ואז להודיע ל-service worker לעדכן את סמל הפעולה של הדף הזה.
יש שני ממשקי API להעברת הודעות: אחד לבקשות חד-פעמיות, וממשק מורכב יותר לחיבורים לטווח ארוך שמאפשרים לשלוח כמה הודעות.
מידע על שליחת הודעות בין תוספים זמין בקטע הודעות בין תוספים.
בקשות חד-פעמיות
כדי לשלוח הודעה בודדת לחלק אחר של התוסף, ואם רוצים גם לקבל תגובה, קוראים לפונקציה runtime.sendMessage() או tabs.sendMessage().
השיטות האלה מאפשרות לשלוח הודעה חד-פעמית שניתנת לסריאליזציה ב-JSON מסקריפט תוכן לתוסף, או מהתוסף לסקריפט תוכן. שני ממשקי ה-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');
כשקוראים ל-event listener, מעבירים פונקציה sendResponse כפרמטר השלישי. זו פונקציה שאפשר להפעיל כדי לספק תשובה. כברירת מחדל, צריך לקרוא לפונקציית ההתקשרות חזרה sendResponse באופן סינכרוני.
אם מתקשרים אל sendResponse בלי פרמטרים, נשלחת התגובה null.
כדי לשלוח תגובה באופן אסינכרוני, יש שתי אפשרויות: להחזיר true או להחזיר הבטחה.
החזרת מוצר true
כדי להגיב באופן אסינכרוני באמצעות sendResponse(), מחזירים ערך מילולי true
(לא רק ערך שהוא true) ממאזין האירועים. הפעולה הזו תשמור על ערוץ ההודעות פתוח בצד השני עד שמתקשרים אל sendResponse, כך שתוכלו להתקשר אליו מאוחר יותר.
החזרת הבטחה
החל מ-Chrome 144, אפשר להחזיר הבטחה ממאזין הודעות כדי להגיב באופן אסינכרוני. אם ההבטחה מתקבלת, הערך שלה נשלח כתשובה.
אם ההבטחה נדחית, הקריאה של השולח sendMessage() תידחה עם הודעת השגיאה. פרטים נוספים ודוגמאות מופיעים בקטע טיפול בשגיאות.
דוגמה להחזרת הבטחה שיכולה להיות מסוג resolve או reject:
// 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);
אפשר גם להצהיר על מאזין כ-async כדי להחזיר הבטחה:
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 מלכודות בפונקציות
חשוב לזכור שאם משתמשים בפונקציה async כמאזין, היא תמיד תחזיר הבטחה, גם בלי הצהרה של return. אם מאזין async לא מחזיר ערך, ההבטחה שלו נפתרת באופן מרומז ל-undefined, והערך null נשלח כתשובה לשולח. זה יכול לגרום להתנהגות לא צפויה כשיש כמה מאזינים:
// 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, אם מאזין onMessage מעלה שגיאה (באופן סינכרוני או אסינכרוני על ידי החזרת הבטחה שנדחית), ההבטחה שמוחזרת על ידי sendMessage() בשולח תידחה עם הודעת השגיאה.
זה יכול לקרות גם אם מאזין מנסה להחזיר תגובה שלא ניתן לסדר אותה בפורמט JSON בלי לתפוס את ה-TypeError שנוצר.
אם מאזין מחזיר הבטחה שנדחית, הוא חייב להידחות עם מופע של Error
כדי שהשולח יקבל את הודעת השגיאה הזו. אם ה-Promise נדחה עם ערך אחר (כמו null או undefined), sendMessage() יידחה עם הודעת שגיאה כללית.
אם רשומים כמה מאזינים ל-onMessage, רק המאזין הראשון שיגיב, ידחה או יחזיר שגיאה ישפיע על השולח. כל שאר המאזינים יפעלו, אבל התוצאות שלהם יתעלמו.
דוגמאות
אם פונקציית ה-listener מחזירה הבטחה שנדחית, ההבטחה 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()כדי להעביר הודעות מדף תוסף לסקריפט תוכן.
אפשר לתת שם לערוץ על ידי העברת פרמטר של אפשרויות עם מפתח 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().
כדי לטפל בחיבורים נכנסים לסקריפט תוכן או לדף הרחבה, צריך להגדיר פוקנציית event 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);
מגבלות על גודל ההודעה
הגודל המקסימלי של הודעה הוא 64MiB.
משך החיים של יציאה
יציאות מיועדות להיות מנגנון תקשורת דו-כיווני בין חלקים שונים של תוסף. כשחלק מתוסף קורא ל-tabs.connect(), ל-runtime.connect() או ל-runtime.connectNative(), הוא יוצר Port שיכול לשלוח הודעות באופן מיידי באמצעות postMessage().
אם יש כמה מסגרות בכרטיסייה, הקריאה ל-tabs.connect() מפעילה את האירוע runtime.onConnect פעם אחת לכל מסגרת בכרטיסייה. באופן דומה, אם מתבצעת קריאה ל-runtime.connect(), האירוע onConnect יכול להיות מופעל פעם אחת לכל פריים בתהליך ההרחבה.
לדוגמה, אם אתם שומרים מצבים נפרדים לכל יציאה פתוחה, יכול להיות שתרצו לדעת מתי חיבור נסגר. כדי לעשות זאת, צריך להאזין לאירוע 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 אילו אתרים מורשים לשלוח הודעות באמצעות מפתח המניפסט "externally_connectable". לדוגמה:
manifest.json
"externally_connectable": {
"matches": ["https://*.example.com/*"]
}
הפעולה הזו חושפת את ה-API של העברת ההודעות לכל דף שתואם לתבניות כתובות ה-URL שציינתם. תבנית כתובת ה-URL חייבת להכיל לפחות דומיין ברמה השנייה. כלומר, לא ניתן להשתמש בתבניות של שמות מארחים כמו '*', '*.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);
});
אי אפשר לשלוח הודעה מתוסף אל דף אינטרנט.
העברת הודעות מקומית
תוספים יכולים להחליף הודעות עם אפליקציות מקומיות שרשומות כמארח הודעות מקומי. מידע נוסף על התכונה הזו זמין במאמר בנושא הודעות מובנות.
שיקולי אבטחה
ריכזנו כאן כמה שיקולי אבטחה שקשורים להודעות.
סקריפטים של תוכן פחות מהימנים
סקריפטים של תוכן פחות מהימנים מאשר סקריפט שירות של תוסף. לדוגמה, דף אינטרנט זדוני עשוי לפגוע בתהליך העיבוד שבו מופעלים סקריפטים של תוכן. צריך להניח שהודעות מסקריפט תוכן עשויות להיות מנוסחות על ידי תוקף, ולוודא שמאמתים ומסננים את כל הקלט. צריך להניח שכל נתון שנשלח אל סקריפט התוכן עלול לדלוף אל דף האינטרנט. להגביל את היקף הפעולות עם הרשאות מיוחדות שאפשר להפעיל באמצעות הודעות שמתקבלות מסקריפטים של תוכן.
פרצת אבטחה XSS (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; });