на чем пишут расширения для chrome
Создаём своё расширение для Google Chrome
На хабре уже есть несколько статей о создании расширений для хрома, поделюсь своим опытом, затронув основные вещи и места, в которых у меня возникли трудности.
Что понадобится для создания расширения в двух словах:
1) Базовые знания Javascript
2) Базовые знания HTML
3) 5$
Покажу создание расширения для хрома на примере своего, созданного для подсчета «потерянного времени» в интернете. То есть это расширение считает время, проведенное на сайтах с возможностью определения посещенных сайтов по категориям: полезное время, либо потерянное время.
Итак, я начинаю создание расширения с создания папки самого расширения, в которую будем класть все создаваемые нами файлы. Назову её «losttime». Далее, я создаю файл manifest.json, выглядит он следующим образом:
Некоторые из строк должны быть интуитивно понятны, но что обязательно нужно знать:
— Значение manifest_version должно быть обязательно «2»;
— В content_scripts пишем, какой скрипт будет запускаться на всех страницах отдельно;
— В background пишем общий скрипт(фоновый скрипт), который запускается при запуске браузера;
— В permissions пишем адрес сайта, с которого будет браться информация.
Все, что буду использовать я, не обязательно использовать Вам, если вам это по логике просто не нужно. Подробнее о манифесте.
То самое окошко, которое Вы можете видеть по клику на иконку расширения — это страница: popup.html.
Она у меня выглядит следующим образом:
Чтобы было понятнее, описание кода вставил в самом HTML. Меню я организую просто: на картинку ставлю внутреннюю ссылку расширения.
Раз уж начал про popup.html, то расскажу сразу и о popup.js
Выглядит он у меня весьма просто:
Описание кода также вставил.
Именно описанная выше конструкция позволяет вытащить и вывести содержание с Вашего, а может и не с Вашего сайта. Но, что важно знать:
— В файле манифеста обязательно в поле permissions пишем адрес сайта, с которого будет браться информация.
— Файл popup.js связан с фоновым скриптом background.js, т.к. данные, занесенные в локальное хранилище на background.js, видны и на popup.js.
Перед тем, как рассмотреть файл фонового скрипта background.js, рассмотрим файл скрипта, который запускается на каждой странице отдельно: content.js
У меня он выглядит так:
Наиболее интересный момент из моего скрипта, я считаю, должен быть:
chrome.runtime.sendMessage(
Тут происходит отправка сообщения background скрипту, а именно две переменные: site:sait — содержит адрес сайта, на котором скрипт
time:localStorage[sait] — количество времени, проведенное на этом скрипте.
Далее, рассмотрим фоновый скрипт background.js, где и происходит приём данных, а точнее рассмотрим саму функцию приёма данных.
Вот, собственно, и она. Разбирать подробно ничего не стану, т.к. это в принципе и не нужно. Достаточно знать наглядный пример, чтобы осуществить задуманное. Если в скрипте background.js добавить какие-либо данные в локальное хранилище( а также куки, web sql), то эти же данные можно будет использовать и в popup.js скрипте.
Вот собственно всё, что я хотел поведать о создании расширения, но я затрону еще один момент, в котором у меня возникли трудности.
На странице настроек мне необходимо было организовать перетаскивание сайтов в разные колонки.
Т.к. данные вставляются посредством InnerHtml, то данная возможность просто так не появится. Вот, что пришлось организовать:
Думаю, объяснять не нужно. Почитать подробнее можете по ссылке
Заходим в Настройки — Инструменты — Расширения, жмем на «Загрузить распакованное расширение»
Публикация расширения
Заходим на страницу оплачиваем 5$, публикуем.
Я не останавливаюсь на моментах, с которыми у меня не возникли трудности. А трудности возникли при оплате карточкой:
— В моём случае должен быть подключен 3д пароль.
Если Вам при оплате пишет ошибку, звоните своему банку и узнавайте. Мне за минуту помогли и всё гуд.
Как создать расширение для Chrome
Авторизуйтесь
Как создать расширение для Chrome
Расширения и плагины — полезные дополнения к уже существующим функциям на сайте и в браузере. С их помощью можно записывать аудио и видео с экрана, включать поиск ошибок, а также многое другое.
В этой статье мы рассмотрим создание самого простого расширения — запускатора избранных сайтов. Хотя приложение и будет примитивным, оно всё-таки раскроет процесс создания и загрузки расширения для google Chrome.
Желательно знать HTML, CSS и JS (если придётся расширить набор функций) на самом базовом уровне, чтобы понимать материал лучше, но в любом случае мы будем объяснять код.
3–4 декабря, Онлайн, Беcплатно
Давайте же внесём свой вклад в развитие web
Здесь всё очень просто:
Для начала давайте напишем базовый HTML-код:
Не забывайте указывать кодировку, иначе не отобразятся кириллические буквы.
Перейдём ко второму блоку кода, а именно к тегу body и его содержимому.
Так как наше расширение — модальное окно, давайте соответствующим образом назовём контейнеры. Сначала добавим контейнер шапки расширения, в которой укажем ссылку к иконке, напишем название и добавим номер версии.
Переходим к следующему контейнеру. Он содержит описание функций расширений.
Кроме того, мы указали названия иконок для каждого ресурса. Более детально со всеми доступными элементами можно ознакомиться на сайте Bootstrap.
Стили
Чтобы расширение выглядело красивее и было удобнее, чем сейчас, нужно добавить стили на CSS.
Мы постарались как можно подробнее объяснить в комментариях относительно сложные моменты. А сейчас нам нужно лишь загрузить наше расширение в браузер Chrome и оно будет работать, а если пройдёт модерацию, то появится в магазине расширений (плагинов).
Проверка кода и публикация
Прежде чем опубликовать, проверьте ещё раз весь код. Если вы делали всё так, как мы, то у должно было получиться следующее:
После проверки можно приступать к публикации расширения. Для этого у вас должны быть следующие файлы и папки:
И далее следуем инструкциям на скриншотах ниже.
Далее нажимаем на «загрузить распакованное расширение» и выбираем папку с файлами.
После того, как вы выбрали папку с файлами, она загрузится в браузер и превратится в расширение, которое будет доступно за пределами правой части адресной строки.
Надеемся, что всё работает правильно и вы понимаете структуру расширений для Chrome.
Основано на видео с канала «Traversy Media»
Создание расширения для Google Chrome
Тема создания расширений достаточно хорошо раскрыта в сети, есть множество статей, документации на эту тему. Но я не нашел ни одного ресурса, который бы описал процесс создания расширения от начала до конца. Я собираюсь исправить эту ситуацию, и рассказать о том как создать расширение, как хранить, читать настройки, как добавить поддержку нескольких языков.
Для работы с расширениями вам понадобится переключить канал обновлений на Dev или Beta.
Расширение будет иметь кнопку с иконкой на панели инструментов Chrome. При нажатии на кнопку будет появляться всплывающее окно (popup) со случайной картинкой из галлереи фотографий телескопа Hubble. В верхней части окна будут размещены кнопки: настроить (показать страницу настроек), обновить (показать другую фотографию), закрыть (закрыть всплывающее окно).
Расширение будет содержать страницу настроек (options), на которой можно будет выбрать язык интерфейса (русский, английский) и выбрать размер картинки (маленький, большой).
Создание расширения начинается с создания папки, в которой мы будет создавать все необходимы для работы расширения файлы. Созадим папку HubblePics. Далее создадим файл, который будет содержать описание нашего расширения — manifest.json. Данный файл является обязательным для каждого расширения. Именно из него Chrome получает всю необходимую информацию о расширении (название, версия, разрешения, страницы расширения и т.д.).
«options_page» : «options.html» // Страница настроек
>
Подробное описание файла manifest.json вы можете получить здесь
Настройки
Создадим страницу настроек — options.html. Приводить полный код страницы я не буду, только интересные, на мой взгляд моменты, а именно сохранение, извлечение настроек и локализация.
Сохранять настройки можно в объекте localStorage, который, по сути, представляет из себя ассоциативный массив, хранящий пары «название», «значение». Например, для сохранения состояния радиокнопки «Размер картинки — Маленький», используется код:
Для восстановления состояния:
В своем проекте я обернул обращение к localStorage в функцию readProperty чтобы избавится от лишних проверок и получить возможность получения значения по умолчанию:
function readProperty(property, defValue)
<
if (localStorage[property] == null )
<
return defValue;
>
Локализация
С настройками разобрались, приступим к локализации. Способ, который я предлагаю, возможно, не самый лучший, но на данный момент ничего лучше я придумать не смог. Если кто-то подскажет другой, более простой вариант — буду рад.
Идея простая — есть ряд элементов, которые нужно перевести. У них есть идентификаторы. Создается ассоциативный массив или объект, в котором идентификатору элемента соответствует локализованный текст. Функция, которая занимается локализацией «пробегает» по массиву, по идентификатору находит контрол и устанавливает ему текст.
Создадим файл с названием элементов и указанием языка. Язык «регистрируется», путем добавления элемента в выпадающий список «Язык». Например русский язык добавляет в список элемент с текстом «Russian» и значением «ru_RU».
Этот скрипт добавляется на страницу настроек (options.html)
script type =»text/javascript» src =»locale/ru_RU/options.js» > script >
На странице, все локализуемые элементы должны иметь соответствующие идентификаторы, например:
span id =»lngPreviewSmall» > Small span >
Локализацией занимается функция localize
function getSelectedLanguage()
<
var lang = getSelectedValue( «language» ); // Возвращает значение выбранного элемента в выпадающем списке «Language»
return eval( «lang_» + lang);
>
function localize()
<
var lang = getSelectedLanguage();
// Перебираем все элементы объекта lang_ru_RU
for ( var ctrlId in lang)
<
var value = lang[ctrlId];
// Не найден, продолжаем перебор
if (ctrl == null )
<
continue ;
>
// Найден, определить тип и присвоить значение
if (ctrl.tagName == «SPAN» )
<
ctrl.innerText = value;
>
else if (ctrl.tagName == «INPUT» )
<
ctrl.value = value;
>
>
>
Теперь, если нам необходимо добавить новый язык, например английский, мы просто создаем папку \locale\en_US, в ней создаем скрипт options.js
И добавляем скрипт на страницу
script type =»text/javascript» src =»locale/en_US/options.js» > script >
Всплывающее окно
Внутри файла popup.html простая разметка, в которой предусмотрено место для загружаемой картинки, кнопки управления и индикатор процесса загрузки.
ul class =»menu» >
li > img src =»images/options.png» onclick =»showOptions();» /> li >
li > img src =»images/update.png» onclick =»getPicture();» /> li >
li > img src =»images/close.png» onclick =»closePopup();» /> li >
ul >
div id =»loader» >
img src =»images/loader.gif» />
div >
div id =»image» style =»display: none;» >
a href =»#» id =»hrefPlace» onclick =»return openImage();» > img id =»imgPlace» /> a >
div >
В общем ничего интересного. Все интересно вынесено в файл popup.js.
Данный скрипт, используя XMLHttpRequest загружает страницу hubblesite.org/gallery/wallpaper, находит ссылки на изображения, выбирает случайное и отображает в popup-е.
xhr.onreadystatechange = function () <
if (xhr.readyState == 4)
<
if (xhr.responseText)
<
var xmlDoc = xhr.responseText;
var imgs = xmlDoc.match(/http:\/\/imgsrc.hubblesite.org\/hu\/db\/images\/hs-1<4>-2<2>-[a-z]/g);
var hrefs = xmlDoc.match(/gallery\/wallpaper\/pr5<4,>[a-z]/g);
if (imgs.length > 0)
<
var randIdx = Math.floor(Math.random() * imgs.length);
var imgSize = «-wallpaper_thumb.jpg» ;
showImage( «http://hubblesite.org/» + hrefs[randIdx], imgs[randIdx] + imgSize);
>
>
>
Установка и упаковка расширения
Расширение создано, теперь необходимо загрузить его в Chrome. Запускаем Chrome, нажимаем кнопку Настройка и управление , выбираем пункт меню Extensions.
А в списке расширений видим наше расширение.
Нажимаем OK, видим сообщение о том, что расширение упаковано.
Если мы собираемся выпускать обновленные версии расширения — сохраним созданный файл с ключем HubblePics.pem и будем указывать путь к нему при каждой последующей упаковке расширения, иначе, каждый раз будет генерироваться новый файл, что приведет к назначению нового идентификатора для нашего расширения, а это, в свою очередь приведет к тому, что вместо обновления, пользователь будет устанавливать новую копию расширения.
Практически вся информация, необходимая для разработки расширений сосредоточена на странице Google Chrome Extensions: Developer Documentation. Если этого покажется мало, то всегда можно взять готовое расширение, изменить расширение с crx на zip, распаковать и посмотреть как это сделано «у них».
Так же источником информации, так сказать, из первых рук, может стать список изменений при выходе новых версий Google Chrome.
Пишем безопасное браузерное расширение
В отличие от распространенной «клиент-серверной» архитектуры, для децентрализованных приложений характерно:
Существует 2 относительно безопасных хранилища для ключей пользователей — хардверные кошельки и браузерные расширения. Хардверные кошельки в большинстве своем максимально безопасны, однако сложны в использовании и далеко не бесплатны, а вот браузерные расширения являются идеальным сочетанием безопасности и простоты в использовании, а еще могут быть совершенно бесплатны для конечных пользователей.
Учитывая все это, мы захотели сделать максимально безопасное расширение, которое упрощает разработку децентрализованных приложений, предоставляя простой API для работы с транзакциями и подписями.
Об этом опыте мы вам и расскажем ниже.
В статье будет пошаговая инструкция как написать браузерное расширение, с примерами кода и скриншотами. Весь код вы можете найти в репозитории. Каждый коммит логически соответствует разделу данной статьи.
Краткая история браузерных расширений
Браузерные расширения существуют достаточно давно. В Internet Explorer они появились еще в 1999-м году, в Firefox — в 2004-м. Тем не менее, очень долго не было единого стандарта для расширений.
Можно сказать, что он появился вместе с расширениями в четвертой версии Google Chrome. Конечно, никакой спецификации тогда не было, но именно API Chrome стал ее основой: завоевав большую часть рынка браузеров и имея встроенный магазин приложений, Chrome фактически задал стандарт для браузерных расширений.
У Mozilla был свой стандарт, но, видя популярность расширений для Chrome, компания решила сделать совместимый API. В 2015 году по инициативе Mozilla в рамках World Wide Web Consortium (W3C) была создана специальная группа для работы над спецификациями кроссбраузерных расширений.
За основу был взят уже существующий API расширений для Сhrome. Работа велась при поддержке Microsoft (Google в разработке стандарта участвовать отказался), и в результате появился черновик спецификации.
Формально спецификацию поддерживают Edge, Firefox и Opera (заметьте, что в этом списке отсутствует Chrome). Но на самом деле стандарт во многом совместим и с Chrome, так как фактически написан на основе его расширений. Подробнее о WebExtensions API можно прочитать здесь.
Структура расширения
Единственный файл, который обязательно нужен для расширения — манифест (manifest.json). Он же является “точкой входа” в расширение.
Манифест
По спецификации файл манифеста является валидным JSON файлом. Полное описание ключей манифеста с информацией о том, какие ключи в поддерживается в каком браузере, можно посмотреть здесь.
Ключи, которых нет в спецификации, “могут” быть проигнорированы (и Chrome, и Firefox пишут об ошибках, но расширения продолжают работать).
А я бы хотел обратить внимание на некоторые моменты.
Контекст выполнения
У расширения есть три контекста исполнения кода, то есть, приложение состоит из трех частей с разным уровнем доступа к API браузера.
Extension context
Здесь доступна большая часть API. В этом контексте «живут»:
Этот контекст существует независимо от окон и вкладок браузера. Background page существует в единственном экземпляре и работает всегда (исключение — event page, когда background-скрипт запускается по событию и «умирает» после его выполнения). Popup page существует, когда открыто окно popup, а Custom page — пока открыта вкладка с ней. Доступа к другим вкладкам и их содержимому из этого контекста нет.
Content script context
Web page context
Это собственно сама веб-страница. К расширению она не имеет никакого отношения и доступа туда не имеет, кроме случаев, когда в манифесте явно не указан домен этой страницы (об этом — ниже).
Обмен сообщениями
Разные части приложения должны обмениваться сообщениями между собой. Для этого существует API runtime.sendMessage для отправки сообщения background и tabs.sendMessage для отправки сообщения странице (контент-скрипту, popup’у или веб странице при наличии externally_connectable ). Ниже приведен пример при обращении к API Chrome.
Сервер или background:
Схема приложения
Давайте сделаем браузерное расширение, которое хранит приватные ключи, предоставляет доступ к публичной информации (адрес, публичный ключ общается со страницей и позволяет сторонним приложениям запросить подпись транзакций.
Разработка приложения
Наше приложение должно как взаимодействовать с пользователем, так и предоставлять странице API для вызова методов (например, для подписи транзакций). Обойтись одним лишь contentscript не получится, так как у него есть доступ только к DOM, но не к JS страницы. Подключаться через runtime.connect мы не можем, потому что API нужен на всех доменах, а в манифесте можно указывать только конкретные. В итоге схема будет выглядеть так:
Начало
Весь код браузерного расширения доступен на GitHub. В процессе описания будут ссылки на коммиты.
Начнем с манифеста:
Создаем пустые background.js, popup.js, inpage.js и contentscript.js. Добавляем popup.html — и наше приложение уже можно загрузить в Google Chrome и убедиться, что оно работает.
Чтобы убедиться в этом, можно взять код отсюда. Кроме того, что мы сделали, по ссылке настроена сборка проекта с помощью webpack. Чтобы добавить приложение в браузер, в chrome://extensions нужно выбрать load unpacked и папку с соответствующим расширением — в нашем случае dist.
Теперь наше расширение установлено и работает. Запуститьинструменты для разработчиков для разных контекстов можно следующим образом:
Доступ к консоли контент-скрипта осуществляется через консоль самой страницы, на которой он запущен.
Обмен сообщениями
Итак, нам необходимо установить два канала связи: inpage background и popup background. Можно, конечно, просто отправлять сообщения в порт и изобрести свой протокол, но мне больше нравится подход, который я подсмотрел в проекте с открытым кодом metamask.
Это браузерное расширение для работы с сетью Ethereum. В нем разные части приложения общаются через RPC при помощи библиотеки dnode. Она позволяет достаточно быстро и удобно организовать обмен, если в качестве транспорта ей предоставить nodejs stream (имеется в виду объект, реализующий тот же интерфейс):
Теперь мы создадим класс приложения. Оно будет создавать объекты API для popup и веб-страницы, а также создавать dnode для них:
Здесь и далее вместо глобального объекта Chrome мы используем extentionApi, который обращается к Chrome в браузере от Google и к browser в других. Делается это для кроссбраузерности, но в рамках данной статьи можно было бы использовать и просто ‘chrome.runtime.connect’.
Создадим инстанс приложения в background скрипте:
Так как dnode работает со стримами, а мы получаем порт, то необходим класс-адаптер. Он сделан при помощи библиотеки readable-stream, которая реализует nodejs-стримы в браузере:
Теперь создаем подключение в UI:
Затем мы создаем подключение в content script:
Так как API нам нужен не в контент-скрипте, а непосредственно на странице, мы делаем две вещи:
Теперь создаем объект api в inpage и заводим его global:
У нас готов Remote Procedure Call (RPC) с отдельным API для страницы и UI. При подключении новой страницы к background мы можем это увидеть:
Пустой API и origin. На стороне страницы мы можем вызвать функцию hello вот так:
Работать с callback-функциями в современном JS — моветон, поэтому напишем небольшой хелпер для создания dnode, который позволяет передавать в объект API в utils.
Объекты API теперь будут выглядеть вот так:
Получение объекта от remote следующим образом:
А вызов функций возвращает промис:
Версия с асинхронными функциями доступна здесь.
В целом, подход с RPC и стримами кажется достаточно гибким: мы можем использовать steam multiplexing и создавать несколько разных API для разных задач. В принципе, dnode можно использовать где угодно, главное — обернуть транспорт в виде nodejs стрима.
Альтернативой является формат JSON, который реализует протокол JSON RPC 2. Однако он работает с конкретными транспортами (TCP и HTTP(S)), что в нашем случае не применимо.
Внутренний стейт и localStorage
Нам понадобится хранить внутренний стейт приложения — как минимум, ключи для подписи. Мы можем достаточно легко добавить стейт приложению и методы для его изменения в popup API:
В background обернем все в функцию и запишем объект приложения в window, чтобы можно было с ним работать из консоли:
Добавим из консоли UI несколько ключей и посмотрим, что получилось со стейтом:
Стейт нужно сделать персистентным, чтобы при перезапуске ключи не терялись.
Хранить будем в localStorage, перезаписывая при каждом изменении. Впоследствии доступ к нему также будет необходим для UI, и хочется также подписываться на изменения. Исходя из этого удобно будет сделать наблюдаемое хранилище (observable storage) и подписываться на его изменения.
Использовать будем библиотеку mobx (https://github.com/mobxjs/mobx). Выбор пал на нее, так как работать с ней не приходилось, а очень хотелось ее изучить.
Добавим инициализацию начального стейта и сделаем store observable:
«Под капотом» mobx заменил все поля store на proxy и перехватывает все обращения к ним. На эти обращения можно будет подписываться.
Далее я буду часто использовать термин “при изменении”, хотя это не совсем корректно. Mobx отслеживает именно доступ к полям. Используются геттеры и сеттеры прокси-объектов, которые создает библиотека.
Декораторы action служат двум целям:
В background добавим инициализацию и сохранение стейта в localStorage:
Интересна здесь функция reaction. У нее два аргумента:
В отличие от redux, где мы явно получаем стейт в качестве аргумента, mobx запоминает к каким именно observable мы обращаемся внутри селектора, и только при их изменении вызывает обработчик.
В консоли popup снова добавим несколько ключей. На этот раз они попали еще и в localStorage:
При перезагрузке background-страницы информация остается на месте.
Весь код приложения до этого момента можно посмотреть здесь.
Безопасное хранение приватных ключей
Хранить приватные ключи в открытом виде небезопасно: всегда есть вероятность того, что вас взломают, получат доступ к вашему компьютеру и так далее. Поэтому в localStorage мы будем хранить ключи в зашифрованном паролем виде.
Для большей безопасности добавим приложению стейт locked, в котором доступа к ключам не будет совсем. Мы будем автоматически переводить расширение в стейт locked по таймауту.
Mobx позволяет хранить только минимальный набор данных, а остальное автоматически рассчитывать на их основе. Это — так называемые computed properties. Их можно сравнить с view в базах данных:
Теперь мы храним только шифрованные ключи и пароль. Все остальное вычисляется. Перевод в стейт locked мы делаем с помощью удаления пароля из стейта. В публичном API появился метод для инициализации хранилища.
Код до этого шага находится здесь.
Транзакции
Итак, мы подошли к самому главному: созданию и подписи транзакций в блокчейне. Мы будем использовать блокчейн WAVES и библиотеку waves-transactions.
Для начала добавим в стейт массив сообщений, которые необходимо подписать, затем — методы добавления нового сообщения, подтверждения подписи и отказа:
Если не сделать observable вручную, то mobx сделает это сам при добавлении в массив messages. Однако он создаст новый объект, на который у нас не будет ссылки, а она понадобится для следующего шага.
Далее мы возвращаем промис, который резолвится при изменении статуса сообщения. За статусом следит reaction, который сам себя «убьет» при смене статуса.
Код методов approve и reject очень прост: мы просто меняем статус сообщения, предварительно подписав его, если необходимо.
Approve и reject мы выносим в API UI, newMessage — в API страницы:
Теперь попробуем подписать транзакцию расширением:
В целом все готово, осталось добавить простой UI.
Интерфейсу нужен доступ к стейту приложения. На стороне UI мы сделаем observable стейт и добавим в API функцию, которая будет этот стейт менять. Добавим observable в объект API, полученный от background:
В конце мы запускаем рендер интерфейса приложения. Это react-приложение. Background-объект просто передается при помощи props. Правильно, конечно, сделать отдельный сервис для методов и store для стейта, но в рамках данной статьи этого достаточно:
С помощью mobx очень просто запускать рендер при изменении данных. Мы просто вешаем декоратор observer из пакета mobx-react на компонент, и рендер будет автоматически вызываться при изменении любых observable, на которые ссылается компонент. Не нужно никаких mapStateToProps или connect, как в redux. Все работает сразу «из коробки»:
Остальные компоненты можно посмотреть в коде в папке UI.
При получении объекта remote создается reaction на изменение стейта, который вызывает функцию на стороне UI.
Последний штрих — добавим отображение новых сообщений на иконке расширения:
Итак, приложение готово. Веб-страницы могут запрашивать подпись транзакций:
Код доступен по этой ссылке.
Заключение
Если вы дочитали статью до конца, но у вас остались вопросы, вы можете задать их в репозитории с расширением. Там же вы найдете коммиты под каждый обозначенный шаг.
А если вам интересно посмотреть код настоящего расширения, то вы сможете найти это здесь.
Код, репозиторий и описание работы от siemarell