Универсальный обмен сообщениями между страницами в расширениях

от автора

Привет! Сегодня мне хочется показать вам свой маленьких хобби проект, который позволяет сильно упростить разработку расширений в разных браузерах. Сразу хочу предупредить, это не фреймворк который делает везде одно и то же, это библиотека, которая организует единый способ общения между всеми страницами расширения, и для её использования нужно хотя бы в общих чертах понимать работу api браузеров под которое вы пишите.
И да, чуть не забыл, она сильно облегчает портирование расширений из Chrome!

Основные функции:
— Обмен сообщениями с фоновой страницей и возможность отправить ответ;
— Единое хранилище на всех страницах.

Введение

Когда я столкнулся с потребностью портирования расширения на все актуальные браузеры, то обнаружил, что везде все по разному. И что бы использовать единой код, придется написать легкую обертку, которая унифицирует взаимодействие с хранилищем и страницами.

Мне очень хотелось привести все к подобию api хрома. Очень удобно посылать сообщения в фоновую страницу и иметь возможность ответить. Удобно когда есть единое хранилище везде и его можно вызвать из любой страницы.

В общем именно об этой унификации и пойдет речь.

Как работает обмен сообщений

Обмен сообщениями, как уже упоминал, почти как у Chrome, но с не большими изменениями.

На схеме изображен механизм взаимодействия страниц расширения между собой.

Injected page — страница, на которой подключен скрипт расширения, может отсылать сообщения только фоновой странице и получать ответ только через response функцию.

Popup page — всплывающая страница, может посылать сообщения только в фоновую страницу.

Options page — страница настроек расширения, т.е. html страница внутри расширения, открывается при нажатии на пункт настройки (в Chrome например), может отсылать сообщения только в фоновую страницу.

Background page — фоновая страница расширения, когда отсылает сообщение — сообщение приходит сразу и в popup menu, и в options page. Но не приходит в Injected page, но может отсылать сообщения в активную вкладку.
*В Firefox посылка из фоновой страницы в popup menu и options page, включается отдельным флагом, т.к. эта функция почти не нужна.

Так же замечу, что в Safari и Firefox, popup page загружается один раз и работает постоянно, в то время как в Chrome и Opera 12 происходит загрузка страницы при нажатии на кнопку расширения.

*В Firefox нельзя посылать сообщения в закрытую/не активную страницу.

Код получения сообщения:

mono.onMessage(function onMessage(message, response) {   console.log(message);   response("> "+message); }); 

Код посылки сообщения:

mono.sendMessage("message", function onResponse(message) {   console.log(message); }); 

Код посылки сообщений в активную вкладку (только из фоновой страницы):

mono.sendMessageToActiveTab("message", function onResponse(message) {   console.log(message); }); 

В общем все максимально похоже на Chrome.

Хранилище

Во всех браузерах хранилище разное.
Firefox: simple-storage.
Opera: widget.preferences, localStorage.
Chrome: chrome.storage.local, chrome.storage.sync, localStorage.
Safari: localStorage.

Библиотека унифицирует интерфейс работы с хранилищем.

Код работы с хранилищем:

mono.storage.set({a:1}, function onSet(){   console.log("Dune!"); }); mono.storage.get("a", function onGet(storage){   console.log(storage.a); }); mono.storage.clear(); 

Для использования sync хранилища хрома, код выглядит немного иначе, а в остальных браузерах будет использоваться локальное хранилище.

mono.storage.sync.set({a:1}, function onSet(){   console.log("Dune!"); }); mono.storage.sync.get("a", function onGet(storage){   console.log(storage.a); }); mono.storage.sync.clear(); 
Как оно работает:

Работает хранилище следующим образом:

браузер\страница background options popup Injected
Chrome localStorage localStorage via messages
Opera 12 (localStorage)
Safari
Chrome (storage) chrome.storage
Firefox Simple storage Simple storage via messages
Opera 12 widget.preferences

В таблице всё, что с приставкой «via messages» означает, что хранилище работает через посылку сервисных сообщений к фоновой странице, разумеется фоновая страница должна слушать входящие сообщения. В иных случаях работа с хранилищем идет напрямую.

Подключение к расширению

Chrome, Safari, Opera 12
Нужно подключить mono.js на каждую страницу расширения.

Firefox (Addons-sdk only)
Тут все немного сложнее, нужно знать как работает Addons-sdk.
В lib/main.js нужно через require подключить файл monoLib.js и уже к ней подключать все остальные страницы, а так же background.js (т.е. фоновую страницу).

Я приведу пример main.js из тестового расширения:

main.js

