Universal messaging between pages in extensions

    Hello! Today I want to show you my little hobby project, which makes it much easier to develop extensions in different browsers. I want to warn you right away, this is not a framework that does the same everywhere, it is a library that organizes a single way of communication between all extension pages, and to use it you need to at least in general terms understand the operation of the browser api for which you write.
    And yes, I almost forgot, it greatly facilitates the porting of extensions from Chrome!

    Key features:
    - Messaging with a background page and the ability to send a response;
    - A single repository on all pages.

    Introduction


    When I was faced with the need to port the extension to all current browsers, I found that everything is different everywhere. And in order to use a single code, you will have to write a lightweight wrapper that unifies interaction with storage and pages.

    I really wanted to bring everything to the likeness of chrome api. It is very convenient to send messages to the background page and be able to reply. It is convenient when there is a single repository everywhere and it can be called from any page.

    In general, it will be precisely this unification that will be discussed.

    How messaging works


    Messaging, as already mentioned, is almost like Chrome, but with no big changes.

    The diagram shows the mechanism for interaction between extension pages.

    Injected page - the page on which the extension script is connected can send messages only to the background page and receive a response only through the response function.

    Popup page - a popup page, can send messages only to the background page.

    Options page - extension settings page, i.e. The html page inside the extension, opens when you click on the settings item (in Chrome for example), can send messages only to the background page.

    Background page- background extension page, when sends a message - the message arrives immediately in the popup menu and in the options page. But it does not come to the Injected page, but it can send messages to the active tab.
    * In Firefox, sending from the background page to the popup menu and options page is enabled by a separate flag, because this function is almost not needed.

    I also note that in Safari and Firefox, the popup page loads once and works constantly, while in Chrome and Opera 12 the page loads when the extension button is clicked.

    * In Firefox, you cannot send messages to a closed / inactive page.

    Message receipt code:
    mono.onMessage(function onMessage(message, response) {
      console.log(message);
      response("> "+message);
    });
    

    Message sending code:
    mono.sendMessage("message", function onResponse(message) {
      console.log(message);
    });
    

    Code for sending messages to the active tab (only from the background page):
    mono.sendMessageToActiveTab("message", function onResponse(message) {
      console.log(message);
    });
    

    In general, everything is as similar to Chrome as possible.

    Storage


    In all browsers, the storage is different.
    Firefox: simple-storage.
    Opera: widget.preferences, localStorage.
    Chrome: chrome.storage.local, chrome.storage.sync, localStorage.
    Safari: localStorage.

    The library unifies the storage interface.

    Repository code:
    mono.storage.set({a:1}, function onSet(){
      console.log("Dune!");
    });
    mono.storage.get("a", function onGet(storage){
      console.log(storage.a);
    });
    mono.storage.clear();
    


    To use sync chrome storage, the code looks a little different, and in other browsers will use local storage.
    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();
    


    How does it work:

    The repository works as follows:
    browser \ pagebackgroundoptionspopupInjected
    ChromelocalStoragelocalStorage via messages
    Opera 12 (localStorage)
    Safari
    Chrome (storage)chrome.storage
    FirefoxSimple storageSimple storage via messages
    Opera 12widget.preferences

    In the table, everything with the “via messages” prefix means that the repository works by sending service messages to the background page, of course, the background page should listen to incoming messages. In other cases, work with storage is direct.

    Connection to extension


    Chrome, Safari, Opera 12
    You need to connect mono.js to each page of the extension.

    Firefox (Addons-sdk only)
    Everything is a little more complicated here, you need to know how Addons-sdk works.
    In lib / main.js, you need to connect the monoLib.js file through require and connect all the other pages to it, as well as background.js (i.e. the background page).

    I will give an example of main.js from a test extension:
    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);
    })();
    

    But alas, this is not all. Our general background.js page should be able to work in module mode. And you need to connect mono.js. there

    To do this, add the following to the top of the page:
    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();
    }
    

    After the init function is executed, then you can already run everything else that depends on mono.

    * Note, in module mode, scope does not even have a window, so everything needs to be connected separately.

    Crutches


    In order to use the native api in each browser, ways to identify them are needed.
    The library provides the following list of variables.

    • mono.isFF - current Firefox browser;
      • mono.isModule - current page - module;
    • mono.isGM - launched in a GreaseMonkey-like environment;
      • mono.isTM - launched in Tampermonkey;
    • mono.isChrome - extension works in Chrome;
      • mono.isChromeApp - it is determined that this is a chrome application;
      • mono.isChromeWebApp - it is determined that this is a chrome “application” (an early version of chrome applications);
      • mono.isChromeInject - it is determined that the script is connected to the page;
    • mono.isSafari - Safari browser;
      • mono.isSafariPopup - launched in a popup window;
      • mono.isSafariBgPage - launched in the background page;
      • mono.isSafariInject - launched in the connected page;
    • mono.isOpera - launched in Opera 12;
      • mono.isOperaInject - the script is connected to the page.

    Using these flags, you can choose which api to pull in the browser.

    Utilities in Firefox


    In Firefox, any page (if it is not a module, i.e. a background page) is the only thing that can send messages. Therefore, I added a number of services that were useful to me.

    Sending messages in a popup window:
    mono.sendMessage('Hi', function onResponse(message){
      console.log("response: "+message);
    }, "popupWin");
    

    Resize popup page:
    mono.sendMessage({action: "resize", width: 300, height: 300}, null, "service");
    

    Opening a new tab:
    mono.sendMessage({action: "openTab", url: "http://.../"}, null, "service");
    

    In general, if you look at the code, I’m sure it’s easy for you to add your “services” for the convenience of interacting with the API.

    Assembly


    The library is divided into several files for convenience. Everything is assembled using Ant, the build file is in “/ src / vendor / Ant”. In it, you can remove the browsers you do not need.

    Conclusion


    Here is such a simple library. Of course, she has all kinds of bugs and shortcomings. But it seems to work. I am sure that it will not be difficult for you to understand the code and where you need to file it for yourself.
    If all this seemed to you too complicated, there is an example of a simple extension in the git that is built for Chrome, Opera 12, Safari, Firefox. I use mono in several of my extensions and it has become indispensable for me.

    Thank you for reading!

    Github

    Also popular now: