Содержание


Создание своих собственных расширений для браузера

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

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

Comments

Серия контента:

Этот контент является частью # из серии # статей: Создание своих собственных расширений для браузера

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Создание своих собственных расширений для браузера

Следите за выходом новых статей этой серии.

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

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

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

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

Но прежде чем вы взяться за это, прочтите первые три статьи цикла. Эта статья написана с расчетом на 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
Страницы всплывающего окна и свойств Chrome

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

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

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

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

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

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

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

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

Рисунок 4. Страницы всплывающего окна/свойств расширений для Firefox и Safari
Страницы всплывающего окна/свойств расширений для 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
Два набора модулей BA, SM, и GB в Add-on Builder

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

Заключение

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

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

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


Ресурсы для скачивания


Похожие темы


Комментарии

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

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