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

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

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

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

Как работает обмен сообщений Обмен сообщениями, как уже упоминал, почти как у Chrome, но с не большими изменениями.beb54936dcbd46b4b6f1ab1786ff538a.pngНа схеме изображен механизм взаимодействия страниц расширения между собой.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

© Habrahabr.ru