- PVSM.RU - https://www.pvsm.ru -
За 5 лет разработки расширений для браузера Google Chrome накопился некоторый опыт, которым хотелось бы поделиться в цикле статей и, по возможности, пояснить некоторые тонкости, подводные камни, а также описать как были удачно применены современные фронтенд-технологии.
С расширениями для браузера Google Chrome я столкнулся в 2012-м году когда активно покупал товары с витрины Amazon и было там жутко неудобно искать продавцов, которые доставляли товар в РФ и у кого выгоднее всего покупать с учетом стоимости доставки. Вот тут я и решил облегчить жизнь себе, да и другим покупателям тоже, создав расширение «Amazon ships to you”, про которое была даже когда-то статья на Хабре [1]. Со временем оно стало не актуально, т.к. на витрине Amazon сделали спустя пару лет нормальные фильтры и я его снял с публикации.
Далее наступило время пользования сервисом Я.Музыка [2] и Я.Радио [3] и очень уж мне не хватало управления плеером на сайте Я.Музыки когда он играет в фоновой вкладке, ну хотя бы горячими клавишами. В результате, я, как человек опытный, не найдя аналогов, решил сделать для этого расширение “Яндекс.Музыка — управление плеером» [4], которое является моим хобби и по сей день, несмотря даже на выход официального расширения от Яндекса [5].
Предполагается, что читатель должен быть уже знаком с базовой структурой элементов расширения (manifest.json [6], фоновая страница [7], контент-скрипт [8], попап [9]). Краткий ликбез под спойлером ниже.
Раньше можно было локальными расширениями пользоваться без проблем, затем браузер начал оповещать уведомлением о наличии таких расширений, теперь же подобные расширения по умолчанию отключаются при запуске браузера и их надо активировать вручную (что немало насолило разработчикам расширений при активной разработке).
Все метаданные расширения описываются в манифесте, но о нем нечего особо сказать, в документации [6] все описано.
Расширение [12] состоит из фоновой страницы (background page [13] или event page [7]), всплывающего окна [9], страницы настроек [14], внедряемого на целевой сайт контент-скрипта [8] и специфических override pages [15], которые переопределяют стандартные страницы браузера: менеджер истории, закладок, новая страница и т.п.
С точки зрения UI, расширение содержит элемент browserAction [16] (или pageAction [17]) — иконку на панели браузера справа от адресной строки, нажатие на которую может инициировать какое-либо действие или же открывает еще один UI элемент — всплывающее окно [9]. Также на этой иконке можно выводить badge [18] — значок, например, с количеством непрочитанных писем.
Браузер предоставляет расширениям множественные API [19] для различных нужд, например через chrome.tabs [20] API можно получить доступ ко всем вкладкам, закрывать, открывать новые, видеть метаданные вкладок и т.п.
Плюс к этому, между различными страницами расширения есть определенные механизмы передачи данных [21].
Типовая задача расширения — расширение функционала некоторого сайта, для чего необходимо внедрить код на целевую страницу. Возьмем для примера Я.Музыку (здесь и далее): для управления плеером на сайте через всплывающее окно расширения (см. скриншот №1), нам необходимо отследить открытие страницы, затем внедрить в нее наш код, который в дальнейшем будет взаимодействовать с js-кодом и DOM витрины и передавать все изменения в расширение, чтобы при открытии всплывающего окна всегда отображать актуальное состояние плеера на витрине. Вот тут и появляется то важное, о чем я хотел бы для начала рассказать и о чем гласит название раздела: песочницы.
(скриншот №1)
Фоновая страница расширения, являясь основным контроллером расширения, при наличии должных расширений (tabs, activeTab [22]) может отследить открытие страницы music.yandex.ru [2] используя chrome.tabs.onUpdated [23] и внедрить наш код (контент-скрипт) на витрину через chrome.tabs.executeScript [24]. Однако, внедренный код оказывается в своей песочнице (Б), из которой есть доступ к DOM-документу витрины, но не к js-коду витрины, который выполняется в своей песочнице (А). В теории можно было бы отслеживать изменения DOM-элементов на витрине чтобы транслировать текущее состояние в расширение (например, изменился трек — взять из DOM название трека, исполнителя), но это все работает не так как хотелось бы: с задержками на обновление витрины, не всегда присутствующими целевыми DOM-элементами в текущем отображении витрины и прочими вещами, которые в реальном использовании расширения дают рассинхронизацию данных между витриной и отображением в расширении. Как же нам получить доступ к js-коду витрины (песочнице А)?
Для внедрения js-кода в песочницу А из контент-скрипта нам может помочь следующий “хак” (здесь и далее нотация es6):
function injectCode(func, ...args) {
let script = document.createElement('script');
script.textContent = 'try {(' + func + ')(' + args + '); } catch(e) {console.error("injected error", e);};';
(document.head || document.documentElement).appendChild(script);
script.parentNode.removeChild(script);
}
В результате наш контент-скрипт может внедрить свой код в песочницу js-кода витрины для того, чтобы получить доступ к API витрины и на низком уровне иметь доступ к объектам и событиям плеера, плейлиста и прочих сущностей витрины. Но это еще не всё: доступ к API получен, к примеру, событие начала проигрывания трека наш внедренный код “поймал”, но как же его передать обратно в контент-скрипт, который, в свою очередь, имеет механизмы передачи данных на фоновую страницу расширения?
Увы, доступа из песочницы А в песочницу Б нет, во всяком случае, мне он неизвестен. Но это не страшно, потому что мы помним к чему имеет доступ контент-скрипт из своей песочницы Б: к DOM-документу витрины, отсюда выход: внедренный в песочницу А код может создавать произвольные события:
document.dispatchEvent(new CustomEvent(CUSTOM_EVENT_NAME, {detail: payload}));
А контент-скрипт может их ловить:
document.addEventListener(CUSTOM_EVENT_NAME, e => {
console.log(e.detail); //payload
});
Получается такая схема:
После этого, контент-скрипт передает это событие на фоновую страницу одним из механизмов: chrome.runtime.sendMessage [25] или chrome.runtime.connect [26]. Отличие их в том, что первый метод открывает канал, передает данные, закрывает канал, второй же создает постоянный канал, в рамках которого происходит передача данных. При проигрывании треков идет постоянная передача событий progress (текущее время проигрывания), посему для меня стал выбор очевиден: поднимать постоянный канал связи контент-скрипта с фоновой страницей, что дает еще один не очевидный бонус: контроль потери связи с витриной по различным причинам, от исключений в js-коде до закрытия вкладки (хотя именно закрытие вкладки легко отслеживается через chrome.tabs.onRemoved [27]).
С данными песочницами закончили, но это еще не всё: есть еще песочница (В) фоновой страницы и песочница (Г) всплывающего окна. Тут все немного проще, данные песочницы имеют доступ друг к другу через API:
Но тут есть подводный камень: всплывающее окно не может, например, добавить свой слушатель событий на фоновую страницу, но к объекту window друг друга вышеописанные методы доступ дают (т.к. они запущены в одном потоке). Для единообразия интеркоммуникаций и для принудительной изоляции кода фоновой страницы от кода всплывающего окна (иначе popup может модифицировать объекты bg, что нарушит однонаправленный поток данных из контент-скрипта на фоновую страницу для хранения актуального состояния и оттуда во всплывающее окно), я перешел к такому же поднятию канала как и между контент-скриптом и фоновой страницей.
Кода и примеров мало, т.к. всё есть в документации, на которую я максимально ссылаюсь при любых упоминаниях методов и API, в части кода мне добавить тут нечего, т.к. на статус туториала (когда можно скопипастить весь код и что-то заработает) — не претендую. А вот саму концепцию разжевать стоило, т.к. неоднократно коллегам по программистскому цеху объяснял оную. Резюмировать можно раздел следующей иллюстрацией:
В следующей части я планирую рассказать об истории перевода расширения “Яндекс.Музыка — управление плеером" [4] на нынче популярный React+Redux, который, как нельзя, кстати, подошел для отображения состояния плеера во всплывающем окне расширения.
Автор: B_bird
Источник [30]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/198086
Ссылки в тексте:
[1] статья на Хабре: https://habrahabr.ru/post/196484/
[2] Я.Музыка: https://music.yandex.ru
[3] Я.Радио: https://radio.yandex.ru
[4] расширение “Яндекс.Музыка — управление плеером»: https://chrome.google.com/webstore/detail/%D1%8F%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D0%BC%D1%83%D0%B7%D1%8B%D0%BA%D0%B0-%D1%83%D0%BF%D1%80%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BF/gcfheefljlblchcfjlknidfimnfillec
[5] официального расширения от Яндекса: https://chrome.google.com/webstore/detail/%D1%8F%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D0%BC%D1%83%D0%B7%D1%8B%D0%BA%D0%B0/kefiofndeiobnlbabkhfkfmgdhmhhfmc
[6] manifest.json: https://developer.chrome.com/extensions/manifest
[7] фоновая страница: https://developer.chrome.com/extensions/event_pages
[8] контент-скрипт: https://developer.chrome.com/extensions/content_scripts
[9] попап: https://developer.chrome.com/extensions/browserAction#popups
[10] Chrome Web Store: https://chrome.google.com/webstore/
[11] chrome://extensions/: http://chrome://extensions/
[12] Расширение: https://developer.chrome.com/extensions
[13] background page: https://developer.chrome.com/extensions/background_pages
[14] страницы настроек: https://developer.chrome.com/extensions/options
[15] override pages: https://developer.chrome.com/extensions/override
[16] browserAction: https://developer.chrome.com/extensions/browserAction
[17] pageAction: https://developer.chrome.com/extensions/pageAction
[18] badge: https://developer.chrome.com/extensions/browserAction#badge
[19] API: https://developer.chrome.com/extensions/api_index
[20] chrome.tabs: https://developer.chrome.com/extensions/tabs
[21] механизмы передачи данных: https://developer.chrome.com/extensions/messaging
[22] tabs, activeTab: https://developer.chrome.com/extensions/declare_permissions
[23] chrome.tabs.onUpdated: https://developer.chrome.com/extensions/tabs#event-onUpdated
[24] chrome.tabs.executeScript: https://developer.chrome.com/extensions/tabs#method-executeScript
[25] chrome.runtime.sendMessage: https://developer.chrome.com/extensions/runtime#method-sendMessage
[26] chrome.runtime.connect: https://developer.chrome.com/extensions/runtime#method-connect
[27] chrome.tabs.onRemoved: https://developer.chrome.com/extensions/tabs#event-onRemoved
[28] chrome.extension.getBackgroundPage(): https://developer.chrome.com/extensions/extension#method-getBackgroundPage
[29] chrome.extension.getViews({ type: 'popup' }): https://developer.chrome.com/extensions/extension#method-getViews
[30] Источник: https://habrahabr.ru/post/312290/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.