У браузера Google Chrome с самого начала его существования есть достаточно удобное API для создания расширений. Появление большого количества браузеров на той же кодовой базе сделало это API массовым. В итоге новое API расширений браузера Firefox под названием WebExtensions заявлено как совместимое с Google Chrome, а в Microsoft Edge начиная с Redstone 1 добавлена поддержка WebExtensions.
Казалось бы унификация API между браузерами должна облегчить жизнь разработчикам расширений, однако хорошая идея, как это часто бывает, была испорчена реализацией. Автор этих строк по работе связан с разработкой одного браузерного расширения и бувально "на своей шкуре" испытал "радость" от поддержки одним расширением сразу нескольких браузеров.
Всё что написано ниже не претендует на завание истины в последней инстанции и является просто изложением личного опыта автора и, в некотором смысле, приглашением к дискуссии и обмену опытом.
Манифест расширения
Любое расширение начинается с написания манифеста. При написании манифеста расширения надо как минимум указать основной JS-файл. Для большинства браузеров описание выглядит примерно так:
"background": { "scripts": ["background.js"], "persistent": false }
Однако для Microsoft Edge надо писать так:
"-ms-preload": { "backgroundScript": "background.js" }
В качестве альтернативы можно описать так:
"background": { "page": "background.html", "persistent": true }
И уже в заголовке файла "background.html" подключать "background.js". Этот способ работает во всех браузерах, хотя для Chrome является deprecated и в будущем работать перестанет.
Есть и другие отличия в разрборе манифеста разными браузерами. Кроме того в Firefox есть свои специфичные параметры, которых нет в других браузерах. Например ветка "applications".
Объект браузера
Переходим к рассмотрению собственно API расширений. Расширения пишутся на javascript и большая часть логики реализуется через вызов методов глобального объекта, описывающего браузер. И тут разработчика подстерегает первая проблема: имя объекта зависит от браузера:
- Google Chrome: "chrome".
- Mozilla Firefox: рекоммендуется использовать "browser", однако можно использовать и "chrome".
- Opera: для основного функционала "chrome", для Opera-специфичных функций - "opr".
- Microsoft Edge: "msBrowser", объект "chrome" вроде как доступен, но возможность его использования не исследовалась в полной мере.
Таким образом разработка расширения начинается с написания "костыля" примерно такого вида:
var browserObj = (function () { return window.msBrowser || window.browser || window.chrome; })();
Может быть на этом различия заканчиваются? Было бы здорово, если так. Но к сожалению различий ещё много и рассмотреть все в рамках небольшого материала не получится. Потому рассмотрим только несколько характерных примеров.
BrowserAction API
Допустим наше расширение использует API browserAction: кнопка на панели браузера, при нажатию на которую появляеся всплывающее окно с веб-страницей. У кнопки есть глобальные свойства и свойства, привязанные к определённым вкладкам. Рассмотрим поведение метода "browserObj.browserAction.setPopup" в зависимости от браузера. Для начала типичный пример использования:
var popupUrl = "popup.html"; browserObj.browserAction.setPopup({popup: popupUrl});
Подводные камни кроются в том, как разные браузеры обрабатывают параметр "popup" и какие ограничения на него накладывают. Firefox позволяет использовать абсолютные и относительные пути. Причём возможно использование как встроенных в расширение страниц, так и внешних URL. В Chrome и Opera возможно только использование внутренних страниц.
Если попробовать указать не абсолютный путь к странице, а относительный то и тут есть отличия в поведении: в Chrome и Opera относительные пути считаются от корневой директории расширения, а в Firefox - от директории, в которой расположен указанный в манифесте "background.html". Таким образом если у нас в манифесте есть такие строки:
"background": { "page": "html/background.html", "persistent": true }
А в коде такие:
var popupUrl = "popup.html"; browserObj.browserAction.setPopup({popup: popupUrl});
То для Chrome и Opera это будет эквивалентно:
var popupUrl = browserObj.runtime.getURL("popup.html"); browserObj.browserAction.setPopup({popup: popupUrl});
А для Firefox:
var popupUrl = browserObj.runtime.getURL("html/popup.html"); browserObj.browserAction.setPopup({popup: popupUrl});
Ещё интереснее ситуация будет если попробовать в качестве "popupUrl" указать абсолютный URL. Например так:
var popupUrl = "http://ya.ru"; browserObj.browserAction.setPopup({popup: popupUrl});
Firefox здесь отработает нормально и откроет указанный URL во всплывающем окне. А вот Chrome и Opera раскроют это так:
var popupUrl = browserObj.runtime.getURL("http://ya.ru"); browserObj.browserAction.setPopup({popup: popupUrl});
Ну и разумеется никакой внешний URL открыт не будет. Получается что разработчики Firefox давая разработчикам расширений дополнительные возможности нарушили совместимость с Google Chrome.
Notifications API
В определённых ситуациях расширение может показывать оповещения пользователю используя API notifications. Это API поддерживается во всех браузерах, но большая часть расширенных опций доступна только в Google Chrome. Хорошая таблица совместимости есть в документации от разработчиков Mozilla.
Отдельного внимания заслуживает тот факт что браузер Opera при указании неподдерживаемых опций генерирует ошибку. Например вы хотите показать оповещение и на нём же кнопку настроек, чтобы пользователь мог отключить оповещения в дальнейшем. Вы пишите примерно такой код:
chrome.notifications.create( "checkfail", type: "basic", iconUrl: chrome.extension.getURL("data/plugin_logo.png"), title: chrome.i18n.getMessage("pluginTitle"), message: chrome.i18n.getMessage("checkFail", check_info), buttons: [ { title: chrome.i18n.getMessage("notifyButtonSettings"), iconUrl: chrome.extension.getURL("data/notify_icon_settings.png") } ] });
В Google Chrome всё отлично работает, а вот в Opera этот код приводит к ошибке и последующие инструкции не выполняются, что может быть критично.
Management API
Ещё одна интересная тема связана с идентификаторами расширений. Допустим что наше расширение имеет известные проблемы при работе с некоторыми другими расширениями. По идее тут на помощь должен прийти management API, с помощью которого можно проверить наличие в браузере расширений с определёнными идентификаторами. Однако и тут есть нюансы:
- Идентификаторы расширениям присваиваются каталогами расширений и разнятся от браузера к браузеру.
- Яндекс.Браузер позволяет ставить расширения как из Opera Addons, так и из Chrome Webstore. Соответственно в случае Яндекс.Браузера надо проверять два набора идентификаторов.
- Firefox через management API позволяет управлять только темами, но не расширениями. Да и для тем возвращаются не идентификаторы из каталога расширений, а сгенерированные случайным образом и актуальные только для текущей инсталляции браузера.
Соответственно универсального способа проверки наличия определённого расширения в системе нет. Для большинства расширений это не является проблемой, но в некоторых ситуациях это сильно усложняет жизнь разработчику.
Каталоги расширений
Ну и раз уж мы затронули тему каталогов расширений то значит пора рассмотреть их подробнее, тем более что большинство расширений в итоге пишутся для публикации в соответствующих каталогах.
И тут тоже есть некоторые отличия, которые следует иметь ввиду:
- Chrome Webstore: автоматическая модерация в течении часа, каких-то особых требований к коду нет. Заливается обычный zip-архив с файлами расширения.
- Mozilla Addons: ручная модерация от часа до недели. Обфусцированый JS не запрещён, но модератору необходимо предоставить исходник и инструкцию по сборке. Заливается обычный zip-архив с файлами расширения.
- Opera Addons: ручная модерация от суток до четырёх месяцев (возможно и дольше, но автор этих строк с большими сроками не сталкивался). Обфусцированый JS запрещён. Заливается обычный zip-архив с файлами расширения.
- Windows Store: сильно усложнена сборка: необходимо использовать NodeJS с модулями, доступными только для Windows, и далее ЭЦП с помощью встроенной в Windows утилиты (подробнее).
Резюмируя ограничения каталогов расширений:
- Если есть необходимость обфусцировать код то от поддержки Opera придётся отказаться. Плюс будут дополнительные сложности с Firefox.
- Медлительность модераторов Opera лишает разработчиков возможности оперативно доставлять обновления пользователям этого браузера.
- Необходимость использования специфичных для windows инструментов для сборки расширения перед публикацией в Windows Store автоматически требует наличия лицензии на соответствующую ОС и усложняет работу пользователям других ОС.
- Учитывая рыночную долю Microsoft Edge и сложность сборки расширений для публикации ряду разработчиков проще будет отказаться от моддержки этого браузера.
Вместо заключения
Отличий и не соответствий в реализациях вобщем-то схожих API очень много и подробно рассматривать их все бессмысленно. Важно что отметить сам факт их существования. Радует что разработчики Mozilla в своей документации приводят таблицы совместимости хотя бы для основных браузеров, однако это не отменяет необходимости тщательно тестировать работу расширения во всех браузерах, для которых планируется поддержка.
Хотя подобная сегментация и усложняет разработку расширений, автор этих строк надеется что со временем ситуация разрешиться сама собой: непопулярные API отомрут, у популярных основные особенности стабилизируются и станут одинаковым во всех браузерах, а различия останутся лишь в совсем мелких деталях, не критичных для большинства разработчиков.