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