ارسال پیام

APIهای پیام‌رسان به شما امکان می‌دهند بین اسکریپت‌های مختلفی که در زمینه‌های مرتبط با افزونه شما اجرا می‌شوند، ارتباط برقرار کنید. این شامل ارتباط بین سرویس ورکر، chrome-extension://pages و اسکریپت‌های محتوا می‌شود. به عنوان مثال، یک افزونه RSS خوان ممکن است از اسکریپت‌های محتوا برای تشخیص وجود یک فید RSS در یک صفحه استفاده کند، سپس به سرویس ورکر اطلاع دهد که آیکون اکشن را برای آن صفحه به‌روزرسانی کند.

دو API برای ارسال پیام وجود دارد: یکی برای درخواست‌های یک‌باره و دیگری پیچیده‌تر برای اتصالات طولانی‌مدت که امکان ارسال چندین پیام را فراهم می‌کنند.

برای اطلاعات بیشتر در مورد ارسال پیام بین داخلی‌ها، به بخش پیام‌های بین داخلی‌ها مراجعه کنید.

درخواست‌های یک‌بار مصرف

برای ارسال یک پیام واحد به بخش دیگری از افزونه خود و دریافت اختیاری پاسخ، runtime.sendMessage() یا tabs.sendMessage() را فراخوانی کنید. این متدها به شما امکان می‌دهند یک پیام JSON-serializable یک‌بار مصرف را از یک اسکریپت محتوا به افزونه یا از افزونه به یک اسکریپت محتوا ارسال کنید. هر دو 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');

وقتی شنونده‌ی رویداد فراخوانی می‌شود، یک تابع sendResponse به عنوان پارامتر سوم ارسال می‌شود. این تابعی است که می‌تواند برای ارائه پاسخ فراخوانی شود. به طور پیش‌فرض، تابع فراخوانی sendResponse باید به صورت همزمان فراخوانی شود.

اگر تابع sendResponse بدون هیچ پارامتری فراخوانی کنید، null به عنوان پاسخ ارسال می‌شود.

برای ارسال پاسخ به صورت غیرهمزمان، دو گزینه دارید: برگرداندن true یا برگرداندن یک promise.

true را برگردانید

برای پاسخ دادن ناهمگام با استفاده از sendResponse() ، یک مقدار true (نه فقط یک مقدار truthy) را از شنونده رویداد برگردانید. انجام این کار کانال پیام را تا زمانی که sendResponse فراخوانی شود، به سمت دیگر باز نگه می‌دارد و به شما امکان می‌دهد بعداً آن را فراخوانی کنید.

وعده‌ای را پس بگیرید

از کروم ۱۴۴، می‌توانید یک promise را از یک شنونده پیام برای پاسخ ناهمگام برگردانید. اگر promise حل شود، مقدار حل شده آن به عنوان پاسخ ارسال می‌شود.

اگر promise رد شود، فراخوانی sendMessage() فرستنده به همراه پیام خطا رد خواهد شد. برای جزئیات و مثال‌های بیشتر به بخش مدیریت خطا مراجعه کنید.

مثالی که بازگشت یک promise را نشان می‌دهد که می‌تواند 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);

همچنین می‌توانید یک شنونده (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 به عنوان شنونده، حتی بدون دستور return ، همیشه یک promise را برمی‌گرداند. اگر یک شنونده 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 = 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');
  });
});

مدیریت خطا

از کروم ۱۴۴، اگر یک شنونده‌ی onMessage خطایی (چه به صورت همزمان و چه به صورت غیرهمزمان با برگرداندن یک promise که رد می‌کند) ایجاد کند، promise برگردانده شده توسط sendMessage() در فرستنده به همراه پیام خطا رد می‌شود. این اتفاق همچنین می‌تواند رخ دهد اگر یک شنونده سعی کند پاسخی را برگرداند که نمی‌تواند بدون گرفتن TypeError حاصل ، به صورت JSON سریالی شود.

اگر شنونده‌ای، promiseای را برگرداند که رد می‌شود، باید آن را با یک نمونه Error رد کند تا فرستنده بتواند آن پیام خطا را دریافت کند. اگر promise با هر مقدار دیگری (مانند null یا undefined ) رد شود، sendMessage() به جای آن با یک پیام خطای عمومی رد خواهد شد.

