ارسال پیام

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) {
  fetch(message.url)
    .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({
  url: 'https://example.com'
});

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

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

اگر چندین صفحه در حال گوش دادن به رویدادهای onMessage باشند، تنها اولین صفحه‌ای که تابع sendResponse() را برای یک رویداد خاص فراخوانی کند، موفق به ارسال پاسخ خواهد شد. سایر پاسخ‌ها به آن رویداد نادیده گرفته می‌شوند.

اتصالات طولانی مدت

برای ایجاد یک کانال ارسال پیام با طول عمر بالا و قابل استفاده مجدد، از دستور زیر استفاده کنید:

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