(function() {     var monoLib = require("./monoLib.js");     var ToggleButton = require('sdk/ui/button/toggle').ToggleButton;     var panels = require("sdk/panel");     var self = require("sdk/self");      // говорим, что при нажатии на кнопку settingsBtn в настройках - открывать options.html     var simplePrefs = require("sdk/simple-prefs");     simplePrefs.on("settingsBtn", function() {         var tabs = require("sdk/tabs");         tabs.open( self.data.url('options.html') );     });      // подключаем виртуальный port к странице, т.к. options.html уже содержит mono.js     var pageMod = require("sdk/page-mod");     pageMod.PageMod({         include: [             self.data.url('options.html')         ],         contentScript: '('+monoLib.virtualPort.toString()+')()',         contentScriptWhen: 'start',         onAttach: function(tab) {             monoLib.addPage(tab);         }     });      // подключаем библиотеку к injected page     pageMod.PageMod({         include: [             'http://example.com/*',             'https://example.com/*'         ],         contentScriptFile: [           self.data.url("js/mono.js"),           self.data.url("js/inject.js")         ],         contentScriptWhen: 'start',         onAttach: function(tab) {             monoLib.addPage(tab);         }     });      // добавляем кнопку на панель браузера     var button = ToggleButton({         id: "monoTestBtn",         label: "Mono test!",         icon: {             "16": "./icons/icon-16.png"         },         onChange: function (state) {             if (!state.checked) {                 return;             }             popup.show({                 position: button             });         }     });      // добавляем к кнопке попап     var popup = panels.Panel({         width: 400,         height: 250,         contentURL: self.data.url("popup.html"),         onHide: function () {             button.state('window', {checked: false});         }     });     // добавляем попап к monoLib *прошу заметить, что именно так, а не через onAttach     monoLib.addPage(popup);     // создаем виртуальный addon для фоновой страницы     var backgroundPageAddon = monoLib.virtualAddon();     // добавляем фоновую страницу в monoLib     monoLib.addPage(backgroundPageAddon);     // подключаем фоновую страницу, как модуль     var backgroundPage = require("./background.js");     // отдаем виртуальный addon фоновой странице     backgroundPage.init(backgroundPageAddon); })(); 

Но увы и это ещё не всё. Наша общая страница background.js должна уметь работать и в режиме модуля. И нужно подключить туда mono.js.

Для этого в начало страницы добавляем следующее:

background.js

(function() {     // проверяем модуль ли это     if (typeof window !== 'undefined') return;     // добавляем window (не обязательно)     window = require('sdk/window/utils').getMostRecentBrowserWindow();     // на всякий случай добавляем флаг, что это модуль     window.isModule = true;     var self = require('sdk/self');     // подключаем библиотеку из директории data/js     mono = require('toolkit/loader').main(require('toolkit/loader').Loader({         paths: {             'data/': self.data.url('js/')         },         name: self.name,         prefixURI: self.data.url().match(/([^:]+:\/\/[^/]+\/)/)[1],         globals: {             console: console,             _require: function(path) {                 // описываем все require которые нужны mono.js                 switch (path) {                     case 'sdk/simple-storage':                         return require('sdk/simple-storage');                     case 'sdk/window/utils':                         return require('sdk/window/utils');                     case 'sdk/self':                         return require('sdk/self');                     default:                         console.log('Module not found!', path);                 }             }         }     }), "data/mono"); })(); var init = function(addon) {     if (addon) {         mono = mono.init(addon);     }     console.log("Background page ready!"); } if (window.isModule) {     // если модуль, объявляем init метод.     exports.init = init; } else {     // если не модуль - стартуем     init(); } 

После того, как выполнится функция init, далее уже можно запускать всё остальное, что зависит от mono.

*замечание, в режиме модуля в scope даже нету window, поэтому все нужно подключать отдельно.

Костыли

Для того, что бы использовать нативный api в каждом браузере нужны способы их идентификации.
Библиотека предоставляет следующий список переменных.

  • mono.isFF — текущий браузер Firefox;
    • mono.isModule — текущая страница — модуль;
  • mono.isGM — запущено в GreaseMonkey подобной среде;
    • mono.isTM — запущено в Tampermonkey;
  • mono.isChrome — расширение работает в Chrome;
    • mono.isChromeApp — определено что это chrome приложение;
    • mono.isChromeWebApp — определено что это chrome “приложение” (ранняя версия хром приложений);
    • mono.isChromeInject — определено что скрипт подключен к странице;
  • mono.isSafari — браузер Safari;
    • mono.isSafariPopup — запущено в popup окне;
    • mono.isSafariBgPage — запущено в фоновой странице;
    • mono.isSafariInject — запущено в подключаемой странице;
  • mono.isOpera — запущено в Opera 12;
    • mono.isOperaInject — скрипт подключен к странице.

Вот по этим флагам можно и выбирать какой api дергать в браузере.

Утилиты в Firefox

В Firefox любая страница (если она не модуль, т.е. фоновая страница) единственное что может это отсылать сообщения. Поэтому добавил некоторое количество сервисов, которые мне пригодились.

Посылка сообщений в popup окно:

mono.sendMessage('Hi', function onResponse(message){   console.log("response: "+message); }, "popupWin"); 

Изменение размера всплывающей страницы:

mono.sendMessage({action: "resize", width: 300, height: 300}, null, "service"); 

Открытие новой вкладки:

mono.sendMessage({action: "openTab", url: "http://.../"}, null, "service"); 

В общем то если взгляните на код, уверен, у вас не составит труда добавлять свои “сервисы” для удобства взаимодействия с API.

Сборка

Библиотека для удобства разбита на несколько файлов. Собирается всё с помощью Ant, файл сборки лежит в “/src/vendor/Ant”. В нем можно убрать не нужные вами браузеры.

Заключение

Вот такая незамысловатая библиотечка. Конечно у ней всяко есть какие нибудь баги и недочеты. Но вроде бы работает. Уверен что у вас не составит большого труда разобраться в коде и где нужно что нужно подпилить под себя.
Если вам показалось все это слишком сложным, в гите есть пример простенького расширения, которое собирается для Chrome, Opera 12, Safari, Firefox. Я использую mono в нескольких своих расширениях и она стала для меня незаменимой.

Спасибо что дочитали!

GitHub

ссылка на оригинал статьи http://habrahabr.ru/post/246351/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *