העברת ההודעה

סקריפטים של תוכן פועלים בהקשר של דף אינטרנט ולא של התוסף, ולכן הם צריכים בדרך כלל דרך לתקשר עם שאר התוסף. לדוגמה, תוסף לקריאת RSS עשוי להשתמש בסקריפטים של תוכן כדי לזהות אם יש פיד RSS בדף, ואז להודיע על כך לדף הרקע כדי להציג סמל של פעולת דף עבור הדף הזה.

התקשורת בין תוספים לבין סקריפטים של תוכן מתבצעת באמצעות העברת הודעות. כל אחד מהצדדים יכול להאזין להודעות שנשלחות מהצד השני ולהגיב באותו הערוץ. הודעה יכולה להכיל כל אובייקט JSON תקין (null, ערך בוליאני, מספר, מחרוזת, מערך או אובייקט). יש API פשוט לבקשות חד-פעמיות ו-API מורכב יותר שמאפשר חיבורים לטווח ארוך להחלפת כמה הודעות עם הקשר משותף. אפשר גם לשלוח הודעה לתוסף אחר אם יודעים את המזהה שלו, כמו שמוסבר בקטע הודעות בין תוספים.

בקשות פשוטות חד-פעמיות

אם אתם צריכים לשלוח רק הודעה אחת לחלק אחר של התוסף (ואם רוצים, לקבל תגובה), כדאי להשתמש ב-runtime.sendMessage או ב-tabs.sendMessage במצב מפושט . הפונקציה הזו מאפשרת לשלוח הודעה חד-פעמית שניתנת לסריאליזציה ב-JSON מסקריפט תוכן לתוסף, או להיפך. פרמטר אופציונלי של קריאה חוזרת מאפשר לכם לטפל בתגובה מהצד השני, אם יש כזו.

כך נראית שליחת בקשה מסקריפט תוכן:

chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
  console.log(response.farewell);
});

שליחת בקשה מהתוסף לסקריפט תוכן נראית דומה מאוד, אבל צריך לציין לאיזו כרטיסייה לשלוח את הבקשה. בדוגמה הזו מוצג איך שולחים הודעה לסקריפט התוכן בכרטיסייה שנבחרה.

chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
  chrome.tabs.sendMessage(tabs[0].id, {greeting: "hello"}, function(response) {
    console.log(response.farewell);
  });
});

בצד המקבל, צריך להגדיר פונקציית event listener מסוג runtime.onMessage כדי לטפל בהודעה. התוצאה נראית זהה בסקריפט תוכן או בדף תוסף.

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() עבור אירוע מסוים יצליח לשלוח את התגובה. המערכת תתעלם מכל התגובות האחרות לאירוע.
הערה: הקריאה החוזרת sendResponse תקפה רק אם משתמשים בה באופן סינכרוני, או אם גורם מטפל באירועים מחזיר true כדי לציין שהוא יגיב באופן אסינכרוני. הקריאה החוזרת של הפונקציה sendMessage תופעל אוטומטית אם אף אחת מהפונקציות לטיפול באירועים לא תחזיר את הערך true או אם הקריאה החוזרת של sendResponse תעבור איסוף אשפה.

חיבורים לטווח ארוך

לפעמים כדאי לנהל שיחה שנמשכת יותר מבקשה ותשובה אחת. במקרה כזה, אפשר לפתוח ערוץ לזמן ארוך מסקריפט תוכן לדף של תוסף , או להיפך, באמצעות runtime.connect או tabs.connect, בהתאמה . אפשר לתת לערוץ שם כדי להבחין בין סוגים שונים של חיבורים.

מקרה שימוש אחד יכול להיות תוסף למילוי טפסים באופן אוטומטי. סקריפט התוכן יכול לפתוח ערוץ לדף התוסף עבור התחברות מסוימת, ולשלוח הודעה לתוסף עבור כל רכיב קלט בדף כדי לבקש את נתוני הטופס למילוי. החיבור המשותף מאפשר לתוסף לשמור על מצב משותף שמקשר בין כמה הודעות שמגיעות מתסריט התוכן.

כשיוצרים חיבור, כל קצה מקבל אובייקט runtime.Port שמשמש לשליחה ולקבלה של הודעות דרך החיבור הזה.

כך פותחים ערוץ מסקריפט תוכן, שולחים הודעות ומאזינים להן:

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"});
});

