Універсальний обмін повідомленнями між сторінками в розширеннях

Привіт! Сьогодні мені хочеться показати вам свій маленьких хобі проект, який дозволяє сильно спростити розробку розширень в різних браузерах. Відразу хочу попередити, що це не фреймворк який робить скрізь одне і те ж, це бібліотека, яка організує єдиний спосіб спілкування між усіма сторінками розширення, і для її використання потрібно хоча б у загальних рисах розуміти роботу 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 — запущено в all-пн-ключ подібної середовищі;
    • 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/виробника/Ant". В ньому можна прибрати не потрібні вами браузери.

Висновок

Ось така нехитра бібліотечка. Звичайно у ній всяко є які небудь баги і недоліки. Але начебто працює. Впевнений що у вас не складе великої праці розібратися в коді і де потрібно що треба підпиляти під себе.
Якщо вам здалося все це занадто складним, в гіті є приклад найпростішого розширення, яке збирається для Chrome, Opera 12, Safari, Firefox. Я використовую mono в кількох своїх розширення і вона стала для мене незамінною.

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

GitHub

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.