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

в 6:22, , рубрики: расширения, Расширения для браузеров

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

Основные функции:
— Обмен сообщениями с фоновой страницей и возможность отправить ответ;
— Единое хранилище на всех страницах.

Введение

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

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

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

Как работает обмен сообщений

Обмен сообщениями, как уже упоминал, почти как у Chrome, но с не большими изменениями.
Универсальный обмен сообщениями между страницами в расширениях - 1
На схеме изображен механизм взаимодействия страниц расширения между собой.

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

Автор: feverqwe

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js