שליחת בקשה מהתוסף לסקריפט תוכן נראית דומה מאוד, אבל צריך לציין לאיזו כרטיסייה להתחבר. פשוט מחליפים את הקריאה לחיבור בדוגמה שלמעלה ב-tabs.connect.

כדי לטפל בחיבורים נכנסים, צריך להגדיר מאזין לאירוע runtime.onConnect. התוצאה נראית זהה בסקריפט תוכן או בדף תוסף. כשחלק אחר של התוסף קורא ל-connect(), האירוע הזה מופעל, יחד עם האובייקט runtime.Port שאפשר להשתמש בו כדי לשלוח ולקבל הודעות דרך החיבור. כך נראה מענה לחיבורים נכנסים:

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."});
  });
});

משך החיים של הניוד

היציאות מיועדות כשיטת תקשורת דו-כיוונית בין חלקים שונים של התוסף, כאשר פריים (ברמה העליונה) נחשב לחלק הקטן ביותר. כשמתקשרים אל 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 ציבורי שתוספים אחרים יכולים להשתמש בו.

ההאזנה לבקשות ולחיבורים נכנסים דומה לתרחיש הפנימי, רק שמשתמשים ב-method‏ runtime.onMessageExternal או ב-method‏ runtime.onConnectExternal. דוגמה לכל אחד מהם:

// For simple requests:
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.
  });
});

באופן דומה, שליחת הודעה לתוסף אחר דומה לשליחת הודעה בתוך התוסף. ההבדל היחיד הוא שצריך להעביר את המזהה של התוסף שאיתו רוצים לתקשר. לדוגמה:

// The ID of the extension we want to talk to.
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// Make a simple request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
  }
);

// Start a long-running conversation:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

שליחת הודעות מדפי אינטרנט

בדומה להעברת הודעות בין תוספים, האפליקציה או התוסף יכולים לקבל הודעות מדפי אינטרנט רגילים ולהשיב להן. כדי להשתמש בתכונה הזו, צריך קודם לציין בקובץ manifest.json את האתרים שרוצים לתקשר איתם. לדוגמה:

"externally_connectable": {
  "matches": ["*://*.example.com/*"]
}

הפעולה הזו תחשוף את ה-API של המרכז להודעות לכל דף שתואם לתבניות כתובות ה-URL שציינתם. תבנית כתובת ה-URL חייבת להכיל לפחות דומיין ברמה השנייה – כלומר, אסור להשתמש בתבניות של שמות מארחים כמו '*',‏ '*.com',‏ '*.co.uk' ו-'*.appspot.com'. בדף האינטרנט, משתמשים בממשקי ה-API‏ runtime.sendMessage או runtime.connect כדי לשלוח הודעה לאפליקציה או לתוסף ספציפיים. לדוגמה:

// 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, בדומה להעברת הודעות בין תוספים. רק דף האינטרנט יכול ליזום חיבור. לדוגמה:

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)

כשמקבלים הודעה מסקריפט תוכן או מתוסף אחר, הסקריפטים צריכים להיזהר שלא ליפול קורבן לפרצת אבטחה XSS‏ (cross-site scripting). ההמלצה הזו רלוונטית לסקריפטים שפועלים בדף הרקע של התוסף, וגם לסקריפטים של תוכן שפועלים במקורות אינטרנט אחרים. ספציפית, צריך להימנע משימוש בממשקי API מסוכנים כמו אלה שמופיעים בהמשך:

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be evaluating an evil script!
  var resp = eval("(" + response.farewell + ")");
});
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be injecting a malicious script!
  document.getElementById("resp").innerHTML = response.farewell;
});

במקום זאת, מומלץ להשתמש בממשקי API בטוחים יותר שלא מריצים סקריפטים:

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // JSON.parse does not evaluate the attacker's scripts.
  var resp = JSON.parse(response.farewell);
});
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // innerText does not let the attacker inject HTML elements.
  document.getElementById("resp").innerText = response.farewell;
});

דוגמאות

דוגמאות פשוטות לתקשורת באמצעות הודעות אפשר למצוא בספרייה examples/api/messaging. בדוגמה של העברת הודעות בין אפליקציות נייטיב אפשר לראות איך אפליקציית Chrome יכולה לתקשר עם אפליקציית נייטיב. דוגמאות נוספות ועזרה בצפייה בקוד המקור זמינות במאמר דוגמאות.