اگر چندین شنونده برای onMessage ثبت شده باشند، فقط اولین شنونده‌ای که پاسخ دهد، رد کند یا خطایی ایجاد کند، فرستنده را تحت تأثیر قرار می‌دهد؛ همه شنونده‌های دیگر اجرا می‌شوند، اما نتایج آنها نادیده گرفته می‌شود.

مثال‌ها

اگر یک شنونده، 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'));
});

اگر شنونده‌ای با مقداری که قابل سریال‌سازی نیست پاسخ دهد، 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() برای انتقال پیام‌ها از یک صفحه افزونه به یک اسکریپت محتوا.

شما می‌توانید کانال خود را با ارسال یک پارامتر options به همراه یک کلید 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 تنظیم کنید. وقتی بخش دیگری از افزونه شما connect() را فراخوانی می‌کند، این رویداد و شیء runtime.Port را فعال می‌کند. کد پاسخ به اتصالات ورودی به این شکل است:

سرویس-ورکر.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."});
    }
  });
});

سریال‌سازی

در کروم، APIهای ارسال پیام از سریال‌سازی JSON استفاده می‌کنند. نکته قابل توجه این است که این با سایر مرورگرهایی که APIهای مشابه را با الگوریتم کلون ساختاریافته پیاده‌سازی می‌کنند، متفاوت است.

این یعنی یک پیام (و پاسخ‌های ارائه شده توسط گیرندگان) می‌تواند شامل هر مقدار معتبر JSON.stringify() باشد. سایر مقادیر به صورت مقادیر قابل سریال‌سازی درخواهند آمد (به ویژه undefined که به صورت null سریال‌سازی می‌شود)؛

محدودیت‌های اندازه پیام

حداکثر حجم یک پیام ۶۴ مگابایت است.

طول عمر پورت

پورت‌ها به عنوان یک مکانیزم ارتباطی دو طرفه بین بخش‌های مختلف یک افزونه طراحی شده‌اند. وقتی بخشی از یک افزونه، tabs.connect() ، runtime.connect() یا runtime.connectNative() را فراخوانی می‌کند، یک پورت ایجاد می‌کند که می‌تواند بلافاصله با استفاده از postMessage() پیام‌ها را ارسال کند.

اگر چندین فریم در یک تب وجود داشته باشد، فراخوانی tabs.connect() رویداد runtime.onConnect را برای هر فریم در تب یک بار فراخوانی می‌کند. به طور مشابه، اگر runtime.connect() فراخوانی شود، رویداد onConnect می‌تواند برای هر فریم در فرآیند توسعه، یک بار اجرا شود.

شاید بخواهید بفهمید چه زمانی یک اتصال بسته می‌شود، مثلاً اگر برای هر پورت باز، حالت‌های جداگانه‌ای را حفظ می‌کنید. برای انجام این کار، به رویداد runtime.Port.onDisconnect گوش دهید. این رویداد زمانی رخ می‌دهد که هیچ پورت معتبری در انتهای دیگر کانال وجود نداشته باشد، که می‌تواند هر یک از دلایل زیر را داشته باشد:

  • در انتهای دیگر هیچ شنونده‌ای برای runtime.onConnect وجود ندارد.
  • زبانه حاوی پورت تخلیه می‌شود (برای مثال، اگر زبانه پیمایش شود).
  • فریمی که connect() در آن فراخوانی شده بود، تخلیه شده است.
  • تمام فریم‌هایی که پورت را دریافت کرده‌اند (از طریق runtime.onConnect ) تخلیه شده‌اند.
  • تابع runtime.Port.disconnect() توسط طرف دیگر فراخوانی می‌شود. اگر فراخوانی connect() منجر به ایجاد چندین پورت در طرف گیرنده شود و disconnect() روی هر یک از این پورت‌ها فراخوانی شود، آنگاه رویداد onDisconnect فقط در پورت فرستنده فعال می‌شود، نه در پورت‌های دیگر.

پیام‌رسانی بین افزونه‌ها

علاوه بر ارسال پیام بین اجزای مختلف در افزونه خود، می‌توانید از API پیام‌رسانی برای ارتباط با سایر افزونه‌ها استفاده کنید. این به شما امکان می‌دهد یک API عمومی را برای استفاده سایر افزونه‌ها در معرض نمایش قرار دهید.

