И да, чуть не забыл, она сильно облегчает портирование расширений из 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 из тестового расширения:
(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.
Для этого в начало страницы добавляем следующее:
(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 в нескольких своих расширениях и она стала для меня незаменимой.
Спасибо что дочитали!
ссылка на оригинал статьи http://habrahabr.ru/post/246351/
Добавить комментарий