ממשקי API להעברת הודעות מאפשרים תקשורת בין סקריפטים שונים שפועלים בהקשרים שמשויכים לתוסף. התקשורת הזו כוללת את קובץ השירות (service worker), דפי chrome-extension://וסקריפטים של תוכן. לדוגמה, תוסף לקריאת RSS יכול להשתמש בסקריפטים של תוכן כדי לזהות אם יש פיד RSS בדף, ואז להודיע ל-service worker לעדכן את סמל הפעולה של הדף הזה.
יש שני ממשקי API להעברת הודעות: אחד לבקשות חד-פעמיות, וממשק מורכב יותר לחיבורים ארוכי טווח שמאפשרים לשלוח כמה הודעות.
מידע על שליחת הודעות בין תוספים זמין בקטע הודעות בין תוספים.
בקשות חד-פעמיות
כדי לשלוח הודעה בודדת לחלק אחר של התוסף, ואם רוצים גם לקבל תגובה, מתקשרים אל runtime.sendMessage() או אל tabs.sendMessage().
השיטות האלה מאפשרות לשלוח הודעה חד-פעמית שניתנת לסריאליזציה ב-JSON מסקריפט תוכן לתוסף, או מהתוסף לסקריפט תוכן. שני ממשקי ה-API מחזירים Promise
שמפנה לתגובה שסופקה על ידי הנמען.
כך נראית שליחת בקשה מסקריפט תוכן:
content-script.js:
(async () => {
const response = await browser.runtime.sendMessage({greeting: "hello"});
// do something with response here, not outside the function
console.log(response);
})();
תשובות
כדי להאזין להודעה, משתמשים באירוע browser.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;
}
browser.runtime.onMessage.addListener(handleMessages);
// From the sender's context...
const {statusCode} = await browser.runtime.sendMessage('get-status');
כשקוראים ל-event listener, מעבירים פונקציה sendResponse כפרמטר השלישי. זו פונקציה שאפשר להפעיל כדי לספק תשובה. כברירת מחדל, צריך להפעיל את הקריאה החוזרת sendResponse באופן סינכרוני.
אם מתקשרים למספר sendResponse בלי פרמטרים, נשלחת התגובה null.
כדי לשלוח תגובה באופן אסינכרוני, יש שתי אפשרויות: להחזיר true או להחזיר אובייקט promise.
החזרת מוצר true
כדי להגיב באופן אסינכרוני באמצעות sendResponse(), מחזירים ערך מילולי true
(לא רק ערך שהוא true) מרכיב event listener. הפעולה הזו תשמור על ערוץ ההודעות פתוח בצד השני עד שיתקשרו אל sendResponse, כך שתוכלו להתקשר אליו מאוחר יותר.
החזרת הבטחה
החל מ-Chrome 148, אפשר להחזיר אובייקט promise ממאזין הודעות כדי להגיב באופן אסינכרוני. העדכון הזה מושק בהדרגה, ולכן יכול להיות שהוא עדיין לא זמין בדפדפנים של כל המשתמשים. חשוב לוודא שהתוסף יכול לפעול גם אם היכולת הזו מופעלת וגם אם היא מושבתת. השימוש ב-return true; ימשיך לפעול לתשובות אסינכרוניות, בין אם היכולת הזו מופעלת ובין אם לא.
אם ה-Promise נפתר, הערך שלו נשלח כתשובה.
אם ההבטחה נדחית, הקריאה של השולח sendMessage() תידחה עם הודעת השגיאה. פרטים נוספים ודוגמאות מופיעים בקטע טיפול בשגיאות.
דוגמה להחזרת הבטחה שיכולה להיות מאושרת או להידחות:
// 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);
});
});
}
browser.runtime.onMessage.addListener(handleMessages);
אפשר גם להצהיר על מאזין כ-async כדי להחזיר הבטחה:
browser.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 בתור listener תמיד תחזיר promise, גם בלי הצהרת return. אם מאזין async לא מחזיר ערך, אובייקט ה-promise שלו נפתר באופן מרומז ל-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 = browser.runtime.sendMessage('test');
sending.then(handleResponse);
}
notifyBackgroundPage();
// background.js
browser.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`.
});
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
return new Promise((resolve) => {
setTimeout(resolve, 1000, 'response');
});
});
טיפול בשגיאות
החל מגרסה 146 של Chrome, אם מאזין onMessage מעלה שגיאה (באופן סינכרוני או אסינכרוני על ידי החזרת אובייקט promise שנדחה), אובייקט ה-promise שמוחזר על ידי sendMessage() בשולח יידחה עם הודעת השגיאה. העדכון הזה מושק בהדרגה, ולכן יכול להיות שהשינוי הזה עדיין לא יחול על הדפדפנים של כל המשתמשים. חשוב לוודא שהתוסף יכול להתמודד עם המצב שבו השינוי הזה מופעל או לא מופעל.
אם מאזין מנסה להחזיר תגובה שלא ניתן לסדר (בלי לתפוס את TypeError שנוצר), זה ייחשב גם כשגיאה שהמאזין זורק.
אם מאזין מחזיר הבטחה שנדחית, הוא חייב להידחות עם מופע של Error
כדי שהשולח יקבל את הודעת השגיאה הזו. אם ה-Promise נדחה עם ערך אחר (כמו null או undefined), הפונקציה sendMessage() תידחה עם הודעת שגיאה כללית.
אם רשומים כמה מאזינים ל-onMessage, רק המאזין הראשון שמגיב, דוחה או מעלה שגיאה ישפיע על השולח. כל שאר המאזינים יפעלו, אבל התוצאות שלהם יתעלמו.
דוגמאות
אם פונקציית ה-listener מחזירה אובייקט promise שנדחית, אובייקט ה-promise sendMessage() נדחית:
// sender.js
try {
await browser.runtime.sendMessage('test');
} catch (e) {
console.log(e.message); // "some error"
}
// listener.js
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
return Promise.reject(new Error('some error'));
});
אם מאזין מגיב עם ערך שלא ניתן לסדר אותו, הבקשה sendMessage() נדחית:
// sender.js
try {
await browser.runtime.sendMessage('test');
} catch (e) {
console.log(e.message); // "Error: Could not serialize message."
}
// listener.js
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse(() => {}); // Functions are not serializable
return true; // Keep channel open for async sendResponse
});
אם מאזין מעלה שגיאה באופן סינכרוני לפני שמאזין אחר מגיב, אובייקט ה-promise sendMessage() של המאזין נדחה:
// sender.js
try {
await browser.runtime.sendMessage('test');
} catch (e) {
console.log(e.message); // "error!"
}
// listener.js
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
throw new Error('error!');
});
עם זאת, אם מאזין אחד מגיב לפני שמאזין אחר מעלה שגיאה, הפונקציה sendMessage() מצליחה:
// sender.js
const response = await browser.runtime.sendMessage('test');
console.log(response); // "OK"
// listener.js
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse('OK');
});
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
throw new Error('error!');
});
חיבורים לטווח ארוך
כדי ליצור ערוץ להעברת הודעות לטווח ארוך שאפשר לעשות בו שימוש חוזר, קוראים לפונקציה:
-
runtime.connect()כדי להעביר הודעות מסקריפט תוכן לדף תוסף -
tabs.connect()כדי להעביר הודעות מדף של תוסף לסקריפט תוכן.
אתם יכולים לתת שם לערוץ על ידי העברת פרמטר של אפשרויות עם מפתח name כדי להבחין בין סוגים שונים של חיבורים:
const port = browser.runtime.connect({name: "example"});
תרחיש שימוש פוטנציאלי לחיבור לטווח ארוך הוא תוסף למילוי טפסים אוטומטי. יכול להיות שתסריט התוכן יפתח ערוץ לדף התוסף להתחברות ספציפית, וישלח הודעה לתוסף לכל רכיב קלט בדף כדי לבקש את נתוני הטופס למילוי. החיבור המשותף מאפשר לתוסף לשתף את המצב בין רכיבי התוסף.
כשמקימים חיבור, לכל קצה מוקצה אובייקט runtime.Port לשליחה ולקבלה של הודעות דרך החיבור הזה.
אפשר להשתמש בקוד הבא כדי לפתוח ערוץ מסקריפט תוכן, לשלוח הודעות ולהאזין להן:
content-script.js:
const port = browser.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:
browser.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:
browser.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:
browser.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:
browser.runtime.sendMessage(laserExtensionId, {getTargetData: true},
function(response) {
if (targetInRange(response.targetData))
browser.runtime.sendMessage(laserExtensionId, {activateLasers: true});
}
);
// For a long-lived connection:
const port = browser.runtime.connect(laserExtensionId);
port.postMessage(...);
שליחת הודעות מדפי אינטרנט
תוספים יכולים גם לקבל הודעות מדפי אינטרנט ולהשיב להן. כדי לשלוח הודעות מדף אינטרנט לתוסף, צריך לציין בקובץ manifest.json אילו אתרים מורשים לשלוח הודעות באמצעות מפתח המניפסט "externally_connectable". לדוגמה:
manifest.json
"externally_connectable": {
"matches": ["https://*.example.com/*"]
}
הפעולה הזו חושפת את 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 && browser.runtime) {
// Make a request:
browser.runtime.sendMessage(
editorExtensionId,
{
openUrlInEditor: url
},
(response) => {
if (!response.success) handleError(url);
}
);
}
בתוסף, מאזינים להודעות מדפי אינטרנט באמצעות ממשקי ה-API runtime.onMessageExternal או runtime.onConnectExternal, כמו בהעברת הודעות בין תוספים. לדוגמה:
service-worker.js
browser.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)
חשוב להגן על הסקריפטים מפני פרצת אבטחה XSS (cross-site scripting). כשמקבלים נתונים ממקור לא מהימן, כמו קלט של משתמשים, אתרים אחרים באמצעות סקריפט תוכן או API, חשוב להיזהר ולא לפרש את הנתונים כ-HTML או להשתמש בהם באופן שיאפשר לקוד לא צפוי לפעול.
כדאי להשתמש בממשקי API שלא מריצים סקריפטים, אם אפשר:
service-worker.js
browser.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
browser.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
browser.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) { // WARNING! Might be evaluating a malicious script! const resp = eval(`(${response.farewell})`); });
service-worker.js
browser.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) { // WARNING! Might be injecting a malicious script! document.getElementById("resp").innerHTML = response.farewell; });