برای گوش دادن به درخواست‌ها و اتصالات ورودی از سایر افزونه‌ها، از متدهای runtime.onMessageExternal یا runtime.onConnectExternal استفاده کنید. در اینجا مثالی از هر کدام آورده شده است:

سرویس-ورکر.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.
  });
});

برای ارسال پیام به داخلی دیگر، شناسه داخلی که می‌خواهید با آن ارتباط برقرار کنید را به صورت زیر ارسال کنید:

سرویس-ورکر.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" مشخص کنید که می‌خواهید از کدام وب‌سایت‌ها پیام‌ها را دریافت کنید. برای مثال:

مانیفست.json

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

این کار API پیام‌رسانی را در معرض هر صفحه‌ای قرار می‌دهد که با الگوهای URL مشخص‌شده توسط شما مطابقت داشته باشد. الگوی URL باید حداقل شامل یک دامنه سطح دوم باشد؛ یعنی الگوهای نام میزبان مانند "*"، "*.com"، "*.co.uk" و "*.appspot.com" پشتیبانی نمی‌شوند. می‌توانید <all_urls> برای دسترسی به همه دامنه‌ها استفاده کنید.

برای ارسال پیام به یک داخلی خاص، از APIهای runtime.sendMessage() یا runtime.connect() استفاده کنید. برای مثال:

صفحه وب.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 مانند پیام‌رسانی بین افزونه‌ای، به پیام‌های صفحات وب گوش دهید. در اینجا مثالی آورده شده است:

سرویس-ورکر.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);
  });

ارسال پیام از یک افزونه به یک صفحه وب امکان‌پذیر نیست.

پیام‌رسانی بومی

افزونه‌ها می‌توانند با برنامه‌های بومی که به عنوان میزبان پیام‌رسانی بومی ثبت شده‌اند، پیام رد و بدل کنند. برای کسب اطلاعات بیشتر در مورد این ویژگی، به پیام‌رسانی بومی مراجعه کنید.

ملاحظات امنیتی

در اینجا چند نکته امنیتی مربوط به پیام رسانی آورده شده است.

اسکریپت‌های محتوا کمتر قابل اعتماد هستند

اسکریپت‌های محتوا نسبت به کارگر سرویس افزونه، کمتر قابل اعتماد هستند . برای مثال، یک صفحه وب مخرب ممکن است بتواند فرآیند رندرینگی که اسکریپت‌های محتوا را اجرا می‌کند، به خطر بیندازد. فرض کنید که پیام‌های یک اسکریپت محتوا ممکن است توسط یک مهاجم ساخته شده باشند و مطمئن شوید که تمام ورودی‌ها را اعتبارسنجی و پاکسازی می‌کنید . فرض کنید که هر داده‌ای که به اسکریپت محتوا ارسال می‌شود، ممکن است به صفحه وب نشت کند. دامنه اقدامات دارای امتیازی را که می‌تواند توسط پیام‌های دریافتی از اسکریپت‌های محتوا ایجاد شود، محدود کنید.

اسکریپت نویسی بین سایتی

مطمئن شوید که اسکریپت‌های خود را در برابر اسکریپت‌نویسی بین‌سایتی محافظت می‌کنید. هنگام دریافت داده‌ها از یک منبع غیرقابل اعتماد مانند ورودی کاربر، وب‌سایت‌های دیگر از طریق یک اسکریپت محتوا یا یک API، مراقب باشید که این را به عنوان HTML تفسیر نکنید یا از آن به روشی استفاده نکنید که می‌تواند باعث اجرای کد غیرمنتظره شود.

روش‌های ایمن‌تر

تا حد امکان از APIهایی استفاده کنید که اسکریپت اجرا نمی‌کنند:

سرویس-ورکر.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);
});

سرویس-ورکر.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;
});
روش‌های ناامن

از روش‌های زیر که افزونه شما را آسیب‌پذیر می‌کنند، استفاده نکنید:

سرویس-ورکر.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be evaluating a malicious script!
  const resp = eval(`(${response.farewell})`);
});

سرویس-ورکر.js

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