пятница, 21 февраля 2014 г.

Обмен сообщениями

Поскольку контентный скрипт работает в контексте веб-страницы, а не расширения, он должен как-то обмениваться данными с основной частью расширения. Например расширение для чтения RSS должно определять наличие RSS на странице с помощью контентного скрипта и уведомлять об этом фоновую страницу чтобы та в свою очередь добавила иконку page action. Как же это сделать?
Есть два варианта – простой API для однократной передачи сообщений (самое-то для рассмотренного примера) и более сложный, позволяющий создать постоянный канал обмена сообщениями.

Простая передача сообщений

Если нужно просто отправить сообщение и, опционально, получить ответ, используется вызов методов runtime.sendMessage или tabs.sendMessage.
Из контентного скрипта фоновой странице:
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);
  });
});

На принимающей стороне должен быть установлен обработчик 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"});
  });

Примечание: если много страниц имеют обработчики runtime.onMessage и в них не предусмотрен механизм отделения “своих” сообщений от “чужих”, то это кончится плохо – отработают все обработчики, но только первый из них успешно отправит ответ. Остальные обломаются.

Постоянное соединение


Иногда сообщениями нужно обмениваться постоянно. Чтобы не создавать лишнюю нагрузку на процессор – каждое сообщение вызывает активацию обработчиков на всех страницах, есть другой механизм. Для этого используются методы runtime.connect и tabs.connect
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"});
});

И на другой стороне:
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."});
  });
});

Таким образом создается именованный канал и все последующие сообщения передаются в рамках этого канала. Напомню – пока канал открыт, фоновая страница не может быть деактивирована.Так что если сообщения передаются редко, то лучше все же использовать первый механизм. А когда необходимость в канале отпала – закрывать его вызовом runtime.Port.disconnect. Этот метод может быть вызван с любой стороны, при этом на другой стороне возникает событие runtime.Port.onDisconnect.

Обмен сообщениями между расширениями


Кроме обмена сообщениями между компонентами вашего расширения, можно обмениваться сообщениями и с другими расширениями. Для приема таких сообщений есть события runtime.onMessageExternal и runtime.onConnectExternal, соответственно для разовых сообщений и для создания постоянного канала:
// For simple requests:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id == blacklistedExtension)
      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(...);

Если я ничего не путаю, идентификатор расширения фиксируется при его загрузке в Google Store. Значит для расширений не опубликованных там возможность использования данного механизма под вопросом.

Отправка сообщений с веб-страницы


Подобным же образом расширение может получать сообщения от веб-страницы. Для этого нужно прописать такую возможность в манифесте:
"externally_connectable": {
  "matches": ["*://*.example.com/*"]
}

Чтобы обмениваться сообщениями с расширением веб страница должна знать его ID:
// 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);
  });

А расширение принимает их через тот же интерфейс, что и от других расширений:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.url == blacklistedWebsite)
      return;  // don't allow this web page access
    if (request.openUrlInEditor)
      openUrl(request.openUrlInEditor);
  });


Примечание: как быть с тем, что нам заранее не известен ID расширения (жаба душит отдавать 5$ за регистрацию на ГуглСтор)? Можно передать странице ID расширения контентным скриптом, через DOM.

Обмен сообщениями с нативными приложениями


Нативное приложение должно зарегистрировать соответствующую возможность (native messaging host) и указать с какими расширениями оно может работать:
{
  "name": "com.my_company.my_application",
  "description": "My Application",
  "path": "C:\\Program Files\\My Application\\chrome_native_messaging_host.exe",
  "type": "stdio",
  "allowed_origins": [
    "chrome-extension://knldjmfmopnpolahpmmgbagdohdnhkik/"
  ]
}

Манифест приложения должен содержать следующие параметры:
ИмяОписание
nameИмя хоста. Клиентское расширение передает это значение в методы runtime.connectNative или runtime.sendNativeMessage.
descriptionКраткое описание приложения
pathПуть к бинарному файлу приложения. В системах Linux and OSX путь должен быть абсолютным. На Windows все не как у людей – путь может быть относительным, указывать месторасположение бинарного файла относительно каталога где лежит манифест.
typeТип интерфейса для обмена сообщениями. На текущий момент он единственный: stdio.
allowed_originsСписок ID расширений которым дозволено работать с этим приложением. Фактически это означает что нельзя предоставить публичный интерфейс приложения всем и каждому. Разработчик приложения должен явно предоставить доступ к своему приложению разработчику расширения.

