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

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

    Подробнее
    Реклама

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое