Создание своих собственных расширений для браузера: Часть 4. Переход к расширениям, не зависящим от браузера

Исключение перегрузки процессора и избыточности в расширении для Chrome, Firefox и Safari

У каждого браузера свои сторонники и противники ― и свои преимущества и недостатки. Связывает их то, что люди проводят в браузерах все больше и больше времени. Этот цикл статей посвящен тому, как самостоятельно построить простое расширение для браузеров Chrome, Firefox и Safari. Читатель узнает, как создать расширение для каждого браузера, насколько трудно или легко решаются некоторые типичные задачи и как распространять свое расширение. В этой последней части цикла статей о расширениях для браузеров мы попробуем создать универсальное расширение, способное работать во всех трех браузерах.

Дуэйн О'Брайен, разработчик PHP, EPAM Systems

Дуэйн О'Брайен (Duane O'Brien) был разносторонним специалистом еще в те времена, когда у игры Oregon Trail был только текстовый интерфейс. Его любимый цвет - sushi (суши). Он никогда не бывал на луне.



10.06.2013

Не решил ли уже эту задачу кто-нибудь другой?

Crossrider и Kango Framework (см. раздел Ресурсы) помогают создавать кросс-браузерные расширения, но каждый из этих инструментов имеет свои ограничения. Crossrider поддерживает Safari в меньшей степени, чем Firefox и Chrome. Kango Framework, как утверждается, создает расширения для всех основных браузеров (включая Internet Explorer и Opera), но если проект не относится к разработке ПО с открытым исходным кодом, то нужно заплатить за лицензию. И оба инструмента страдают одной и той же проблемой: вы работаете с браузерами не напрямую, так что ваши возможности ограничены API, предоставляемыми средой, что добавляет уровень, которым нельзя или почти нельзя управлять. В будущем каждый из этих инструментов может оказаться действенным решением для ваших задач, но сначала полезно решить такую задачу самостоятельно. Считайте это учебной практикой.

В этом цикле статей мы уже написали расширение Gawkblocker для браузеров Chrome, Firefox и Safari. Напомним, что Gawkblocker позволяет блокировать определенные домены, которые пользователь предпочитает не посещать, такие как блоги, отнимающие много времени. Мы написали расширение Gawkblocker для Chrome, затем адаптировали его для Firefox и для Safari. Но наличие трех аналогичных наборов файлов расширений для всех трех браузеров ведет к дополнительной работе и избыточному коду.

В этой последней статье мы устраним избыточность и дополнительную работу, подойдя как можно ближе к созданию общей базы кода Gawkblocker. (Полный исходный код приведен в разделе Загрузки). Некоторые вещи легко поддаются объединению, например, основные файлы JavaScript и шаблоны HTML. Из предыдущих статей вы знаете, что механизмы хранения данных от браузера к браузеру различается, и у каждого браузера свой уникальный API для отслеживания изменений в URL. Прочитав статью, вы сможете ответить на основные вопросы:

  • Какую часть расширения можно сделать не зависящей от браузера?
  • Не окажется ли такое расширение неоправданно сложным?

Перед началом работы

Об этом цикле статей

В этом цикле из четырех статей рассматривается процесс создания расширения Gawkblocker для трех браузеров: Chrome, Firefox и Safari.

  • Первая часть посвящена созданию расширения для Google Chrome ― от начала и до размещения в App Store.
  • Во второй части мы построили дополнение (или расширение) для Mozilla Firefox.
  • В третьей части адаптировали его для браузера Safari.
  • А теперь попытаемся сделать код расширения вообще не зависящим от браузера.

Но прежде чем вы взяться за это, прочтите первые три статьи цикла. Эта статья написана с расчетом на Chrome 23 (сборка Canary), Firefox 16 (beta channel) и Safari 6.0 (см. раздел Ресурсы). Для работы с браузером Safari понадобится Mac, поскольку у него нет версий для других операционных систем. Вам также понадобится инструмент для редактирования HTML, CSS и JavaScript. И если вы все же решите взяться за эту статью без предварительного прочтения предыдущих, то прочтите хотя бы описание установок для создания расширений для каждого браузера.

Справочными документами могут служить документация по Chrome Extension, Firefox Add-on SDK и Safari Extensions Reference (см. раздел Ресурсы). Благодаря уже приобретенным знаниям, вам вряд ли придется обращаться к этим документам больше, чем пару раз. Но на всякий случай откройте их.


Анатомия Gawkblocker

В Gawkblocker для Chrome используются:

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

На рисунке 1 показаны страницы всплывающего окна и свойств.

Рисунок 1. Страницы всплывающего окна и свойств Chrome
Страницы всплывающего окна и свойств Chrome

В Firefox мы скомбинировали страницы свойств и всплывающего окна в одну, изменив файл JavaScript для использования API хранения данных Firefox, и управляли приложением посредством файла main.js. Переадресуемые пользователи направляются прямо на YouTube. Комбинированная версия страницы всплывающего окна и свойств в Firefox показана на рисунке 2.

Рисунок 2. Комбинированная версия страницы всплывающего окна и свойств для Firefox
Комбинированная версия страницы всплывающего окна и свойств для Firefox

В браузере Safari используется комбинированная страница всплывающего окна и свойств, фоновая страница для управления приложением и тот же файл JavaScript, что и в Chrome. Комбинированная страница всплывающего окна и свойств Safari показана на рисунке 3.

Рисунок 3. Комбинированная страница всплывающего окна и свойств для Safari
Комбинированная страница всплывающего окна и свойств для Safari

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


Абстрагирование кода, специфического для браузера

Абстрагирование кода, зависящего от браузера, можно начать с двух разных мест: кода всплывающего окна и основного файла JavaScript. Начнем с кода всплывающего окна.

На рисунке 4 показаны бок о бок страницы всплывающего окна/свойств расширений для Firefox и Safari.

Рисунок 4. Страницы всплывающего окна/свойств расширений для Firefox и Safari
Страницы всплывающего окна/свойств расширений для Firefox и Safari

Основные важные различия между ними относятся к способу обращения к объекту GB из фоновой страницы. В Safari (и в Chrome, не показан) код всплывающего окна может обращаться непосредственно к фоновой странице. В Firefox он должен отправить сообщение в файл main.js. Кроме того, каждый браузер выполняет свою процедуру инициализации при открытии страницы всплывающего окна. Если экстернализировать процесс инициализации и обращений к объекту GB, то можно будет использовать один и тот же код всплывающего окна.

Создадим объект BA для обработки действий, зависящих от браузера, и поместим его в BA.js. В этом объекте поставим в соответствие каждому браузеру то, что в нем нужно делать. Кроме того, перенесем объект StorageManager (SM) в отдельный файл. Код JavaScript страницы всплывающего окна представлен в листинге 1.

Листинг 1. Код JavaScript страницы всплывающего окна
<script src="BA.js"></script>
<script src="SM.js"></script>
<script src="GB.js"></script>
<script>
    $(document).ready(function () {
        BA.handle.popup();
        ...
        showBlockList(GB.getBlockedSites());
    });
</script>

В объекте BA определим таблицу соответствия браузеров и относящихся к ним действий. Мы знаем наверняка, что объект BA должен настраивать всплывающую страницу, фоновую страницу и перехватывать запросы браузера, как это делается в листинге 2.

