Устройство расширений для браузера Firefox (WebExtensions)
- понедельник, 16 марта 2020 г. в 00:23:39
Для людей, работа которых связана с использованием сети Интернет, расширения браузера могут быть очень полезными инструментами. С помощью них можно избавить пользователя от повторения одних и тех же действий и лучше организовать рабочий процесс. Можно составить набор инструментов из уже существующих расширений, но этого бывает недостаточно.
Тому, кто разбирается в веб-разработке, будет несложно создать новое расширение для браузера. Сейчас большинство самых популярных браузеров поддерживает стандартную систему разработки, которая использует в основном только JavaScript, HTML и CSS, — WebExtensions.
Человеку, который никогда раньше не создавал дополнение для браузера на основе WebExtensions, может быть тяжело сразу понять, из каких основных частей оно должно состоять и что может делать. В сети Интернет есть много информации об этой системе, но для того, чтобы создать для себя общую картину, придётся потратить много времени. Эта статья поможет быстро разобраться в устройстве системы WebExtensions и покажет, как лучше ориентироваться в документации к её API. Здесь описывается расширение для браузера Firefox, поэтому почти вся информация, используемая в статье, взята с сайта MDN. Но статья будет полезна и тем, кто хочет создать расширение для других браузеров, поддерживающих WebExtensions, — в первую очередь для Google Chrome и Chromium.
Здесь рассматривается создание расширений только для настольных компьютеров. Если нужно создать расширение для мобильного браузера Chrome или Firefox, эта статья тоже может быть чем-то полезной, но основную часть информации придётся найти и изучить самостоятельно.
А нужно вот что:
Firefox до версии 60 хуже поддерживал WebExtensions — там не были реализованы многие полезные функции. Но вообще более-менее сносно Firefox поддерживает эту систему начиная с версии 52.
В Google Chrome или Chromium расширения на основе WebExtensions всегда работают хорошо, ведь эта система изначально была сделана как их часть. У них, собственно, другие браузеры и позаимствовали эту систему. У API для браузера Firefox есть существенные отличия, о которых можно узнать из статьи Building a cross-browser extension. Она может помочь и для того, чтобы сделать расширение, которое подойдёт как для Firefox, так и для Chrome.
Независимо от того, для какого браузера создаётся расширение, может пригодиться информация об API расширений Chrome.
Многие из популярных браузеров работают на том же «движке», что и Google Chrome. Это и Яндекс-Браузер, и Opera, и Microsoft Edge. Поэтому и механизм расширений у всех этих браузеров очень похож на тот, что используется в Chrome. Отличия в API WebExtensions у этих браузеров от браузера Chrome, конечно, есть, но обычно их меньше, чем у Firefox.
Для Яндекс-Браузера на момент написания статьи рекомендовалось использовать расширения из каталога для браузера Opera. Значит, скорее всего, API для расширений у двух этих браузеров если и различаются, то мало. Поэтому, если будет нужно создать расширение для Яндекс-Браузера или Opera, обязательно просмотрите API для браузера Opera.
Если нужно сделать расширение для Edge, тогда смотрите список его средств API на сайте Microsoft.
Для начала поверхностно рассмотрим, что такое расширение и каковы его основные возможности. Большинство ссылок в этом разделе ведут к более подробному описанию в этой же статье, поэтому раздел можно использовать как своеобразное оглавление. Но учтите, что ссылки будут даны не в том порядке, в котором идут главы статьи.
Расширение — это набор скриптов. Самый главный из них — manifest.json. В некоторых случаях расширение может состоять вообще только из этого файла — такое может быть у расширения-темы, которое только изменяет стиль оформления окна браузера. В файле-манифесте содержится вся информация о расширении, а также о том, что и когда нужно запускать, что расширению разрешено делать, к каким ресурсам оно само даёт доступ, и некоторые настройки браузера. Остальное — скрипты Javascript, HTML, CSS, а также данные для них (например, файлы изображений).
Если совсем коротко, вот что может делать расширение:
Скрипты расширения делятся на группы, у каждой из которых — свои программная среда и время жизни. Эти группы называются контекстами выполнения. У каждого всплывающего меню, а также у фонового скрипта и у скриптов, добавленных в веб-страницу, и у других подобных частей расширения — свои отдельные контексты выполнения.
Скрипты разных контекстов выполнения могут общаться друг с другом с помощью механизмов сообщений — например, передавая информацию о нужных событиях.
По умолчанию расширению доступны только «безопасные» возможности, то есть только часть API WebExtensions. Чтобы использовать больше средств API, нужно дать расширению разрешения на доступ к ним.
Дополнительно можно почитать эти статьи:
Теперь рассмотрим основные понятия и особенности, связанные с созданием расширения в системе WebExtensions, с которыми придётся часто сталкиваться и в данной статье, и при чтении документации на MDN. Почти все ссылки этого раздела тоже ведут к разным местам текущей статьи.
Любому скрипту расширения доступен глобальный объект, который содержит все свойства и функции API WebExtensions, доступные в этом скрипте. В расширениях для браузера Firefox он называется browser
, а для Google Chrome — chrome
. Но главный, родительский объект для скриптов расширения — это всегда window
, — как и у скриптов обычных страниц веб-сайтов.
Таким образом, каждому скрипту расширения всегда доступно два глобальных объекта. Один из них — это объект window
. Другой объект — точка входа в API WebExtensions, в нашем случае — browser
. Этот объект не является частью window
и существует параллельно ему. Всё это значит, что любые объекты и функции, определённые в скрипте глобально, — как обычно, будут частью объекта window
, и для доступа к ним не обязательно каждый раз писать «window.
» перед их именем. А для доступа к средствам API WebExtensions нужно каждый раз добавлять «browser.
» перед именем объекта или функции. Например, просматривая документацию к WebExtensions на сайте MDN, можно заметить, что через объект runtime.id можно узнать идентификатор расширения. Если посмотреть его подробное описание, синтаксис для доступа к нему —
var myAddonId = browser.runtime.id;
Состав объекта browser
может отличаться как в разных скриптах, так и в разных расширениях. Это зависит от того, к какой части API WebExtensions разрешён доступ всему расширению, и в каком контексте выполнения работает скрипт.
Работающее расширение можно условно разделить на части — контексты выполнения. В каждом из этих контекстов работает свой отдельный набор скриптов, у которых практически одинаковое время жизни и одна и та же программная среда. Контексты выполнения расширения бывают разных видов. У каждого из них есть свои особенности, но можно выделить общие черты.
На самом деле ещё к расширению может быть подключена внешняя программа, но она работает практически вне браузера, и плагин взаимодействует с ней по-другому. Поэтому она не относится к рассматриваемым в данном разделе контекстам. Внешняя программа будет рассмотрена отдельно, ближе к концу статьи.
Каждому контексту выполнения доступна DOM своего отдельного документа. Да-да, каждой части расширения соответствует некоторая HTML-страница, даже если она никому не видна, и даже если никто не добавлял в расширение соответствующий HTML-файл! И, конечно, у всех скриптов одного и того же контекста выполнения — общие глобальные объекты window
и browser
.
Все контексты выполнения можно разделить на две группы. В первую группу входит всего один вид контекстов — контексты содержимого веб-страниц. В каждом из них работает набор скриптов, добавленных нашим расширением к загруженной с сервера веб-странице. То есть каждой вкладке браузера, в которую наше расширение добавило скрипты, соответствует один такой контекст выполнения. Во вторую группу входят все остальные, привилегированные контексты. В них работают те части расширения, которые добавляют функционал к самой программе браузера. Например, всплывающее меню, боковая панель, фоновый скрипт (или «фоновая страница») — работают в таких контекстах.
У привилегированных контекстов выполнения есть такие особенности:
<script>
, — вместо этого всегда загружайте файлы с помощью <script src="my_script.js"></script>
! Иначе браузер может выдавать ошибки и будет тяжело сразу понять, откуда они взялись.window
такого контекста даёт доступ только к его собственному HTML-документу.Контекст содержимого веб-страницы отличается такими особенностями:
window
. И время жизни этих скриптов почти совпадает со временем жизни «родных» скриптов веб-страницы. Тем не менее в описании рабочего окружения скриптов содержимого сказано, что их контекст выполнения не тот же самый, что у настоящих скриптов веб-страницы. Это значит, что у настоящей веб-страницы один объект window
, а у добавленных к ней скриптов — другой.Из последней особенности в списке видно, что можно выделить ещё один вид контекстов выполнения — назовём его контекстом подлинной страницы веб-сайта. Он тесно связан с контекстом содержимого расширения браузера, но, несмотря на это, не является частью расширения.
Каждому контексту выполнения отведено своё время жизни. Вернее, это время жизни его программного окружения: объекта window
вместе с его HTML-документом и объекта browser
. Например, время жизни всплывающего меню — пока видно это меню, а время жизни скриптов содержимого — от загрузки веб-страницы в браузер (или от момента, когда эти скрипты добавили) и до закрытия вкладки или начала загрузки следующей страницы. Время жизни отдельных контекстов выполнения будет указано в разделах о соответствующих частях расширения.
Об уже упомянутых механизмах сообщений, которые используются для взаимодействия между контекстами выполнения, мы поговорим позже.
Я советую не создавать файлы расширения прямо в корневой папке проекта, а сделать там несколько папок: одну — собственно, для файлов расширения («Extension»), ещё одну — для файлов, описывающих дизайн («Design»), и ещё одну — для внешней программы, если она нужна («Native»). Можно ещё добавить папку «Build», чтобы складывать туда готовые к публикации файлы-архивы. А внутри папки расширения для тех контекстов выполнения, которым соответствует более одного файла, лучше создать отдельные папки. В папке расширения файлы, которые не являются скриптами, тоже обычно раскладывают по папкам: например, для иконок обычно создают папку «icons», а файлы локализации обязательно должны быть в папке «_locales» (если расширение поддерживает несколько языков). В итоге папка проекта может выглядеть так:
My_project
|--Design
| |--concept.md
|
|--Build
| |--my_extension-1.0.0.zip
|
|--Native
| |--native_program.py
| |--native_program_manifest.json
| |--run.bat
| |--install.sh
| |--install.bat
|
|--Extension
|--manifest.json
|--background.js
|--content.js
|--browser_popup
| |--menu.html
| |--menu.css
| |--menu.js
|
|--icons
|--my_extension.svg
При установке и запуске расширения браузер читает файл manifest.json
, который должен находиться в корневой папке расширения. Из этого манифеста он получает всю информацию о расширении и о том, как с ним работать. Этот файл должен быть в формате JSON. В него можно добавлять строчные комментарии в стиле JavaScript, что может быть полезным.
Файл-манифест не только обязателен для любого расширения. Он может быть и единственным файлом в расширении. Например, можно сделать расширение, при установке которого изменяется тема оформления браузера, используя только один файл манифеста. Смотрите документацию к элементу манифеста theme.
Документацию к файлу манифеста смотрите в статье о manifest.json на MDN.
Файл манифеста лучше всего начинать с общего описания расширения, а именно — с трёх обязательных элементов:
2
.На всякий случай можно добавить ещё и сокращённое имя расширения — с помощью элемента short_name. Оно будет показываться там, где места для полного имени не хватает.
Следующим лучше добавить краткое описание расширения — с помощью элемента description
Ещё нужно добавить информацию об авторе. Для этого, обычно, используют такие элементы манифеста:
Вместо этих двух элементов можно использовать один элемент-группу developer. Он делает то же самое, только выглядит немного иначе.
Теперь можно добавить элемент browser_specific_settings.
В старых браузерах Firefox — до версии 48 — этот элемент был обязательным, и до версии 42 он назывался applications
. Сейчас можно использовать как browser_specific_settings
, так и applications
, но многие расширения используют applications
. Возможно так получилось потому, что эти расширения написаны уже давно и несколько раз их адаптировали для браузера новой версии, а это имя изменять было необязательно. А может и потому, что имя applications
часто встречается в примерах расширений на MDN.
Этот элемент необязателен и поддерживается только браузером Firefox, но часто бывает полезным. С помощью него можно указать диапазон версий браузера, в которых расширение может работать, а также ID расширения. Если в расширении используется внешняя программа (которую мы рассмотрим позже) или взаимодействие с другими расширениями, то этот ID должен быть известен и лучше указать его вручную. А если ID знать не нужно, можно вообще не беспокоиться о его существовании. Тогда просто не указывайте его, и он будет присвоен расширению автоматически — когда будет нужен.
По стандарту идентификатором может быть или GUID или строка текста, похожая на адрес e-mail. Не пытайтесь генерировать GUID — это непрактично и бесполезно. Лучше использовать строку вида название_расширения@организация.org
. Конечно, буквы здесь можно использовать только латинские. Вместо названия сайта организации можно использовать что угодно: или название личного сайта, или имя пользователя и сайт какого-нибудь сервиса или блога, на котором Вы зарегистрированы. На самом деле ID расширения нужен только для того, чтобы гарантированно отличить его от других расширений, и он почти никому не будет виден. Поэтому здесь не обязательно указывать настоящее название сайта или адрес e-mail. Например, если название расширения — my_extension, имя автора расширения — user, а личного сайта или сайта организации нет, то ID может быть одним из таких:
my_extension@user.habr.com
my_extension@user.github.com
my_extension@user.addons.mozilla.org
Ещё нужно добавить иконку-логотип, по которой будут узнавать наше расширение. Она будет видна везде, где расширение должно быть обозначено: в Менеджере Дополнений (меню браузера -> Дополнения -> Расширения) и в списке расширений на сайте, где оно будет опубликовано для общего использования. Для этого в манифест нужно добавить элемент icons.
В документации на MDN сказано, что стандартный размер иконки для Менеджера Дополнений — 48x48 пикселов, но на момент написания статьи намного чаще используется размер 32x32. А ещё желательно добавить иконки размером в два раза больше — для экранов с большим разрешением, таких как Retina display в устройствах от фирмы Apple. Таким образом, чтобы иконка всегда хорошо отображалась, нужно добавить изображения таких размеров: 32x32, 64x64, 48x48 и 96x96 пикселов.
Рекомендуется использовать изображения в формате PNG или SVG. Даже если использовать один и тот же файл SVG для разных размеров иконки, лучше определить его в манифесте несколько раз — для разных размеров. Кстати, браузер Google Chrome не поддерживает файлы SVG в качестве иконок.
Вот пример начала файла manifest.json
. Название и описание расширения выдуманы, на самом деле такого расширения нет.
{
// Это значение всегда должно быть числом 2
"manifest_version": 2,
// Название расширения
"name": "Habr article editor",
"short_name": "Habr Editor",
// Версия
"version": "1.0.0",
// Краткое описание расширения
"description": "Enhances editor of articles on habr.com site to support Markdown Extra",
// Имя автора — никнейм или полное
"author": "Aleksandr Solovyov",
// Адрес домашней страницы расширения, обычно — специальный сайт
// или страница на GitHub
"homepage_url": "https://github.com/alexandersolovyov/habr_editor",
// Идентификатор расширения и совместимые версии браузера Firefox
"browser_specific_settings": {
"gecko": {
"id": "habr_editor@alexandersolovyov.github.com",
"strict_min_version": "52.0"
}
},
// Иконка-логотип
"icons": {
"32": "icons/habr_editor.svg",
"64": "icons/habr_editor.svg",
"48": "icons/habr_editor.svg",
"96": "icons/habr_editor.svg"
},
...
}
Дальше, по мере рассмотрения наборов скриптов, возможностей расширения и разных настроек, будет показано, какие элементы файла-манифеста им соответствуют. Порядок расположения элементов в этом файле не имеет значения.
В целях безопасности, по умолчанию расширению доступен не весь API WebExtensions. Для того, чтобы открыть доступ к «небезопасным» средствам API, в файле-манифесте расширения нужно объявить соответствующие разрешения на доступ.
Такие разрешения добавляют с помощью элемента permissions в манифесте расширения. Если объявлены такие разрешения, при установке расширения пользователь увидит всплывающее окно, где будут перечислены дополнительные возможности, которые требует расширение, и вопрос, можно ли дать разрешение на доступ к ним. Если пользователь посчитает, что расширение требует слишком много возможностей и поэтому может быть опасным, он ответит «Не разрешать», и установка расширения будет отменена.
Некоторые разрешения лучше запросить позже, во время выполнения расширения. Например, пользователя может насторожить, что расширение при установке требует доступ к данным о его месте расположения. Лучше, чтобы расширение спросило разрешение в тот момент, когда эти данные будут использоваться. Для таких случаев используется элемент манифеста optional_permissions. Количество разрешений, доступных при его использовании, немного меньше, чем у элемента permissions
. Зато каждое из разрешений в этом списке будет запрашиваться только прямо перед использованием соответствующего средства API.
Список разрешений в манифесте представляет собой массив — как для элемента permissions
, так и для optional_permissions
. Разрешение на доступ к каждому из средств API даётся при добавлении специального ключевого слова в этот массив. Многие из этих слов совпадают с названиями средств API, к которым они дают доступ. Информацию о том, к каким возможностям можно открыть доступ, можно получить из списка ключевых слов для элемента permissions, а также из списка для элемента optional_permissions. Внимание: не забудьте посмотреть на таблицу совместимости с браузерами внизу этих страниц — она точнее показывает, какие ключевые слова можно использовать в действительности! Подробно узнать о средствах API, названных в этих списках, можно из описания всех средств API WebExtensions. Если в списке средств API не получается найти то, что нужно — просто добавляйте в расширение нужный функционал, читая документацию о соответствующих средствах API, и увидите, какие разрешения нужно добавить.
Изначально доступ скриптов расширения к содержимому веб-страниц немного ограничен. Чтобы снять ограничения, нужно дать разрешение на доступ к определённым сайтам (host permissions) или к активной вкладке браузера (activeTab
). В зависимости от целей, лучше выбрать один из этих видов разрешений.
Разрешение activeTab относительно безобидно. Оно даёт дополнительные полномочия для работы с активной вкладкой браузера, при этом не открывая слишком большой доступ к веб-сайту. Когда объявлено это разрешение, в активную вкладку можно добавлять скрипты содержимого из любых привилегированных скриптов, а также получать доступ к её адресу URL, заголовку и файлу иконки. Подобного эффекта можно добиться, если одновременно использовать разрешения <all_urls>
и tabs
, но тогда полномочия расширения зачастую будут больше, чем нужно.
Разрешения host permissions более потенциально опасны. В списке разрешений они обычно представлены как шаблоны адреса URL и дают расширению практически полный доступ к веб-сайтам, чей адрес URL совпадает с одним из этих шаблонов. К разрешённым таким образом сайтам можно добавлять скрипты содержимого из любых привилегированных скриптов, отправлять запросы AJAX из любого скрипта содержимого — даже из тех, которые были добавлены к странице другого сайта, а также читать и изменять данные HTTP-запросов и файлов cookie. Разрешение <all_urls>
тоже относится к host permissions. Оно даёт доступ ко всем сайтам сразу, поэтому его лучше использовать только в крайних случаях.
Не забудьте добавить в расширение страницу настроек, которая ещё называется страницей опций — особенно если расширение будет опубликовано для использования другими людьми. Когда пользователь установит расширение, откроет меню браузера и в нём выберет Дополнения -> Расширения (Менеджер Дополнений), он увидит список установленных расширений. В этом списке, при нажатии на кнопку (или ссылку, или пункт всплывающего меню) Настройки расширения, будет показана страница опций. На ней обычно дают краткую информацию о расширении, краткую инструкцию по использованию или ссылку на страницу помощи и, конечно же, меню настроек расширения. Если нужны примеры — можно установить какое-нибудь расширение и посмотреть его страницу опций.
Эту страницу создают таким же образом, как и обычную веб-страницу: файл HTML, к которому подключены CSS и JavaScript.
HTML-файл страницы опций подключают к расширению в файле манифеста с помощью элемента options_ui. Обычно страница опций открывается прямо в Менеджере Дополнений, в специально отведённом месте. Если нужно, чтобы эта страница открывалась в отдельной вкладке, добавьте "open_in_tab": true
в свойства элемента options_ui
.
Страница опций работает в отдельном контексте выполнения, который, конечно же, является привилегированным. Время жизни скриптов этой страницы — пока она открыта.
Более подробное описание страницы опций есть в статье Options page.
Для хранения настроек расширения используйте storage API.
Можно заметить, что в Менеджере Дополнений браузера у каждого расширения есть опция Подробности. В старых версиях Firefox (приблизительно до 68) она, как правило, открывала ту же самую страницу опций. В более свежих версиях браузера опция Подробности открывает вкладку с общей информацией о расширении. Причём краткое описание расширения берётся не из файла-манифеста расширения или из какого-то другого файла. Его нужно написать при регистрации расширения на сайте расширений Firefox.
Очень часто расширению нужен фоновый скрипт. Он работает всё время, пока работает браузер — при условии, что расширение установлено и включено.
Можно создать один или более таких файлов JavaScript, или целую фоновую HTML-страницу с подключёнными к ней скриптами (и даже файлами CSS!). Эти файлы добавляются в расширение с помощью элемента background файла манифеста. Конечно, фоновая страница никогда не будет видна пользователю, а её элементы DOM нельзя полноценно использовать в других контекстах выполнения. Поэтому польза от фонового файла HTML небольшая.
Файл HTML в качестве фоновой страницы может быть полезен для того, чтобы вынести список используемых в расширении фоновых скриптов в отдельный файл. Использовать фоновую страницу в качестве библиотек функций и HTML-элементов — обычно, плохая идея.
В API WebExtensions есть одна любопытная функция — browser.runtime.getBackgroundPage(). Она наталкивает на мысль, что фоновую страницу можно использовать как библиотеку элементов DOM или функций JavaScript. Но, во-первых, она ненадёжна: если она используется в скрипте всплывающего меню, и пользователь открыл веб-страницу в приватном режиме, функция возвращает пустое значение. Во-вторых, она вообще не работает в скриптах содержимого — в единственном месте, где действительно пригодилась бы библиотека HTML-элементов.
С помощью механизма сообщений WebExtensions тоже нельзя передать HTML-элементы и функции. Конечно, можно передавать HTML в виде текста, но тогда для превращения этой строки обратно в элемент DOM придётся создать специальную функцию или использовать дополнительные библиотеки (смотрите ответ на StackOverflow). Чаще всего для всплывающих меню и боковых панелей такая HTML-библиотека не нужна, а для скриптов, добавленных в страницу, есть способы добавить отдельный файл HTML.
Если нужно добавить в расширение библиотеки функций, лучше используйте элемент user_scripts манифеста расширения, который появился в Firefox с версии 68. Или подключайте нужные файлы JavaScript к каждому контексту выполнения.
Фоновый скрипт можно использовать для координации действий скриптов других контекстов выполнения: например, получать сообщения о событиях от одних контекстов выполнения и давать команды скриптам других контекстов. Механизм сообщений WebExtensions мы рассмотрим позже. В фоновом скрипте можно хранить данные о состоянии программы — флаги и параметры, которые изменяются во время работы расширения и могут быть сброшены в начальное состояние каждый раз при перезагрузке браузера. В этом случае обратите внимание на значение persistent
элемента background
в файле манифеста — установите его значение в true
. А бывает и так, что все основные действия выполняются в фоновом скрипте, и он является чуть ли ни единственным файлом JavaScript в расширении.
Вот в какие места вкладки браузера можно добавить элементы управления:
.addListener()
объекта-события browser.browserAction.onClicked. Чтобы получить больше информации о кнопке в панели инструментов — смотрите Toolbar Buttton на MDNО всплывающих меню можно больше узнать из статьи Popups на MDN
Каждому всплывающему меню, а также боковой панели, будет соответствовать отдельный контекст выполнения. Все эти контексты — привилегированные и «живут», пока видно соответствующее меню или боковая панель.
Как было сказано в описании контекстов выполнения, в любую веб-страницу, открытую во вкладке браузера, можно добавить код JavaScript и стили CSS. Скрипты, которые добавили к одной и той же веб-странице в одной вкладке браузера, работают в одном и том же контексте выполнения — независимо от того, в какой момент времени и каким способом они добавлены. В данной статье такой контекст выполнения называется контекстом содержимого.
Добавить скрипты к веб-странице можно сразу тремя способами:
permissions
)..then()
) получит в качестве аргумента специальный объект типа browser.contentScripts.RegisteredContentScript, связанный с добавленным «элементом списка». С помощью функции .unregister()
этого объекта можно удалить соответствующий элемент из списка. Для того, чтобы привилегированные скрипты могли добавлять скрипты содержимого к веб-страницам, расширению нужно разрешить более полный доступ к этим страницам. В элемент permissions файла-манифеста нужно добавить такие разрешения на доступ к сайтам (host permissions), чтобы они полностью покрывали список веб-страниц, к которым будут добавляться скрипты.Promise
. Для того, чтобы найти вкладку по URL открытой в ней страницы или по другому признаку, поможет функция browser.tabs.query(). Она вернёт объект типа Promise
, функция-обработчик успешной ветки которого получит массив с информацией о найденных вкладках. Из этого массива можно получить ID вкладок и использовать их в функции browser.tabs.executeScript()
. Конечно, чтобы добавлять скрипты данным способом, расширению нужно разрешить доступ к активной вкладке, добавив в элемент permissions
манифеста ключевое слово activeTab
, или к нужным сайтам, добавив шаблоны URL-адреса (host permissions).Привилегированные скрипты могут открывать в новых вкладках браузера HTML-файлы, которые находятся в составе расширения — с помощью функции browser.windows.create(). Если нужно добавить скрипты содержимого в такие страницы, расширению для этого не нужны никакие разрешения (permissions
) — независимо от того, каким способом добавляются скрипты.
При любом способе добавления скриптов можно задать момент, когда они начнут действовать. По умолчанию это время, когда страница и все подключённые к ней файлы загрузились, но можно указать и другое: когда страница всё ещё загружается, или когда основная HTML-страница уже получена, но подключённые к ней файлы ещё не загрузились. Подробности об этом есть в документации к API: если используется элемент content_scripts
в манифесте, нужно смотреть информацию о его поле run_at
, а для функций JavaScript, которые добавляют скрипты содержимого, — искать поле runAt
среди описания их аргументов.
Любой контекст содержимого, образованный скриптами, добавленными во вкладку браузера, «умирает» при начале перезагрузки страницы или при закрытии вкладки браузера (или окна браузера, или всего браузера).
Добавленные файлы CSS будут иметь такой приоритет, как будто они добавлены в заголовке веб-страницы с помощью элемента <link>
. А у JavaScript будут такие возможности:
window
. О том, как это делается, можно узнать в статье о скриптах содержимого на MDN. Важно помнить, что скрипты содержимого видят «чистый» DOM страницы, а значит у них свой объект window
, и поэтому:
Вот информация о скриптах содержимого на MDN.
Как уже было сказано, к содержимому веб-страницы можно добавлять только файлы CSS и JavaScript. Но, если очень хочется, всё-таки можно использовать файлы HTML. Правда, для этого придётся использовать некоторые хитрости — например, как в этом ответе на StackOverflow.
Можно назначить сочетания клавиш, при нажатии которых будут производиться какие-нибудь действия. Для этого нужно добавить элемент commands в файл-манифест.
Таким образом можно назначить группу сочетаний клавиш для одного имени события. Можно указать одно из специальных событий — тогда при нажатии горячих клавиш будет открываться боковая панель браузера, или нажиматься одна из кнопок в его панели инструментов. В этом случае само событие ввода горячих клавиш нельзя перехватить и обработать — браузер реагирует на них так же, как если бы пользователь нажал на кнопку в панели инструментов мышкой или открыл боковую панель через меню браузера. А можно добавить своё событие с любым именем. Его можно использовать в любом контексте выполнения — обработчик события можно добавить функцией browser.commands.onCommand.addListener().
Можно добавить строку-опцию в любое меню, которое появляется при нажатии правой кнопкой мыши в окне браузера. Для этого расширению нужно дать разрешение context_menus
. Такие опции можно добавить только из привилегированных контекстов выполнения, лучше всего — в фоновом скрипте.
Для взаимодействия с контекстными меню в Firefox используют API объекта browser.menus. У этого объекта есть ещё одно имя-синоним — browser.contextMenus
. Если планируется использовать расширение не только для браузера Firefox, но и для других, то лучше использовать это второе имя. Оно и было добавлено для совместимости расширений с другими браузерами.
Чтобы добавить опцию в контекстное меню, используют функцию browser.menus.create(). С помощью неё можно добавить опции в меню разных контекстов вызова (не путать с контекстами выполнения расширения). Например, можно добавить одни опции в меню, которое появляется при правом клике на пустом месте страницы, другие — на выделенном тексте, третьи — на пункте меню закладок.
На действия с добавленными опциями меню можно реагировать, добавив обработчики событий в любом привилегированном контексте выполнения. Например, можно добавить обработчик для события browser.menus.onClicked. Этот обработчик получит в качестве параметров все данные о пункте меню, выбранном пользователем, и об условиях, в которых вызвано это меню: в какой вкладке браузера, на каком HTML-элементе, какой текст при этом выделен и многое другое.
Если нужно сделать изменения в DOM веб-страницы, при правом клике на которой вызвано меню, в обработчике события нажатия на пункт меню нужно добавлять строку JavaScript к содержимому этой страницы — с помощью функции browser.tabs.executeScript(). В строку кода можно добавить все необходимые данные: например, выделенный текст или идентификатор HTML-элемента, на котором вызвано меню. Можно использовать пример для функции browser.menus.getTargetElement().
Для взаимодействия между контекстами выполнения используются два объекта из API WebExtensions. Объект browser.runtime предоставляет средства взаимодействия с общим рабочим окружением расширения. Через его API любые контексты выполнения могут взаимодействовать с привилегированными контекстами выполнения. Второй объект — это browser.tabs. Он служит для взаимодействия с системой вкладок браузера. С помощью API этого объекта привилегированные контексты выполнения могут обращаться к контекстам содержимого.
То есть при использовании API WebExtensions для взаимодействия между скриптами полезно помнить, что:
browser.runtime
.browser.tabs
.Все функции JavaScript, предназначенные для передачи сообщений, получают в качестве аргумента объект, который затем преобразуется в строку JSON и передаётся. На принимающей стороне происходит обратное преобразование. Поэтому такой объект должен быть совместимым с форматом JSON. То есть чтобы передаваемый объект дошёл до принимающей стороны в том же виде, в котором его отправили, он не должен содержать функции, значения undefined
, объекты типа Symbol
и другие специальные объекты. Например, элементы DOM не получится передать. Чтобы проверить, подходит ли объект для передачи, попробуйте преобразовать его в строку JSON с помощью функции JSON.stringify() и посмотрите, все ли части объекта присутствуют в получившейся строке.
Теперь рассмотрим два возможных способа общения между контекстами выполнения.
Главная особенность этого способа общения в том, что сообщение может быть отправлено или сразу всем привилегированным скриптам расширения, или скриптам содержимого, добавленным в заданную вкладку браузера. Кроме того, для разового обмена сообщениями нужно совершать минимум действий. Такой способ общения хорошо подходит для одновременного оповещения всех привилегированных скриптов о каком-нибудь событии или в случаях, когда нужно отправить всего несколько сообщений. Вот как происходит общение:
sendMessage()
. Если сообщение адресовано привилегированному скрипту, то это будет browser.runtime.sendMessage(), а если скрипту содержимого — то browser.tabs.sendMessage(). Этой функции передаётся объект-сообщение, и, если нужно, — другие параметры, которые указывают, куда и как должно быть доставлено это сообщение.onMessage
, а точнее — объект browser.runtime.onMessage. Он предназначен для получения сообщения любым скриптом из любого скрипта в пределах одного расширения. Этому событию должна быть назначена функция-обработчик — с помощью функции browser.runtime.onMessage.addListener()
. В качестве аргумента функции-обработчику будет передан объект-сообщение, который получен от другого контекста выполнения.Promise
. Описание механизма Promise можно почитать здесь. В зависимости от того, удалось сделать все нужные действия без ошибок или нет, при создании этого Promise
должна быть вызвана функция resolve()
или reject()
. Какая бы из этих функций ни использовалась, в качестве аргумента ей нужно передать объект-сообщение, совместимый с JSON. То есть в случае ошибки нельзя передавать объект типа Error
: он не дойдёт до получателя!sendMessage()
возвращает объект типа Promise
, к которому можно добавить обработчики для успешного и неудачного результата обмена сообщениями — с помощью его функций .then()
и .catch()
. В качестве аргумента каждая из этих функций-обработчиков получит объект-сообщение — ответ от другого контекста выполнения.Обратите внимание, что при использовании этого способа общения только разработчик расширения определяет условия удачного или неудачного обмена сообщениями, ведь ошибок связи здесь не бывает — могут быть только необработанные сообщения. Например, если в сообщении передана команда (придуманная разработчиком расширения), тогда именно от результата её выполнения другим скриптом должно зависеть, «успешная» ветка Promise
выбрана или «ошибочная».
Особенность такого способа в том, что общение происходит по установленному «каналу связи». Документация к API говорит, что в рамках одного соединения можно установить только один канал связи между двумя контекстами выполнения — несмотря на то, что теоретически можно установить много каналов от одного скрипта к другим. Этот способ хорошо подходит для обмена большим количеством информации. Общение происходит так:
connect()
. Точнее, если соединение устанавливается из любого контекста выполнения к привилегированному контексту, то используется функция browser.runtime.connect(), а если привилегированный скрипт соединяется со скриптом содержимого — функция browser.tabs.connect(). В качестве одного из параметров такой функции можно передать информацию о соединении, в которой можно указать имя соединения. Это может пригодиться, чтобы потом отличить соединение от других.connect()
возвращает объект типа browser.runtime.Port, через API которого будет осуществляться общение со скриптом на другом «конце» соединения. Будем считать, что в скрипте-инициаторе соединения порт сохранён в переменной с именем port1
.browser.runtime.onConnect.addListener()
. Когда другая сторона запрашивает соединение, этот обработчик выполняется и получает в качестве аргумента объект-порт типа browser.runtime.Port
— такой же, как тот, что вернула функция на стороне, установившей соединение. Допустим, этот порт присвоили переменной с именем port2
. Теперь любая из двух соединённых сторон может передавать и получать сообщения или разорвать соединение, используя свой объект-порт.postMessage()
своего порта (в нашем случае это port1.postMessage()
) и передаёт ей в качестве аргумента объект-сообщение.onMessage
порта (в нашем случае — port2.onMessage
). Как обычно, обработчик этого события назначается функцией port2.onMessage.addListener()
. Функция-обработчик получит в качестве аргумента принятый объект. Когда функция выполнит все необходимые действия, она может отправить ответ с помощью функции port2.postMessage()
..disconnect()
объекта-порта. Например, допустим, что инициатором соединения было всплывающее меню. Тогда будет правильно при его закрытии в обработчике события window.onclose()
вызвать функцию port1.disconnect()
. Эта функция не принимает параметров.port2.onDisconnect.addListener()
. Функции-обработчику разъединения будет передан новый объект-порт в качестве аргумента. Если всё-таки соединение было разорвано без использования функции .disconnect()
на другом конце, этот возвращённый объект будет содержать свойство .error
типа window.Error
. В нём будет содержаться сообщение об ошибке, которая привела к разрыву соединения. Свойство error
порта есть только в браузере Firefox, поэтому если расширение должно быть совместимым с другими браузерами, вместо этого проверяйте значение переменной browser.runtime.lastError.Если возможностей, которые предоставляет API WebExtensions в рамках браузера, окажется недостаточно, можно подключить внешнюю программу (также называемую «родной» или «native»), которая будет работать не внутри браузера, а прямо в операционной системе. Браузер будет запускать эту программу и обмениваться с ней сообщениями.
Все недостатки такого подхода вытекают из того, что у разных видов операционных систем API сильно отличается. Один из способов решения этой проблемы — создать универсальную программу на платформе, которая мало зависит от типа операционной системы (например, Python, Node.js или Java), учитывая некоторые особенности работы в разных системах. Второй — сделать отдельную программу для каждого вида операционной системы. В любом случае пользователю придётся выполнить больше действий для установки расширения, использующего внешнюю программу. Иногда могут возникнуть трудности с установкой.
Самые популярные способы создания внешней программы — сделать скрипт для интерпретатора языка Python или для платформы Node.js, но в принципе можно использовать любые технологии и языки. Можно связать с расширением и обычную программу, установленную на компьютере, которая не рассчитывалась для обмена сообщениями с браузером. Для этого программа должна поддерживать интерфейс командной строки. Кроме того, придётся сделать свою программу-переходник, которая будет общаться с браузером через его механизм взаимодействия, а нужную программу запускать как команду.
Полная инструкция об использовании внешней программы в расширении есть в статье Native messaging. Рассмотрим общие принципы установки такой программы, и чуть подробнее — как браузер взаимодействует с ней.
Внешняя программа, которую использует браузер, должна быть установлена в операционной системе, как и все остальные программы. Кроме того, при установке расширения, в специальной папке, которая находится где-то среди файлов настроек браузера, должен быть создан файл-манифест внешней программы (не путайте его с манифестом расширения). Этот файл нужен для того, чтобы браузер «знал», какие расширения могут использовать внешнюю программу и где находится её исполняемый файл. Конечно, для разных видов операционных систем и браузеров манифест внешней программы устанавливается по-разному. Подробности о том, как создать этот файл и установить его, читайте в статье Native Manifests.
Использование средств API WebExtensions для работы с внешней программой похоже на обмен сообщениями между частями расширения. Точно так же используется или разовый обмен сообщениями, или установка соединения. Но есть некоторые особенности.
Взаимодействовать с внешней программой могут только привилегированные скрипты.
Сообщения, которые расширение отправляет внешней программе, вводятся в виде объектов JavaScript, совместимых с JSON. При передаче сначала этот объект преобразуется в строку символов в кодировке UTF-8. Затем строка превращается в последовательность байт (массив), а перед ним ставится целое число размером 4 байта. Это число — количество байт в массиве. Вся эта последовательность байт передаётся в стандартный поток ввода внешней программы (обычно называемый stdin). Внешняя программа отправляет ответ в свой стандартный поток вывода (stdout) в том же совместимом с JSON формате, что и принятое сообщение. Браузер преобразовывает эти данные в объект JavaScript, который передаётся скрипту расширения.
Внешняя программа должна уметь делать такие же преобразования, как те, что делает браузер. Обычно данные, полученные программой через поток ввода, преобразуют в некоторый объект или структуру данных, которые отражают объект JSON — для того, чтобы сообщение было удобнее обрабатывать. При отправке ответа используют такой же объект или структуру данных, преобразуя их в последовательность байт, описанную выше, и отправляя в стандартный поток вывода.
Это происходит так:
name
в манифесте внешней программы) и объект-сообщение, которое нужно передать.browser.runtime.sendNativeMessage()
), возвращает объект типа Promise
. К этому объекту можно добавить обработчики событий: с помощью функций .then()
и .catch()
. Эти обработчики могут вести себя так:.then()
, и при этом он получает объект-сообщение в качестве параметра.null
или undefined
)..catch()
. Он бы получил в качестве параметра объект типа Error
с сообщением об ошибке.Встроенных средств для обработки ошибок, возникающих в процессе работы внешней программы, в таком механизме взаимодействия нет — расширение отслеживает только ошибки связи. Если нужно, чтобы ошибки и предупреждения, возникающие при работе внешней программы, были видны скриптам расширения, добавьте информацию об ошибке в сообщение, которое отправляет внешняя программа.
Можно установить соединение с внешней программой. Тогда взаимодействие с ней происходит так:
browser.runtime.connectNative()
, вызванная в скрипте расширения, возвращает объект типа browser.runtime.Port — точно такой же, как и в других случаях при связи, основанной на установке соединений. Назовём эту переменную port
.port.postMessage()
можно отправить сообщение внешней программе. port.onMessage.addListener()
. Обработчик события получает объект-сообщение в качестве параметра.port.disconnect()
, браузер тоже завершает работу внешней программы.port.onDisconnect
. В качестве параметра обработчику будет передан новый объект-порт. Если была ошибка, у этого объекта будет присутствовать элемент .error
, содержащий ошибку. Если используется браузер, основанный на Chromium, свойства-ошибки в объекте порта не будет — оно есть только в Firefox. Тогда нужно в обработчике события разъединения проверять содержимое переменной browser.runtime.lastError.Как и в предыдущем способе взаимодействия с внешней программой, если нужно обрабатывать ошибки внешней программы в скрипте расширения, разработчику прийдётся позаботиться об этом самостоятельно.
API WebExtensions предоставляет ещё много возможностей. Вот самые интересные из них:
В статьях о расширениях WebExtensions на сайте MDN часто упоминается инструмент web-ext. Это программа, работающая на платформе Node.js, которая помогает запускать расширение в браузере во время разработки, проверять синтаксис его файлов, а также упаковать файлы готового расширения в архив, готовый к опубликованию на сайте расширений Mozilla. Эта программа очень полезна в случаях, когда нужно протестировать расширение, создаваемое для мобильного браузера, или попробовать, как оно работает в разных версиях «настольного» браузера. Но на практике если нужно сделать расширение для «настольного» браузера Firefox той версии, что установлен в системе, и новее, то все эти действия можно делать вручную.
Информация об инструменте web-ext есть на сайте Extension Workshop. Мы рассмотрим, в основном, только как делать всё «вручную».
Для запуска расширения, когда оно ещё на стадии разработки, в виде набора файлов, нужно сделать так:
about:debugging
и нажать Enter на клавиатуре. Откроется страница Отладка с инструментами разработчика Firefox.Теперь расширение будет временно установлено в браузере. С помощью страницы about:debugging
можно в любое время отключить расширение или удалить его из браузера. При следующей перезагрузке браузера расширение «исчезнет» из него.
Когда расширение установлено, можно запустить его отладку:
about:debugging
нужно проверить, чтобы был установлен флажок Отладка дополнений.Эти средства отладки относятся к выбранному расширению. В их вкладке Инспектор будет показана фоновая страница этого расширения, а во всех остальных вкладках — данные о файлах и ресурсах, которые оно использует. В консоли будут показаны сообщения и ошибки, полученные от скриптов расширения. В зависимости от версии браузера, в этой же консоли могут появляться сообщения об ошибках и с загружаемых веб-страниц. Но для нормального отслеживания действий, которые выполняют скрипты, добавленные к содержимому веб-страницы, всегда нужно открывать ещё и обычные инструменты разработчика в соответствующей вкладке браузера.
Для упаковки расширения браузера Firefox рекомендуют использовать инструмент web-ext, но, вместо этого, можно вручную архивировать все файлы, относящиеся к расширению, в файл ZIP. Конечно, в этом файле не должно быть ничего лишнего: конфигурационных файлов GIT и других инструментов, файлов внешней программы, внешних ресурсов для Managed Storage и модулей безопасности PKCS#11.
После этого нужно зарегистрироваться на сайте дополнений браузера Firefox и там загрузить расширение. Смотрите подробную инструкцию по опубликованию на сайте Extension Workshop.
Надеюсь, что эта статья помогла понять устройство расширения браузера и узнать какие возможности доступны разработчику, и что этого будет достаточно, чтобы спроектировать расширение со всем нужным функционалом. Если хочется узнать больше о возможностях расширения, можно просмотреть документацию ко всем элементам манифеста расширения и к элементам API WebExtensions. Если в статье есть ошибки или неточности — пожалуйста, расскажите о них в комментариях.
Вот список «корневых» страниц, с которых можно начинать поиск информации:
Полезные статьи:
Полезные сервисы