Устройство расширений для браузера Firefox (WebExtensions)

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


    Тому, кто разбирается в веб-разработке, будет несложно создать новое расширение для браузера. Сейчас большинство самых популярных браузеров поддерживает стандартную систему разработки, которая использует в основном только JavaScript, HTML и CSS, — WebExtensions.


    Человеку, который никогда раньше не создавал дополнение для браузера на основе WebExtensions, может быть тяжело сразу понять, из каких основных частей оно должно состоять и что может делать. В сети Интернет есть много информации об этой системе, но для того, чтобы создать для себя общую картину, придётся потратить много времени. Эта статья поможет быстро разобраться в устройстве системы WebExtensions и покажет, как лучше ориентироваться в документации к её API. Здесь описывается расширение для браузера Firefox, поэтому почти вся информация, используемая в статье, взята с сайта MDN. Но статья будет полезна и тем, кто хочет создать расширение для других браузеров, поддерживающих WebExtensions, — в первую очередь для Google Chrome и Chromium.


    Здесь рассматривается создание расширений только для настольных компьютеров. Если нужно создать расширение для мобильного браузера Chrome или Firefox, эта статья тоже может быть чем-то полезной, но основную часть информации придётся найти и изучить самостоятельно.



    Вступление


    Что нужно, чтобы начать


    А нужно вот что:


    • Браузер Firefox — лучше всего версии 60 или выше. Во всей статье подразумевается, что используется именно такой браузер, если не сказано другое. Но можно вместо этого использовать Google Chrome или Chromium.
    • Знания в HTML и CSS, достаточные для создания хотя бы несложных веб-страниц.
    • Знания Javascript. Не обязательно в совершенстве знать последний стандарт ECMA, но как работать с Promise нужно знать.
    • Знания английского языка, достаточные для чтения документации к API. Информации, переведённой на русский язык, на эту тему мало (на момент написания статьи), и иногда она может быть неполной. Но всё-таки лучше попробуйте переключать язык на «английских» страницах MDN — может быть, там уже есть перевод на русский язык.
    • Иногда при создании расширения могут пригодиться знания языка Python или платформы Node.js.
    • И, конечно же, нужно желание научиться чему-то новому и написать своё расширение для браузера.

    О браузерах


    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


    Теперь рассмотрим основные понятия и особенности, связанные с созданием расширения в системе WebExtensions, с которыми придётся часто сталкиваться и в данной статье, и при чтении документации на MDN. Почти все ссылки этого раздела тоже ведут к разным местам текущей статьи.


    API WebExtensions: точка входа


    Любому скрипту расширения доступен глобальный объект, который содержит все свойства и функции API WebExtensions, доступные в этом скрипте. В расширениях для браузера Firefox он называется browser, а для Google Chromechrome. Но главный, родительский объект для скриптов расширения — это всегда 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.


    Все контексты выполнения можно разделить на две группы. В первую группу входит всего один вид контекстов — контексты содержимого веб-страниц. В каждом из них работает набор скриптов, добавленных нашим расширением к загруженной с сервера веб-странице. То есть каждой вкладке браузера, в которую наше расширение добавило скрипты, соответствует один такой контекст выполнения. Во вторую группу входят все остальные, привилегированные контексты. В них работают те части расширения, которые добавляют функционал к самой программе браузера. Например, всплывающее меню, боковая панель, фоновый скрипт (или «фоновая страница») — работают в таких контекстах.



    Привилегированные контексты выполнения


    У привилегированных контекстов выполнения есть такие особенности:


    • Им доступен весь API WebExtensions, к которому разрешён доступ в файле манифеста. Именно поэтому в документации на сайте MDN они всегда называются привилегированными.
    • Содержимое любого из этих контекстов можно создать или в виде HTML-документа, к которому могут быть подключены файлы CSS и JavaScript, или как набор одних только файлов JavaScript. Во многих случаях HTML-документ подходит лучше. Например, содержимое добавленного в браузер всплывающего меню или боковой панели удобнее создавать тем же способом, что и обычные веб-страницы. Внимание: не используйте в таком HTML-файле код JavaScript, который находится непосредственно в элементе <script>, — вместо этого всегда загружайте файлы с помощью <script src="my_script.js"></script>! Иначе браузер может выдавать ошибки и будет тяжело сразу понять, откуда они взялись.
    • У скриптов этих контекстов нет прямого доступа к содержимому веб-страницы, открытой во вкладке браузера. Объект window такого контекста даёт доступ только к его собственному HTML-документу.
    • Любой привилегированный скрипт может добавлять JavaScript в контекст содержимого веб-страницы, открытой в любой вкладке или окне браузера. Для этого расширению обычно нужно дать одно из подходящих разрешений на доступ. Если нужно добавить скрипт во вкладку браузера, где открыт файл HTML, входящий в состав данного расширения, то разрешение не нужно. Подробнее об этом — в разделе о скриптах содержимого.


    Контексты содержимого (не привилегированные)


    Контекст содержимого веб-страницы отличается такими особенностями:


    • Ему доступна только небольшая часть API WebExtensions, поэтому такие контексты называют не привилегированными.
    • Скрипты содержимого добавляются к уже существующей загруженной в браузер веб-странице. Поэтому их контекст выполнения нельзя создать с помощью файла HTML — он заменил бы собой загруженную веб-страницу. Можно добавить только JavaScript и CSS.
    • Все скрипты такого контекста имеют доступ к DOM страницы, к которой они добавлены — как обычно, с помощью API объекта 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.


    Основные элементы манифеста


    Файл манифеста лучше всего начинать с общего описания расширения, а именно — с трёх обязательных элементов:


    • manifest_version — в этом элементе всегда должно быть число 2.
    • name — строка, полное название расширения.
    • version — версия расширения в виде строки.

    На всякий случай можно добавить ещё и сокращённое имя расширения — с помощью элемента short_name. Оно будет показываться там, где места для полного имени не хватает.


    Следующим лучше добавить краткое описание расширения — с помощью элемента description


    Информация об авторе


    Ещё нужно добавить информацию об авторе. Для этого, обычно, используют такие элементы манифеста:


    • author — имя автора. Обычно здесь указывают короткое имя, по которому будут узнавать разработчика, или полные имя и фамилию, или название организации.
    • homepage_url — URL-адрес домашней страницы расширения. Обычно здесь указывают адрес веб-страницы на личном сайте автора, посвящённой расширению, или целого сайта о расширении, или адрес проекта на GitHub.

    Вместо этих двух элементов можно использовать один элемент-группу developer. Он делает то же самое, только выглядит немного иначе.


    Идентификатор расширения, поддерживаемые версии браузера


    Теперь можно добавить элемент browser_specific_settings.


    Во многих расширениях он называется по-старому — applications

    В старых браузерах Firefox — до версии 48 — этот элемент был обязательным, и до версии 42 он назывался applications. Сейчас можно использовать как browser_specific_settings, так и applications, но многие расширения используют applications. Возможно так получилось потому, что эти расширения написаны уже давно и несколько раз их адаптировали для браузера новой версии, а это имя изменять было необязательно. А может и потому, что имя applications часто встречается в примерах расширений на MDN.



    Этот элемент необязателен и поддерживается только браузером Firefox, но часто бывает полезным. С помощью него можно указать диапазон версий браузера, в которых расширение может работать, а также ID расширения. Если в расширении используется внешняя программа (которую мы рассмотрим позже) или взаимодействие с другими расширениями, то этот 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


    В целях безопасности, по умолчанию расширению доступен не весь 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. Оно даёт доступ ко всем сайтам сразу, поэтому его лучше использовать только в крайних случаях.



    Страница настроек расширения (options page)


    Не забудьте добавить в расширение страницу настроек, которая ещё называется страницей опций — особенно если расширение будет опубликовано для использования другими людьми. Когда пользователь установит расширение, откроет меню браузера и в нём выберет Дополнения -> Расширения (Менеджер Дополнений), он увидит список установленных расширений. В этом списке, при нажатии на кнопку (или ссылку, или пункт всплывающего меню) Настройки расширения, будет показана страница опций. На ней обычно дают краткую информацию о расширении, краткую инструкцию по использованию или ссылку на страницу помощи и, конечно же, меню настроек расширения. Если нужны примеры — можно установить какое-нибудь расширение и посмотреть его страницу опций.


    Эту страницу создают таким же образом, как и обычную веб-страницу: файл 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 в качестве фоновой страницы может быть полезен для того, чтобы вынести список используемых в расширении фоновых скриптов в отдельный файл. Использовать фоновую страницу в качестве библиотек функций и 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 в расширении.



    Добавление элементов управления в браузер


    Вот в какие места вкладки браузера можно добавить элементы управления:


    Окно браузера с указателями на элементы управления
    1. В верхней панели инструментов браузера Firefox, справа, между строкой адреса (или поиска) и кнопкой Открыть Меню, обычно есть кнопки управления, среди которых могут быть Просмотр истории, Показать закладки и другие. Можно добавить рядом с ними свою кнопку. Для этого в файл манифеста нужно добавить элемент browser_action. Эту кнопку можно использовать для вызова всплывающего меню, которое можно создать подобно веб-странице — с помощью HTML, CSS и JavaScript. Как и другие всплывающие меню браузера, оно будет закрываться при нажатии мышкой за его пределами. Если всплывающее меню не нужно — добавьте в фоновый скрипт обработчик события нажатия на кнопку с помощью функции .addListener() объекта-события browser.browserAction.onClicked. Чтобы получить больше информации о кнопке в панели инструментов — смотрите Toolbar Buttton на MDN
    2. Точно таким же образом можно добавить кнопку в строке для ввода URL в панели инструментов браузера. Для этого используется другой элемент файла манифеста — page_action. Эта кнопка тоже может открывать всплывающее окно. Если кнопка не должна вызывать окно, обработчик в фоновом скрипте добавляется для события browser.pageAction.onClicked. Больше информации о кнопке в строке адреса — на странице Address bar button.
    3. У браузера Firefox есть встроенные боковые панели, которые можно открыть с помощью кнопки Показать боковые панели в панели инструментов браузера или через меню окна браузера Вид -> Боковая панель. Можно создать свою боковую панель, добавив в файл манифеста элемент sidebar_action. Эта панель откроется автоматически после установки расширения — так браузер показывает, что оно использует боковую панель. Правда, её нельзя будет открывать автоматически с помощью скриптов расширения — как и встроенные боковые панели. Зато можно назначить горячие клавиши (подробнее о которых — позже), чтобы пользователь мог быстро её открыть. Как и всплывающее меню, содержимое боковой панели можно создать подобно странице веб-сайта. Больше о боковых панелях можно узнать на странице Sidebars

    О всплывающих меню можно больше узнать из статьи Popups на MDN


    Каждому всплывающему меню, а также боковой панели, будет соответствовать отдельный контекст выполнения. Все эти контексты — привилегированные и «живут», пока видно соответствующее меню или боковая панель.



    Добавление скриптов в веб-страницы


    Как было сказано в описании контекстов выполнения, в любую веб-страницу, открытую во вкладке браузера, можно добавить код JavaScript и стили CSS. Скрипты, которые добавили к одной и той же веб-странице в одной вкладке браузера, работают в одном и том же контексте выполнения — независимо от того, в какой момент времени и каким способом они добавлены. В данной статье такой контекст выполнения называется контекстом содержимого.


    Добавить скрипты к веб-странице можно сразу тремя способами:


    • Добавить в файл манифеста элемент content_scripts. С помощью него можно добавить как файлы JavaScript, так и CSS. Этот элемент должен содержать массив объектов, в каждом из которых определён набор шаблонов URL-адреса и соответствующий ему набор имён файлов скриптов. Когда веб-страница загружается в браузер, её URL-адрес сравнивается с этими списками шаблонов. Когда (или если) в одном из объектов найдено совпадение, к странице добавляются все файлы, определённые внутри этого объекта. Для использования этого способа расширению не нужны разрешения на доступ (permissions).
    • Есть ещё один способ. Он похож на предыдущий: с помощью него тоже собирается некоторый список наборов шаблонов URL и соответствующих им наборов скриптов. Но элементы этого списка можно добавлять и удалять прямо во время работы расширения, в любом привилегированном скрипте. То есть шаблоны и списки скриптов для них могут быть изменены в зависимости от режима работы или состояния программы. Можно добавлять и JavaScript, и CSS, причём это могут быть как файлы, так и скрипты в виде строк текста. Для этого способа используется API объекта browser.contentScripts. С помощью функции browser.contentScripts.register() в некоторый список в памяти компьютера добавляется набор шаблонов URL и соответствующие ему скрипты. Эта функция возвращает объект типа Promise. Обработчик «успешной» ветки этого промиса (добавленный с помощью функции .then()) получит в качестве аргумента специальный объект типа browser.contentScripts.RegisteredContentScript, связанный с добавленным «элементом списка». С помощью функции .unregister() этого объекта можно удалить соответствующий элемент из списка. Для того, чтобы привилегированные скрипты могли добавлять скрипты содержимого к веб-страницам, расширению нужно разрешить более полный доступ к этим страницам. В элемент permissions файла-манифеста нужно добавить такие разрешения на доступ к сайтам (host permissions), чтобы они полностью покрывали список веб-страниц, к которым будут добавляться скрипты.
    • Следующий способ тоже добавляет скрипт во вкладку браузера из любого привилегированного скрипта. Он позволяет добавлять только JavaScript — или в виде отдельного файла, или как текст кода. Здесь используется функция browser.tabs.executeScript(). С помощью неё можно добавить скрипт в активную вкладку браузера, а можно и в любую другую, если знать её идентификатор (ID). Эта функция возвращает 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 будут такие возможности:

    • Доступ к DOM веб-страницы, к которой добавлен скрипт.
    • Эти скрипты являются не привилегированными и имеют доступ к небольшому подмножеству API WebExtensions. Вот список доступных средств API.
    • Могут отправлять запросы AJAX (с помощью XMLHttpRequest или Fetch). Причём если расширению дано разрешение на доступ к определённым веб-сайтам (host permissions), то к веб-серверам этих сайтов можно отправлять запросы AJAX даже из тех скриптов содержимого, которые добавлены к страницам других сайтов.
    • Если нужно более тесное взаимодействие между «родным» скриптом веб-страницы и скриптом содержимого, можно использовать обмен сообщениями через API объекта window. О том, как это делается, можно узнать в статье о скриптах содержимого на MDN.

    Важно помнить, что скрипты содержимого видят «чистый» DOM страницы, а значит у них свой объект window, и поэтому:


    • Добавленный скрипт не видит объекты JavaScript, созданные «родными» скриптами веб-страницы.
    • Объекты, созданные добавленными скриптами, не видны «родным» скриптам страницы.
    • Но и у скриптов содержимого, и у «родных» скриптов веб-страницы, работающих в одной вкладке браузера, общая DOM. Это значит, что если скрипт содержимого изменит, добавит или удалит элемент веб-страницы, это сражу же отразится в «родном» контексте выполнения страницы, и наоборот.

    Вот информация о скриптах содержимого на 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


    Для взаимодействия между контекстами выполнения используются два объекта из API WebExtensions. Объект browser.runtime предоставляет средства взаимодействия с общим рабочим окружением расширения. Через его API любые контексты выполнения могут взаимодействовать с привилегированными контекстами выполнения. Второй объект — это browser.tabs. Он служит для взаимодействия с системой вкладок браузера. С помощью API этого объекта привилегированные контексты выполнения могут обращаться к контекстам содержимого.


    То есть при использовании API WebExtensions для взаимодействия между скриптами полезно помнить, что:


    • Для обращения к привилегированным скриптам из любой другой части расширения используется API объекта browser.runtime.
    • Для обращения к контекстам содержимого из привилегированного контекста нужно использовать API из browser.tabs.
    • Скрипты содержимого, внедрённые в разные веб-страницы (то есть работающие в разных вкладках браузера), не могут общаться друг с другом напрямую.

    Формат сообщения


    Все функции JavaScript, предназначенные для передачи сообщений, получают в качестве аргумента объект, который затем преобразуется в строку JSON и передаётся. На принимающей стороне происходит обратное преобразование. Поэтому такой объект должен быть совместимым с форматом JSON. То есть чтобы передаваемый объект дошёл до принимающей стороны в том же виде, в котором его отправили, он не должен содержать функции, значения undefined, объекты типа Symbol и другие специальные объекты. Например, элементы DOM не получится передать. Чтобы проверить, подходит ли объект для передачи, попробуйте преобразовать его в строку JSON с помощью функции JSON.stringify() и посмотрите, все ли части объекта присутствуют в получившейся строке.


    Теперь рассмотрим два возможных способа общения между контекстами выполнения.


    Разовый обмен сообщениями


    Главная особенность этого способа общения в том, что сообщение может быть отправлено или сразу всем привилегированным скриптам расширения, или скриптам содержимого, добавленным в заданную вкладку браузера. Кроме того, для разового обмена сообщениями нужно совершать минимум действий. Такой способ общения хорошо подходит для одновременного оповещения всех привилегированных скриптов о каком-нибудь событии или в случаях, когда нужно отправить всего несколько сообщений. Вот как происходит общение:


    1. В контексте выполнения, из которого нужно передать сообщение, вызывается функция sendMessage(). Если сообщение адресовано привилегированному скрипту, то это будет browser.runtime.sendMessage(), а если скрипту содержимого — то browser.tabs.sendMessage(). Этой функции передаётся объект-сообщение, и, если нужно, — другие параметры, которые указывают, куда и как должно быть доставлено это сообщение.
    2. На принимающей стороне используется событие onMessage, а точнее — объект browser.runtime.onMessage. Он предназначен для получения сообщения любым скриптом из любого скрипта в пределах одного расширения. Этому событию должна быть назначена функция-обработчик — с помощью функции browser.runtime.onMessage.addListener(). В качестве аргумента функции-обработчику будет передан объект-сообщение, который получен от другого контекста выполнения.
    3. Функция-обработчик совершает некоторые действия, после чего она должна вернуть объект типа Promise. Описание механизма Promise можно почитать здесь. В зависимости от того, удалось сделать все нужные действия без ошибок или нет, при создании этого Promise должна быть вызвана функция resolve() или reject(). Какая бы из этих функций ни использовалась, в качестве аргумента ей нужно передать объект-сообщение, совместимый с JSON. То есть в случае ошибки нельзя передавать объект типа Error: он не дойдёт до получателя!
    4. На стороне скрипта, передавшего сообщение, функция sendMessage() возвращает объект типа Promise, к которому можно добавить обработчики для успешного и неудачного результата обмена сообщениями — с помощью его функций .then() и .catch(). В качестве аргумента каждая из этих функций-обработчиков получит объект-сообщение — ответ от другого контекста выполнения.

    Обратите внимание, что при использовании этого способа общения только разработчик расширения определяет условия удачного или неудачного обмена сообщениями, ведь ошибок связи здесь не бывает — могут быть только необработанные сообщения. Например, если в сообщении передана команда (придуманная разработчиком расширения), тогда именно от результата её выполнения другим скриптом должно зависеть, «успешная» ветка Promise выбрана или «ошибочная».


    Общение с установлением соединения


    Особенность такого способа в том, что общение происходит по установленному «каналу связи». Документация к API говорит, что в рамках одного соединения можно установить только один канал связи между двумя контекстами выполнения — несмотря на то, что теоретически можно установить много каналов от одного скрипта к другим. Этот способ хорошо подходит для обмена большим количеством информации. Общение происходит так:


    1. Сначала одна из сообщающихся сторон должна установить соединение с помощью функции connect(). Точнее, если соединение устанавливается из любого контекста выполнения к привилегированному контексту, то используется функция browser.runtime.connect(), а если привилегированный скрипт соединяется со скриптом содержимого — функция browser.tabs.connect(). В качестве одного из параметров такой функции можно передать информацию о соединении, в которой можно указать имя соединения. Это может пригодиться, чтобы потом отличить соединение от других.
    2. Любая функция connect() возвращает объект типа browser.runtime.Port, через API которого будет осуществляться общение со скриптом на другом «конце» соединения. Будем считать, что в скрипте-инициаторе соединения порт сохранён в переменной с именем port1.
    3. В скрипте другого контекста выполнения, с которым устанавливается связь, используется событие browser.runtime.onConnect — независимо от того, привилегированный это скрипт или нет, и от скрипта какого вида нужно получить запрос на соединение. Этому событию назначается функция-обработчик — с помощью функции browser.runtime.onConnect.addListener(). Когда другая сторона запрашивает соединение, этот обработчик выполняется и получает в качестве аргумента объект-порт типа browser.runtime.Port — такой же, как тот, что вернула функция на стороне, установившей соединение. Допустим, этот порт присвоили переменной с именем port2. Теперь любая из двух соединённых сторон может передавать и получать сообщения или разорвать соединение, используя свой объект-порт.
    4. Допустим, первая сторона должна передать сообщение. Тогда она вызывает функцию postMessage() своего порта (в нашем случае это port1.postMessage()) и передаёт ей в качестве аргумента объект-сообщение.
    5. На другой стороне соединения используется событие onMessage порта (в нашем случае — port2.onMessage). Как обычно, обработчик этого события назначается функцией port2.onMessage.addListener(). Функция-обработчик получит в качестве аргумента принятый объект. Когда функция выполнит все необходимые действия, она может отправить ответ с помощью функции port2.postMessage().
    6. Таким образом если на одной из сторон при получении сообщения сразу же передавать сообщение в ответ, получится общение типа «запрос-ответ». А если нужна обработка ошибок, придётся придумать, как отличить объект-сообщение, содержащий данные об ошибке, от обычного сообщения: например, всегда добавлять в сообщение поле «status» со значением «error» или «ok».
    7. «Канал связи» может быть разорван любой стороной соединения с помощью функции .disconnect() объекта-порта. Например, допустим, что инициатором соединения было всплывающее меню. Тогда будет правильно при его закрытии в обработчике события window.onclose() вызвать функцию port1.disconnect(). Эта функция не принимает параметров.
    8. Скрипт на другой стороне может обрабатывать событие разъединения, если ему при этом нужно сделать какие-то действия. Обработчик этого события добавляется с помощью функции 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 — для того, чтобы сообщение было удобнее обрабатывать. При отправке ответа используют такой же объект или структуру данных, преобразуя их в последовательность байт, описанную выше, и отправляя в стандартный поток вывода.


    Разовый обмен сообщениями


    Это происходит так:


    1. В скрипте расширения вызывается функция browser.runtime.sendNativeMessage(). Она принимает в качестве параметров название программы (которое определено с помощью атрибута name в манифесте внешней программы) и объект-сообщение, которое нужно передать.
    2. Браузер запускает внешнюю программу и передаёт в её поток stdin строку сообщения, созданную из объекта, который нужно передать.
    3. Внешняя программа выполняется и при этом обрабатывает полученное сообщение.
    4. Внешняя программа подготавливает ответ и отправляет его в поток stdout.
    5. Внешняя программа самостоятельно завершает работу! Только после этого браузер получает ответ.
    6. Функция отправки сообщения, которая была вызвана в расширении (browser.runtime.sendNativeMessage()), возвращает объект типа Promise. К этому объекту можно добавить обработчики событий: с помощью функций .then() и .catch(). Эти обработчики могут вести себя так:
      • В нашем случае, сообщение принято нормально. Поэтому вызывается обработчик, добавленный функцией .then(), и при этом он получает объект-сообщение в качестве параметра.
      • Если бы программа завершила работу, но ничего не передала, то сработал бы тот же самый обработчик события, но параметр, переданный ему, был бы пустым (null или undefined).
      • Если бы браузер вообще не смог запустить внешнюю программу, выполнился бы обработчик, добавленный с помощью .catch(). Он бы получил в качестве параметра объект типа Error с сообщением об ошибке.


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


    Установление соединения с внешней программой


    Можно установить соединение с внешней программой. Тогда взаимодействие с ней происходит так:


    1. Скрипт расширения вызывает функцию browser.runtime.connectNative() с единственным параметром — именем внешней программы (определённым в манифесте внешней программы).
    2. Браузер запускает внешнюю программу. Эта программа не должна выполниться и закрыться. Вместо этого она должна работать постоянно, используя бесконечный цикл!
    3. Функция browser.runtime.connectNative(), вызванная в скрипте расширения, возвращает объект типа browser.runtime.Port — точно такой же, как и в других случаях при связи, основанной на установке соединений. Назовём эту переменную port.
    4. С помощью функции port.postMessage() можно отправить сообщение внешней программе.
    5. Внешняя программа должна прослушивать свой стандартный поток ввода (stdin) и получить через него сообщение от браузера.
    6. Обработав сообщение и отреагировав на него, внешняя программа может отправить сообщение в ответ — в свой поток stdout.
    7. Когда браузер принимает сообщение, в скрипте расширения срабатывают обработчики события порта. Такую функцию-обработчик добавляют с помощью функции port.onMessage.addListener(). Обработчик события получает объект-сообщение в качестве параметра.
    8. Внешняя программа может завершить работу в одном из таких случаев:
      • Внешняя программа так устроена, что в определённых условиях сама завершила работу. Или произошла ошибка, в результате которой программа завершилась.
      • Если переменная-порт в скрипте расширения перестала существовать, браузер завершает работу внешней программы.
      • Если скрипт расширения вызвал функцию port.disconnect(), браузер тоже завершает работу внешней программы.

    9. В любом из этих случаев срабатывает обработчик события port.onDisconnect. В качестве параметра обработчику будет передан новый объект-порт. Если была ошибка, у этого объекта будет присутствовать элемент .error, содержащий ошибку. Если используется браузер, основанный на Chromium, свойства-ошибки в объекте порта не будет — оно есть только в Firefox. Тогда нужно в обработчике события разъединения проверять содержимое переменной browser.runtime.lastError.

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



    Больше возможностей


    API WebExtensions предоставляет ещё много возможностей. Вот самые интересные из них:


    • Для хранения настроек расширения и других данных используют API browser.storage.
    • Часто бывает нужно добавить в расширение поддержку нескольких языков. Как это сделать, читайте в статье Internationalization.
    • Можно использовать механизм загрузок браузера: скачивать файлы в указанное место внутри папки загрузок, открывать скачанный файл в ассоциированной с ним программе, показывать папку со скачанными файлами в файловом менеджере операционной системы и кое-что ещё. Смотрите информацию об API browser.downloads.
    • Можно добавить служебное окно к интерфейсу браузера — например, чтобы показывать настройки, помощь или сообщение в отдельном окне. А можно добавить ещё одну панель в окно браузера. Смотрите статью Страницы расширения и информацию о функции browser.windows.create().
    • Можно открывать новые окна браузера. В них можно открывать любые веб-страницы или перемещать туда вкладки браузера, которые уже открыты. Для этого тоже используют функцию browser.windows.create().
    • Можно создать свою тему оформления браузера — с помощью элемента theme манифеста расширения.
    • Можно изменять или блокировать HTTP-запросы на любой стадии. Для этого используют API browser.webRequest.
    • Можно организовать общение между привилегированными скриптами разных расширений. При этом для отправки сообщения используется функция browser.runtime.sendMessage(), а для принятия — событие browser.runtime.onMessageExternal.
    • Можно расширить функционал стандартных средств разработчика браузера, добавив в них свою вкладку. Подробности читайте в статье Extending the developer tools.

    Запуск и отладка расширения


    Инструмент web-ext


    В статьях о расширениях WebExtensions на сайте MDN часто упоминается инструмент web-ext. Это программа, работающая на платформе Node.js, которая помогает запускать расширение в браузере во время разработки, проверять синтаксис его файлов, а также упаковать файлы готового расширения в архив, готовый к опубликованию на сайте расширений Mozilla. Эта программа очень полезна в случаях, когда нужно протестировать расширение, создаваемое для мобильного браузера, или попробовать, как оно работает в разных версиях «настольного» браузера. Но на практике если нужно сделать расширение для «настольного» браузера Firefox той версии, что установлен в системе, и новее, то все эти действия можно делать вручную.


    Информация об инструменте web-ext есть на сайте Extension Workshop. Мы рассмотрим, в основном, только как делать всё «вручную».


    Пробный запуск расширения


    Для запуска расширения, когда оно ещё на стадии разработки, в виде набора файлов, нужно сделать так:


    • Ввести в адресной строке браузера about:debugging и нажать Enter на клавиатуре. Откроется страница Отладка с инструментами разработчика Firefox.
    • На этой странице нажать на кнопку Загрузить временное дополнение.
    • В появившемся окне диалога выбрать файл-манифест расширения, которое нужно загрузить.

    Теперь расширение будет временно установлено в браузере. С помощью страницы about:debugging можно в любое время отключить расширение или удалить его из браузера. При следующей перезагрузке браузера расширение «исчезнет» из него.


    Отладка


    Когда расширение установлено, можно запустить его отладку:


    • На странице about:debugging нужно проверить, чтобы был установлен флажок Отладка дополнений.
    • После этого в строке, соответствующей любому расширению, нажимаем на кнопку Отладка.
    • Может появиться диалоговое окно, где нужно разрешить запуск средств отладки.
    • Теперь будет показано окно отладки, по функциональности очень похожее на панель стандартных инструментов разработчика в браузере, которая открывается при нажатии клавиши F12.

    Эти средства отладки относятся к выбранному расширению. В их вкладке Инспектор будет показана фоновая страница этого расширения, а во всех остальных вкладках — данные о файлах и ресурсах, которые оно использует. В консоли будут показаны сообщения и ошибки, полученные от скриптов расширения. В зависимости от версии браузера, в этой же консоли могут появляться сообщения об ошибках и с загружаемых веб-страниц. Но для нормального отслеживания действий, которые выполняют скрипты, добавленные к содержимому веб-страницы, всегда нужно открывать ещё и обычные инструменты разработчика в соответствующей вкладке браузера.


    Как упаковать и опубликовать расширение


    Для упаковки расширения браузера Firefox рекомендуют использовать инструмент web-ext, но, вместо этого, можно вручную архивировать все файлы, относящиеся к расширению, в файл ZIP. Конечно, в этом архиве не должно быть ничего лишнего: файлов, относящихся к GIT, NPM и к другим инструментам разработчика, скриптов для тестирования, файлов внешней программы, внешних ресурсов для Managed Storage и модулей безопасности PKCS#11.


    После этого нужно зарегистрироваться на сайте дополнений браузера Firefox и там загрузить расширение. Смотрите подробную инструкцию по опубликованию на сайте Extension Workshop.


    Итог


    Надеюсь, что эта статья помогла понять устройство расширения браузера и узнать какие возможности доступны разработчику, и что этого будет достаточно, чтобы спроектировать расширение со всем нужным функционалом. Если хочется узнать больше о возможностях расширения, можно просмотреть документацию ко всем элементам манифеста расширения и к элементам API WebExtensions. Если в статье есть ошибки или неточности — пожалуйста, расскажите о них в комментариях.


    Полезные ссылки


    Вот список «корневых» страниц, с которых можно начинать поиск информации:



    Полезные статьи:



    Полезные сервисы


    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 2

      0
      Буквально на днях заинтересовался написание своего расширения. Лично для меня статья оказалась очень полезной. Так как я вообще не знал из чего оно состоит и что с ним делать.

      Спасибо за статью
        0
        Пожалуйста. Рад, что смог помочь.

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

      Самое читаемое