Листинг 2. Код объекта BA
actionMap = {
    'safari' : {
        'popup' : function () {
        },
        'background' : function (callback) {
        },
        'intercept' : function (candidate, site, watchthis) {
        }
    },
    'chrome' : {
    ...
}

Нужно также, чтобы объект BA определял, в каком браузере он работает. Выполним тест на наличие API —safari для Safari, chrome для Chrome и require или addon для Firefox (в зависимости от того, находится ли он в main.js или в коде всплывающего окна). Тест выполняется при создании модуля, поэтому его результат доступен в любой момент. Код, приведенный в листинге 3, проверяет наличие API.

Листинг 3. Проверка наличия API Safari, Chrome или Firefox
    if (typeof safari !== 'undefined') {
        console.log("Safari");
        // Обращение к глобальной странице с помощью 
        // safari.extension.globalPage.contentWindow
        my.handle = actionMap.safari;
    } else if (typeof chrome !== 'undefined') {
        console.log("Chrome");
        // Обращение к глобальной странице с помощью chrome.extension.getBackgroundPage()
        my.handle = actionMap.chrome;
    } else if (typeof require !== 'undefined') {
        console.log("Firefox - background");
        // Прослушивание сообщений с помощью addon.port.on(messagename, callback);
        // Отправка сообщения с помощью addon.port.emit(messagename, message); 
        my.handle = actionMap.firefox;
    } else if (typeof addon !== 'undefined') {
        console.log("Firefox - popup");
        // Прослушивание сообщений с помощью addon.port.on(messagename, callback);
        // Отправка сообщения с помощью addon.port.emit(messagename, message); 
        my.handle = actionMap.firefox;
    }

При вызове BA.handle.popup() в Safari BA.handle уже настроен на объект из actionMap.safari, содержащий функцию установки popup() для этого браузера.

Нечто подобное делается на страницах background.html и в файле main.js, который используется для расширений. Мы вызываем BA.handle.background() для настройки фоновой страницы (присоединение прослушивающих процессов и т.п.) и передаем его функции, которая определяет, следует ли блокировать сайт. Страницы JavaScript background.html напоминают листинг 4.

Листинг 4. Страница JavaScript background.html
<script src="BA.js"></script>
<script src="SM.js"></script>
<script src="GB.js"></script>
<script>
    $(document).ready(function () {
        ...
        function shouldIBlockThis(candidate) {
            var site,
                blockedSites = GB.getBlockedSites();
            for (site in blockedSites) {
                if (blockedSites.hasOwnProperty(site)) {
                    BA.handle.intercept(candidate, site, GB.getWatchThisInstead());
                }
            }
        }
        BA.handle.background(shouldIBlockThis);
    });
</script>

Код из файла main.js выглядит так же; только для извлечения модулей BA и GB используется require. Затем модуль GB импортирует модуль SM, так что в main.js это делать необязательно.


Адаптация существующих модулей

Мы слегка изменим модули BA, SM и GB, так чтобы они не зависили от браузера. В частности, Firefox нужно, чтобы модули экспортировались и запрашивались. Откорректируем exports и require и будем использовать их условно. В начале модуля GB поместим код, приведенный в листинге 5, чтобы импортировать модуль SM.

Листинг 5. Импорт модуля SM
if (typeof require !== 'undefined') {
    var SM = require("SM").SM;
}

//И переходим к экспорту модуля GB

if (typeof exports !== 'undefined') {
    exports.GB = GB;
}

Этот модуль SM создает проблему. Напомним, что Firefox не имеет доступа к localStorage из расширения, поэтому в Части 2 мы использовали simple-storage. Для этого к модулю BA можно добавить обработчик. Также можно добавить проверку в объекте SM. Это позволит поддерживать объект localStorage в новой версии Firefox, если он станет доступным, и сохранить поддержку старых версий. Этот обходной путь показан в листинге 6.

Листинг 6. Обходной путь для Firefox
    if (typeof localStorage !== 'undefined') {
        // В настоящее время поддерживается в Chrome и Safari

        my.get = function (key) {
            return localStorage.getItem(key);
        };
        my.put = function (key, value) {
            return localStorage.setItem(key, value);
        };
        my.remove = function (key) {
            return localStorage.removeItem(key);
        };
    } else if (typeof require !== 'undefined') {
        // Это для Firefox.  Обращение и использование simple-storage
        SS = require("simple-storage");
        console.log("SimpleStorage");

        my.get = function (key) {
            return SS.storage[key];
        };
        ...
    }

Этот обходной путь также позволяет использовать модуль SM для другого проекта. С помощью этой структуры можно подключать код, зависящий от браузера.


Получение кода, зависящего от браузера

Страницы всплывающего окна для каждого браузера несколько различаются. Для Safari нужно знать, когда она открывается, чтобы очистить изображение, как показано в листинге 7.

Листинг 7. Страница всплывающего окна Safari
'popup' : function () {
    // Страницы всплывающего окна Safari сохраняют состояние. При его закрытии 
    // обязательно сбрасывайте параметры и приводите элементы div в исходное состояние.
    safari.application.addEventListener("popover", function () {
        $("#onestep").show();
        $("#options").hide();
    }, true);
}

Chrome не требует никакой дополнительной работы — приятно!

Для Firefox нужно настроить сообщения port и сбросить изображение, как показано в листинге 8.

Листинг 8. Страница всплывающего окна Firefox
'popup' : function () {
    // Страницы всплывающего окна Firefox сохраняют состояние. При его закрытии 
    // обязательно сбрасывайте параметры и приводите элементы div в исходное состояние.
    addon.port.on("popshow", function () {
        $("#onestep").show();
        $("#options").hide();
    });
    // Страницы всплывающего окна Firefox не могут обращаться к объекту GB напрямую,  
    // так что приходится передавать сообщения.
    addon.port.emit("pop");
    $("#watchthis").click(function () {
        addon.port.emit("watchthis");
    });
    $("#makethathappen").click(function () {
        addon.port.emit("makethathappen", $("#watchthatinstead").val());
    });
    $("#blockthistoo").click(function () {
        addon.port.emit("dontgothere", $("#dontgothere").val());
    });
    addon.port.on("blocklist", function (blocklist) {
        showBlockList(blocklist);
    });
    addon.port.on("watchthatinstead", function (instead) {
        $("#watchthatinstead").val(instead);
    });
}

Нечто подобное нужно сделать для фоновых страниц и main.js. В Safari добавим процесс прослушивания событий beforeNavigate, как показано в листинге 9.

Листинг 9. Добавление процесса прослушивания событий beforeNavigate в Safari.
'background' : function (callback) {
    safari.application.addEventListener("beforeNavigate", callback, true);
}

Для Chrome добавим процессы прослушивания для вкладок, как показано в листинге 10.

Листинг 10. Добавление процессов прослушивания для вкладок Chrome
'background' : function (callback) {
    chrome.tabs.onUpdated.addListener(function (tabId, changedInfo, tab) {
        callback(tab);
    });
    chrome.tabs.onCreated.addListener(function (tab) {
        callback(tab);
    });
}

Для Firefox создадим панель и виджет, а затем настроим другую половину сеанса связи port. Соберем соответствующие модули, как показано в листинге 11.

Листинг 11. Создание панели и виджета для Firefox
'background' : function (callback) {
    var data = require("self").data,
        tabs = require("tabs"),

        GB = require("GB").GB,
        popupPanel = require("panel").Panel({
            height: 500,
            contentURL: data.url("popup.html"),
            onShow : function () {
                this.port.emit("popshow", true);
            }
        });
    tabs.on("ready", function (tab) {
        callback(tab);
    });
    require("widget").Widget({
        id: "GBBrowserAction",
        label: "Gawkblocker",
        contentURL: data.url("GB-19.png"),
        panel: popupPanel
    });
    popupPanel.port.on("pop", function () {
        popupPanel.port.emit("blocklist", GB.getBlockedSites());
        popupPanel.port.emit("watchthatinstead", GB.getWatchThisInstead());
    });
    ...
}

Теперь нам понадобится обработчик intercept— код, который запускается, чтобы проверить, нужно ли блокировать сайт. Каждый обработчик работает немного по-разному, но структура у них одна и та же: объект вкладки или событие передается обработчику intercept вместе с проверяемым сайтом и страницей переадресации пользователя, если сайт заблокирован. Обработчик intercept выполняет необходимые действия для данного браузера. Для Safari он выглядит, как в листинге 12.

Листинг 12. Обработчик intercept для Safari
'intercept' : function (candidate, site, watchthis) {
    if (candidate.url && candidate.url.match(site)) {
        candidate.preventDefault();
        candidate.target.url = watchthis;
    }
}

Итак, для Safari и Chrome все готово! Можно загружать универсальные файлы, и они будут работать в обоих браузерах.


Западня

Однако для Firefox это еще не все. Chrome и Safari в этом случае работают легко, потому что страница всплывающего окна и фоновая страница могут обращаться в одно и то же место в пределах localStorage. В Firefox, находясь внутри кода всплывающего окна, нельзя получить доступ к объекту simple-storage или объектам из контекста main.js. Вот почему все делается с помощью передачи сообщений. При создании списка заблокированных сайтов на странице всплывающего окна в Firefox каждому сайту требуется дополнительный обработчик click, чтобы передать сообщение в main.js. Этот дополнительный обработчик можно добавить в блок, где создаются обработчики click, как показано в листинге 13.

Листинг 13. Добавление обработчика событий click в Firefox
$("#unblock-" + i).click(function () {
    GB.removeBlockedSite(index);
    BA.handle.removeSite(index);
    showBlockList(GB.getBlockedSites());
});

Затем добавим метод removeSite в объект actionMap.firefox модуля BA, как показано в листинге 14.

Листинг 14. Добавление метода removeSite
'removeSite' : function (index) {
    addon.port.emit("unblock", index);
}

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

Еще хуже, что в случае Firefox вы попадаете в заколдованный круг. Страница всплывающего окна включает в себя модули BA, SM и GB. Но все используемые фактические данные поступают из файла main.js, которому также нужны модули BA, SM и GB. А так как они не могут работать на два фронта, то в расширение для Firefox приходится включать по две копии каждого модуля, как показано на рисунке 5.

Рисунок 5. Два набора модулей — нехорошо!
Два набора модулей BA, SM, и GB в Add-on Builder

Дублированные модули ― не лучший вариант. Их можно было бы разделять или передавать туда-обратно, но это уже слишком. Появляются сомнения, стоит ли игра свеч.


Заключение

Теперь можно ответить на вопросы, поставленные в начале статьи.

  • В какой мере расширение можно сделать не зависящим от браузера? Как выясняется, в довольно значительной. Основной класс JavaScript и код всплывающего окна не зависят от браузера. Код JavaScript фоновой страницы почти идентичен файлу main.js, а другие модули работают в любом из браузеров.
  • Не окажется ли такое расширение неоправданно сложным? Возможно. Модуль GB довольно прост. Модуль BA предоставляет единое место, где можно выполнить большую часть работы браузера. Однако остается преодолеть различия между расширением для Firefox и для Chrome или Safari.

Очень хотелось бы сделать так, чтобы код всплывающего окна и файл main.js расширения для Firefox совместно использовали объекты или по крайней мере получали доступ к одной и той же области хранения. В этом случае удалось бы почти полностью исключить передачу сообщений. Но не слишком зацикливайтесь на этой проблеме. Можно установить паритет между всеми тремя браузерами, если использовать нечто вроде RequireJS или другого загрузчика в стиле CommonJS. А модуль BA можно заменить тремя модулями, по одному для каждого браузера, оставив свой продукт более дискретным.


Загрузка

ОписаниеИмяРазмер
Исходный код Gawkblockersourcecode.zip6 KБ

Ресурсы

Научиться

Получить продукты и технологии

  • Mozilla Firefox: загрузите версию Firefox для своей платформы.
  • Firefox Add-on SDK: загрузите SDK.
  • Firefox Add-on Builder: здесь находится Add-on Builder.
  • Gawkblocker: загрузите Gawkblocker для Firefox из профиля Add-on Builder автора.
  • Chrome Developer Tools: используйте версию Google Chrome из Developer Channel, чтобы получить новейшие инструменты разработки.
  • Kango Framework: познакомьтесь с этой средой для создания JavaScript-расширений.
  • Crossrider: создавайте кросс-браузерные расширения с помощью этой платформы.

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source, Web-архитектура
ArticleID=933550
ArticleTitle=Создание своих собственных расширений для браузера: Часть 4. Переход к расширениям, не зависящим от браузера
publish-date=06102013