...

воскресенье, 21 декабря 2014 г.

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

Привет! Сегодня мне хочется показать вам свой маленьких хобби проект, который позволяет сильно упростить разработку расширений в разных браузерах. Сразу хочу предупредить, это не фреймворк который делает везде одно и то же, это библиотека, которая организует единый способ общения между всеми страницами расширения, и для её использования нужно хотя бы в общих чертах понимать работу 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();


Как оно работает:



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



































браузер\страницаbackgroundoptionspopupInjected
ChromelocalStoragelocalStorage via messages
Opera 12 (localStorage)
Safari
Chrome (storage)chrome.storage
FirefoxSimple storageSimple storage via messages
Opera 12widget.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


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Want something else to read? How about 'Grievous Censorship' By The Guardian: Israel, Gaza And The Termination Of Nafeez Ahmed's Blog


Комментариев нет:

Отправить комментарий