Местонахождение манифеста зависит от платформы:


  • Windows: где угодно. Приложение должно создать ключ реестра HKEY_LOCAL_MACHINE\SOFTWARE\Google\Chrome\NativeMessagingHosts\com.my_company.my_application или HKEY_CURRENT_USER\SOFTWARE\Google\Chrome\NativeMessagingHosts\com.my_company.my_application, и установить в значении ключа по умолчанию абсолютный путь к файлу манифеста.
  • OSX: /Library/Google/Chrome/NativeMessagingHosts/com.my_company.my_application.json, или ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.my_company.my_application.json
  • Linux: /etc/opt/chrome/native-messaging-hosts/com.my_company.my_application.json или ~/.config/chrome/NativeMessagingHosts/com.my_company.my_application.json.

Chrome запускает каждый нативный хост в отдельном процессе и взаимодействует с ним через стандартный ввод/вывод. Сообщения сериализуются в формате JSON, кодировка UTF-8, в начале сообщения 32битная длина, порядок байтов соответствует платформе.

Когда используется runtime.connectNative, Chrome запускает нативный хост и держит его активным пока соединение не будет закрыто. Если используется runtime.sendNativeMessage, то браузер запускает новый процесс для каждого сообщения. От хоста в этом случае ожидается ответ на исходное сообщение, а все последующие сообщения, если таковые будут – игнорируются.

Пример использования runtime.connectNative:
var port = chrome.runtime.connectNative('com.my_company.my_application');
port.onMessage.addListener(function(msg) {
  console.log("Received" + msg);
});
port.onDisconnect.addListener(function() {
  console.log("Disconnected");
});
port.postMessage({ text: "Hello, my_application" });

Пример использования runtime.sendNativeMessage:
chrome.runtime.sendNativeMessage('com.my_company.my_application',
  { text: "Hello" },
  function(response) {
    console.log("Received " + response);
  });

13 комментариев:

  1. не пашет ваш обмен сообщениями

    ОтветитьУдалить
  2. Мне ошибку на принимающей стороне сайта выдает.

    ОтветитьУдалить
  3. Uncaught TypeError: Cannot read property 'addListener' of undefinedai_on вот такую, в чем может быть причина?

    ОтветитьУдалить
    Ответы
    1. Скорее всего разрешений не хватает в manifest.json

      Удалить
  4. Этот комментарий был удален автором.

    ОтветитьУдалить
  5. Спасибо. Теперь яснее стало. Дока какая-то мутная по обработчикам на сайте.

    ОтветитьУдалить
  6. Could not establish connection. Receiving end does not exist.
    В чём может быть ошибка, в externally_connectable url неправильный или id не подходит?

    ОтветитьУдалить
    Ответы
    1. id меняется при установке распакованного расширения на разных рабочих машинах

      Удалить
  7. Этот комментарий был удален автором.

    ОтветитьУдалить
  8. такая же фигня
    var port = browsers.runtime.chromePort = chrome.runtime.connect({name: "bus"});
    port.onMessage.addListener(function(a) {...});
    ...
    var d = browsers.runtime.chromePort;
    d.postMessage(b)
    Ошибка extensions::messaging:78 Uncaught Error: Attempting to use a disconnected port object
    почему то порт закрывается

    ОтветитьУдалить
  9. очень круто всё работает узнал много нового,спс автору огромное!!

    ОтветитьУдалить


  10. Постоянно пользовался отсылкой сообщений на фоновую страницу, но вот в первый раз понадобилось использовать коллбэк. Код просто примитивнейший:

    chrome.extension.sendMessage({ тут параметры },
    function(backMessage){
    console.log("in callback: backMessage: ", backMessage);
    window.alert('Возвращено: ' + backMessage);
    }
    );




    В фоновой странице:
    chrome.extension.onMessage.addListener(function(request, sender, callback) {
    // Тут творятся тёмные дела
    if(callback) callback(counter);
    }

    И стабильно получаю ошибку:
    Unchecked runtime.lastError: The message port closed before a response was received.

    Кто виноват, и что делать?

    ОтветитьУдалить
  11. А в принципе, пофиг, заменил вызов коллбэка в фоновой странице на:

    chrome.tabs.sendRequest(sender.tab.id,{ count: counter }); //запрос на сообщение

    добавил обработчик на вызывающую страницу, и все завертелось.

    ОтветитьУдалить