Сегодня продолжаем тему написания расширения для Chrome, позволяющего внедрять свой код на чужие сайты, тем самым меняя или дополняя их функционал и внешний вид по своему желанию. Для чего это нужно и чем может быть полезно, рассмотрено в предыдущей части. Также в предыдущей статье были рассмотрены вопросы, касающиеся настройки и подготовки к написанию расширения с использованием Angular 18. Напоминаю, что весь код я публикую в открытом доступе на GitHub.
Пользовательские скрипты в Chrome
Давайте разберемся, как устроен функционал внедрения своего произвольного кода на сторонние сайты в Chrome. В этом году состоялся переход на новую платформу для расширений Manifest V3. Ранее, во 2-й версии Manifest, внедрение произвольного кода на страницу не выделялось в API, в результате чего, хотя и были некоторые ограничения со стороны платформы, процесс внедрения имел различные варианты решения со своими нюансами, разработчик сам выбирал и реализовывал подходящий. Однако в текущей версии этот процесс стал более унифицирован, для регистрации и других операций со скриптами используется задокументированное API userScripts.
Для получения возможности работать с пользовательскими скриптами в нашем расширении нам понадобится запросить разрешение на данные операции. В manifest.json
в раздел permissions
нужно добавить userScripts
, а также в разделе host_permissions
задать список сайтов, на которые мы будем внедрять свой код. Если нам необходима возможность внедряться на абсолютно любой сайт, то можно задать такую маску: "host_permissions": [ "http://*/*", "https://*/*" ]
. Для работы с API Chrome
в Angular также потребуется установить описание структуры API: npm install @types/chrome --save-dev
. После этого нам становится доступно API Chrome
, и прежде всего нас интересует отдельная его часть userScripts
.
Регистрация пользовательского скрипта
Чтобы внедрить свой JavaScript код на произвольную страницу какого-либо сайта, необходимо зарегистрировать скрипт с помощью chrome.userScripts.register
:
chrome.userScripts.register([{
id: 'habr-test',
matches: ['https://habr.com/*'],
js: [{code: 'alert("Wow! I\'ve hacked HABR!")'}]
}]);
В качестве параметра функции передается массив, а значит зарегистрировать можно сразу несколько пользовательских скриптов за одну операцию. Для каждого скрипта при регистрации будем задавать следующие свойства:
id
— уникальный символьный код регистрируемого скриптаmatches
— массив строк со списком URL-адресов, на которые внедряется наш скрипт. Вместо адресов можно использовать шаблоны.excludeMatches
— аналогично параметруmatches
, только работает как исключающий фильтр URL-адресовjs
— массив JavaScript-кодов, представленных в виде объектов типаchrome.userScripts.ScriptSource
. Почему массив? Потому что вы можете в одном регистрируемом пользовательском скрипте подключить сразу несколько отдельных кодов, разделенных по смыслу или своему функциональному назначению.runAt
— определяет, в какой момент запускать пользовательский скрипт:document_start
,document_end
илиdocument_idle
Получение списка зарегистрированных скриптов
Для получения списка ранее зарегистрированных скриптов используем функцию chrome.userScripts.getScripts
, которая возвращает Promise с результатом. В качестве параметра в функцию можно передать фильтр, например, для получения скрипта с заданным id:
chrome.userScripts.getScripts({ids: ['habr-test']})
Обновление зарегистрированных скриптов
Формат функции chrome.userScripts.update
абсолютно идентичен функции для регистрации, только в качестве параметра нужно передавать не список новых скриптов, а список обновляемых. Если в каком-либо из переданных скриптов будет задан несуществующий id, выполнение кода будет прервано с ошибкой.
chrome.userScripts.update([{
id: 'habr-test',
matches: ['https://habr.com/*'],
js: [{code: 'alert("Wow! I\'ve hacked HABR twice!")'}]
}]);
Удаление пользовательского скрипта
Вызов функции удаления аналогичен вызову функции получения списка скриптов — в качестве параметра передается фильтр, в котором мы можем задать список id удаляемых скриптов:
chrome.userScripts.getScripts({ids: ['habr-test']})
Скрипты, скрипты, а где CSS?
Ранее, в предыдущей статье, я озвучивал, что готовое расширение сможет подключать связку JS + CSS кода для любых страниц. Однако пока о CSS я не написал ни слова. Почему? Потому что в Chrome реализовано только подключение JS-скриптов. Но разве это проблема? Конечно нет, мы можем с помощью JS реализовать подключение любого CSS-кода самостоятельно точно также, как это делалось со старой версией Manifest V2.
private wrapCSSCodeWithJS(codeCSS: string): string {
return '(() => {' +
'\n\tconst style = document.createElement(\'style\')' +
'\n\tstyle.textContent = /*user style start*/`' + codeCSS + '`/*user style end*/' +
'\n\tdocument.documentElement.appendChild(style)' +
'\n})()'
}
В результате выполнения функции мы получаем JS-код для подключения заданного нами CSS-кода, т. е. мы его как бы оборачиваем в JS-код. А далее уже он может быть зарегистрирован как обычный пользовательский скрипт.
Developer mode
С алгоритмом добавления пользовательского кода в Chrome на этом можно и закончить, все остальное уже относится к добавлению удобств, украшательств, оптимизаций, дополнительного функционала и пр. На основе описанных выше функций мы уже можем реализовать базовый функционал.
Есть только один неприятный нюанс. Зачем-то в команде разработчиков Chrome решили, что недостаточно запроса вида Read and change all your data on all websites
, с которым пользователь соглашается при установке расширения. Чтобы разрешить использование userScripts API
, пользователь должен помимо этого обязательно включить еще и галочку Developer mode (Режим разработчика)
на странице списка установленных расширений.
Да, я понимаю, что это, вероятно, объясняется излишней предосторожностью, т. к. включаемый функционал отнюдь небезопасен для пользователя, но все же можно было это решить и в рамках одного уведомления, просто особенно выделив опасный запрос визуально.
Наверное, это не такая большая проблема даже для тех, кто с компьютером на Вы. В конце концов, если пользователь ищет и ставит какие-то дополнительные расширения для браузера, для него не должно быть проблемой найти в настройках нужную опцию. Однако, установив расширение, пользователь обычно ожидает, что им можно сразу пользоваться без дополнительных манипуляций. К тому же разработчик расширения должен сам позаботиться о том, чтобы уведомить пользователя о необходимости дополнительной настройки. Для этого необходимо в коде проверять, включен ли "Режим разработчика" (и далее реализовать уведомление пользователя о необходимости включить дополнительную опцию для работы расширения):
isChromeUserscriptsAvailable(): boolean {
try {
chrome.userScripts;
return true;
} catch {
return false;
}
}
Развитие функционала
Если посмотреть на представленный Chrome'ом userScripts API
, выглядит он очень скудно, с дополнительным функционалом тут не разгуляешься. Например, возьмем простейшую задачу — временное включение/отключение пользовательских скриптов.
Официальный API такой возможности не предоставляет, а сохраняемый браузером объект chrome.userScripts.RegisteredUserScript
не содержит какого-либо свойства для хранения дополнительной информации о регистрируемом скрипте. В связи с этим необходимо создавать свою структуру для хранения всех дополнительных опций скриптов, которые нам понадобятся в дальнейшем.
Сохранять будем в chrome.storage.local
— специальное локальное хранилище, доступное расширениям. Для его использования в manifest.json
в раздел permisions
необходимо добавить storage
и unlimitedStorage
(без второго разрешения максимальный размер хранимых данных будет ограничен 10 Мб). Создаваемая структура будет связана по id с объектом RegisteredUserScript
, сохраненным в браузере.
Также здесь мы дублируем и остальные свойства сохраненного пользовательского скрипта. Зачем дублировать? Во-первых, так удобнее будет работать, когда все данные будут извлекаться из одного места. Во-вторых, и основное — для реализации требуемого нам функционала (включения/отключения скрипта) нам потребуется удалять зарегистрированный в браузере объект (т. к. он не имеет состояния "отключен"). При этом сами данные скрипта — код, привязка к URL — должны сохраниться. Они и будут храниться в создаваемой нами дополнительной структуре:
export interface CodeBundle {
id: string
urlPatterns: string[]
urlPatternsCommaSeparated?: string
js: string
css: string
isEnabled: boolean
}
Здесь мы добавили дополнительное свойство isEnabled
— именно на основании него мы теперь можем включать/отключать скрипты, т. е. по сути регистрировать и отменять их регистрацию в браузере.
В итоге, реализовав все вышеописанное в коде, мы получаем интерфейс вот такого вида, где мы можем управлять созданными нами скриптами и стилями, привязанными к заданным URL:
Быстрый доступ к функционалу
Чтобы постоянно не лезть в настройки расширения и упростить процесс добавления/редактирования кода, следует добавить возможность добавлять/редактировать код для текущей открытой страницы или сайта в пару кликов. Для этого мы используем специальное всплывающее окно, которое появляется при нажатии на иконку нашего расширения (как включить данную возможность, рассматривалось в предыдущей статье). При открытии данного всплывающего окна нам необходимо получить URL текущей активной вкладки. Для этого добавим простую функцию, использующую Tabs API
:
getCurrentUrlAsync(): Promise<string | undefined> {
return chrome.tabs.query({
active: true,
currentWindow: true,
}).then((tabs: chrome.tabs.Tab[]) => tabs[0].url)
}
Чтобы работать с данным API, нам необходимо также добавить в manifest.json
дополнительный запрос на разрешение tabs
в раздел permissions
.
Теперь, получив текущий URL, мы можем перебрать все добавленные нами пользовательские скрипты, сравнить шаблоны URL (в т. ч. исключающие) с текущим URL и отобразить во всплывающем окне те скрипты, которые привязаны к текущей странице. Также здесь реализуем возможность быстрого добавления скрипта к текущей странице или всему сайту. В итоге, кликнув по иконке установленного расширения, получаем такое всплывающее окно:
На этом написание базового функционала закончено. У нас есть все необходимое для создания своих скриптов, а также стилей и привязки их к определенным страницам. Далее можно заняться различными удобными и полезными улучшениями, к которым мы и приступим в следующей части статьи.
Vinni37
Используйте:
Конечно не проблема:
kanasero Автор
chrome.scripting.executeScript
в данном случае не подойдет, т. к. в качестве аргумента требует либо указать файл js для выполнения, либо функцию. Т. е. данный метод предназначен для фиксированного, заранее прописанного в расширении js-кода. В данном же случае нам нужно выполнить динамический произвольный код, извлекаемый из локального хранилища. Т. е. нам нужно по сути выполнить код, который хранится в строковом виде, типа"let h = 'hello, world!'; alert(h);"
. ИспользуяexecuteScript
, мы такой код в виде строки не запустим, использоватьeval
Chrome не дает из-за ограничений безопасности.А вот с
chrome.scripting.insertCSS
, думаю, вы правы, вполне можно было бы использовать. Другое дело, что по трудозатратности такой вариант ни в чем не выигрывает - нужно самостоятельно выполнять проверку текущего адреса с сохраненными шаблонами URL и подцеплять CSS. К тому же, если я уже все равно используюuserScripts
для хранения js-кода, вполне удобно обернуть CSS в JS и использовать один и тот же функционал и для скриптов, и для стилей, т.е. работать с ними как с идентичными объектами.Vinni37
Для executeScript, можно использовать Ваш подход к CSS.
kanasero Автор
Вы имеете ввиду внедрение кода через вставку элемента
script
в DOM дерево? Нет, такой вариант тоже не проходит. Chrome запрещает вставку любого динамического js-кода в обход официального APIuserScripts
. Если не ошибаюсь, в Manifest V2 такой подход работал, но сейчас в 3-й версии ужесточили этот момент.Vinni37
Насколько мне известно, если в executeScript выставить ExecutionWorld: "MAIN" (по умолчанию ISOLATED) то не должно быть проблем. Изначально при появлении executeScript,параметра ExecutionWorld не было, его добавили потом. Но может что то уже поменялось.