Скрипты содержимого
Скрипт содержимого — это часть вашей расширения, которая выполняется в контексте конкретной веб-страницы (в отличие от фоновых скриптов, которые являются частью расширения, или скриптов, которые являются частью самого веб-сайта, таких как те, которые загружаются с помощью элемента <script>).
Фоновые скрипты могут обращаться ко всем веб-API JavaScript расширений, но они не могут напрямую получать доступ к содержимому веб-страниц. Поэтому, если ваше расширение нуждается в этом, вам понадобятся скрипты содержимого.
Так же, как и скрипты, загружаемые обычными веб-страницами, скрипты содержимого могут читать и изменять содержимое своих страниц с помощью стандартных API DOM.
Скрипты содержимого могут получить доступ только к небольшому подмножеству API расширений WebExtension, но они могут общаться с фоновыми скриптами с помощью системы обмена сообщениями и, таким образом, косвенно получать доступ к API расширений WebExtension.
Примечание: Скрипты содержимого выполняются только в том случае, если расширение получило разрешения на доступ к домену для данного домена.
Однако, к ограниченным доменам никогда нельзя получить доступ, независимо от предоставленных разрешений.
Начиная с Manifest V3, разрешения на доступ к доменам не предоставляются автоматически при установке. Пользователи могут разрешить или запретить доступ к доменам после установки расширения.
Загрузка скриптов содержимого
Вы можете загрузить скрипт содержимого на веб-страницу тремя способами:
-
- При установке, на страницы, соответствующие шаблонам URL.
-
Используя ключ
content_scriptsв вашемmanifest.json, вы можете попросить браузер загрузить скрипт содержимого всякий раз, когда браузер загружает страницу, адрес которой соответствует заданному шаблону.
-
- Во время выполнения, на страницы, соответствующие шаблонам URL.
-
Используя API
contentScripts, вы можете попросить браузер загрузить скрипт содержимого всякий раз, когда браузер загружает страницу, адрес которой соответствует заданному шаблону. (Это похоже на метод 1, за исключением того, что вы можете добавлять и удалять скрипты содержимого во время выполнения.)
-
- Во время выполнения, в конкретные вкладки.
-
В Manifest V2, с помощью
tabs.executeScript(), или Manifest V3, с помощьюscripting.executeScript(), вы можете загрузить скрипт содержимого в конкретную вкладку в любое время. (Например, в ответ на нажатие пользователем кнопки расширения.)
Существует только одна глобальная область видимости на фрейм, на расширение. Это означает, что переменные из одного скрипта содержимого могут напрямую использоваться другим скриптом содержимого, независимо от того, как был загружен скрипт содержимого.
Используя методы (1) и (2), вы можете загружать скрипты только на страницы, адреса которых можно представить с помощью шаблона соответствия.
Используя метод (3), вы также можете загружать скрипты на страницы, упакованные с вашим расширением, но вы не можете загружать скрипты в привилегированные страницы браузера (например, «about:debugging» или «about:addons»).
Примечание: Динамический импорт модулей JS сейчас работает в скриптах содержимого. Для получения более подробной информации см. баг Firefox 1536094. Разрешены только URL со схемой moz-extension, что исключает данные URL (баг Firefox 1587336).
Ограниченные домены
Даже при наличии необходимых разрешений на доступ к домену, расширения не могут получить доступ к некоторым доменам. Скрипты содержимого заблокированы от выполнения на этих доменах, например, для защиты пользователя от расширения, которое может получить повышенные привилегии через специальные страницы.
В Firefox это включает следующие домены:
- accounts-static.cdn.mozilla.net
- accounts.firefox.com
- addons.cdn.mozilla.net
- addons.mozilla.org
- api.accounts.firefox.com
- content.cdn.mozilla.net
- discovery.addons.mozilla.org
- install.mozilla.org
- oauth.accounts.firefox.com
- profile.accounts.firefox.com
- support.mozilla.org
- sync.services.mozilla.com
Другие браузеры имеют аналогичные ограничения на веб-сайты, с которых могут устанавливаться расширения. Например, доступ к chrome.google.com ограничен в Chrome.
Примечание: Поскольку эти ограничения включают addons.mozilla.org, пользователи, которые пытаются использовать ваше расширение сразу после установки, могут обнаружить, что оно не работает. Чтобы избежать этого, вы должны добавить соответствующее предупреждение или страницу ознакомления для перенаправления пользователей от addons.mozilla.org.
Набор доменов может быть дополнительно ограничен с помощью корпоративных политик: Firefox распознает политику restricted_domains как описано в ExtensionSettings в mozilla/policy-templates. Политика runtime_blocked_hosts Chrome описана в Configure ExtensionSettings policy.
Окружение скрипта содержимого
Доступ к DOM
Скрипты содержимого могут получать доступ к DOM страницы и изменять его, как и обычные скрипты страницы. Они также могут видеть любые изменения, внесенные в DOM скриптами страницы.
Однако, скрипты содержимого получают "чистый" вид DOM. Это означает:
- Скрипты содержимого не могут видеть переменные JavaScript, определенные скриптами страницы.
- Если скрипт страницы переопределяет встроенное свойство DOM, скрипт содержимого видит исходную версию свойства, а не переопределённую.
Как указано в "Окружение скрипта содержимого" в несовместимостях Chrome, поведение различается в разных браузерах:
- В Firefox, это поведение называется Xray vision. Скрипты содержимого могут столкнуться с объектами JavaScript из своей собственной глобальной области видимости или Xray-обернутыми версиями с веб-страницы.
- В Chrome это поведение обеспечивается через изолированный мир, который использует принципиально другой подход.
Рассмотрим веб-страницу такого вида:
<!DOCTYPE html> <html lang="en-US"> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> </head> <body> <script src="page-scripts/page-script.js"></script> </body> </html>
Скрипт page-script.js делает следующее:
// page-script.js // add a new element to the DOM let p = document.createElement("p"); p.textContent = "This paragraph was added by a page script."; p.setAttribute("id", "page-script-para"); document.body.appendChild(p); // define a new property on the window window.foo = "This global variable was added by a page script"; // redefine the built-in window.confirm() function window.confirm = () => { alert("The page script has also redefined 'confirm'"); }
Теперь расширение вставляет скрипт содержимого в страницу:
// content-script.js // can access and modify the DOM let pageScriptPara = document.getElementById("page-script-para"); pageScriptPara.style.backgroundColor = "blue"; // can't see properties added by page-script.js console.log(window.foo); // undefined // sees the original form of redefined properties window.confirm("Are you sure?"); // calls the original window.confirm()
То же самое верно и в обратном направлении; скрипты страницы не могут видеть свойства JavaScript, добавленные скриптами содержимого.
Это означает, что скрипты содержимого могут полагаться на предсказуемое поведение свойств DOM, не беспокоясь о конфликте переменных со скриптами страницы.
Одно практическое следствие этого поведения заключается в том, что скрипт содержимого не имеет доступа к каким-либо JavaScript-библиотекам, загруженным на странице. Например, если на странице включена jQuery, скрипт содержимого её не увидит.
Если скрипту содержимого необходима JavaScript-библиотека, то сама библиотека должна быть вставлена в качестве скрипта содержимого наряду со скриптом содержимого, который её использует:
"content_scripts": [ { "matches": ["*://*.mozilla.org/*"], "js": ["jquery.js", "content-script.js"] } ]
Примечание: Firefox предоставляет некоторые API, которые позволяют скриптам содержимого получать доступ к объектам JavaScript, созданным скриптами страницы, и предоставлять свои собственные JavaScript-объекты скриптам страницы.
См. Обмен объектами со скриптами страницы для получения более подробной информации.
API расширений WebExtension
XHR и Fetch
Скрипты содержимого могут выполнять запросы с помощью обычных API window.XMLHttpRequest и window.fetch().
Примечание: В Firefox в манифесте V2 запросы скриптов содержимого (например, с использованием fetch()) выполняются в контексте расширения, поэтому вы должны предоставить абсолютный URL для ссылки на содержимое страницы.
В Chrome и Firefox в манифесте V3 эти запросы выполняются в контексте страницы, поэтому они выполняются по относительному URL. Например, /api отправляется в https://«current page URL»/api.
Скрипты содержимого получают те же привилегии междоменного доступа, что и остальная часть расширения: если расширение запросило междоменный доступ к домену с использованием ключа permissions в manifest.json, то его скрипты содержимого также получат доступ к этому домену.
Примечание: При использовании манифеста V3 скрипты содержимого могут выполнять запросы между источниками, если целевой сервер включил поддержку CORS; однако разрешения на хосты не работают в скриптах содержимого, но они по-прежнему работают на обычных страницах расширения.
Это достигается путём экспонирования более привилегированных экземпляров XHR и fetch в скрипте содержимого, что имеет побочный эффект, заключающийся в том, что заголовки Origin и Referer не устанавливаются так, как это бы произошло при запросе со страницы; это часто предпочтительнее для предотвращения раскрытия междоменного характера запроса.
Примечание: В Firefox в манифесте V2 расширения, которые должны выполнять запросы, как если бы они были отправлены самим содержимым, могут использовать content.XMLHttpRequest и content.fetch() вместо этого.
Для кроссбраузерных расширений наличие этих методов должно проверяться динамически.
Это невозможно в манифесте V3, так как content.XMLHttpRequest и content.fetch() недоступны.
Примечание: В Chrome, начиная с версии 73, и в Firefox, начиная с версии 101 при использовании манифеста V3, скрипты содержимого подчиняются той же политике CORS, что и страница, в которой они выполняются. Только скрипты бэкэнда имеют повышенные привилегии доступа между доменами. См. Изменения в запросах между источниками в скриптах содержимого расширений Chrome.
Взаимодействие со скриптами фонового процесса
Хотя скрипты содержимого не могут напрямую использовать большинство API WebExtension, они могут общаться со скриптами фонового процесса расширения с помощью API обмена сообщениями и, следовательно, косвенно получить доступ ко всем тем же API, что и скрипты фонового процесса.
Существуют два основных паттерна для обмена сообщениями между скриптами фонового процесса и скриптами содержимого:
- Можно отправлять одноразовые сообщения (с необязательным ответом).
- Можно установить более длительное соединение между двумя сторонами и использовать это соединение для обмена сообщениями.
Одноразовые сообщения
Для отправки одноразовых сообщений с необязательным ответом можно использовать следующие API:
| В скрипте содержимого | В скрипте фонового процесса | |
|---|---|---|
| Отправить сообщение | browser.runtime.sendMessage() | browser.tabs.sendMessage() |
| Получить сообщение | browser.runtime.onMessage | browser.runtime.onMessage |
Например, вот скрипт содержимого, который прослушивает события клика на веб-странице.
Если клик был по ссылке, он отправляет сообщение в фоновую страницу с целевым URL:
// content-script.js window.addEventListener("click", notifyExtension); function notifyExtension(e) { if (e.target.tagName !== "A") { return; } browser.runtime.sendMessage({"url": e.target.href}); }
Скрипт фонового процесса прослушивает эти сообщения и отображает уведомление с помощью API notifications:
// background-script.js browser.runtime.onMessage.addListener(notify); function notify(message) { browser.notifications.create({ "type": "basic", "iconUrl": browser.extension.getURL("link.png"), "title": "You clicked a link!", "message": message.url }); }
(Этот пример кода немного адаптирован из примера notify-link-clicks-i18n на GitHub.)
Обмен сообщениями на основе соединения
Отправка одноразовых сообщений может стать громоздкой, если вы обмениваетесь множеством сообщений между скриптом фонового процесса и скриптом содержимого. Поэтому альтернативный способ — установить более долговременное соединение между двумя контекстами и использовать его для обмена сообщениями.
Обе стороны имеют объект runtime.Port, который они могут использовать для обмена сообщениями.
Для создания соединения:
- Одна сторона прослушивает подключения с помощью
runtime.onConnect - Другая сторона вызывает:
-
tabs.connect()(если подключается к скрипту содержимого) -
runtime.connect()(если подключается к скрипту фонового процесса)
-
Это возвращает объект runtime.Port.
- Слушатель
runtime.onConnectполучает свой собственный объектruntime.Port.
После того, как каждая сторона получила порт, две стороны могут:
- Отправлять сообщения с помощью
runtime.Port.postMessage() - Получать сообщения с помощью
runtime.Port.onMessage()
Например, как только он загрузится, следующий скрипт содержимого:
- Подключается к скрипту фонового процесса
- Сохраняет объект
Portв переменнойmyPort - Прослушивает сообщения на
myPort(и регистрирует их) - Использует
myPortдля отправки сообщений в скрипт фонового процесса, когда пользователь нажимает на документ
// content-script.js let myPort = browser.runtime.connect({name:"port-from-cs"}); myPort.postMessage({greeting: "hello from content script"}); myPort.onMessage.addListener((m) => { console.log("In content script, received message from background script: "); console.log(m.greeting); }); document.body.addEventListener("click", () => { myPort.postMessage({greeting: "they clicked the page!"}); });
Соответствующий скрипт фонового процесса:
- Прослушивает попытки подключения от скрипта содержимого
- При получении попытки подключения:
- Сохраняет порт в переменную с именем
portFromCS - Отправляет скрипту содержимого сообщение с помощью порта
- Начинает прослушивать сообщения, полученные по порту, и регистрировать их
- Сохраняет порт в переменную с именем
- Отправляет сообщения скрипту содержимого, используя
portFromCS, когда пользователь нажимает на действие расширения в браузере
// background-script.js let portFromCS; function connected(p) { portFromCS = p; portFromCS.postMessage({greeting: "hi there content script!"}); portFromCS.onMessage.addListener((m) => { portFromCS.postMessage({greeting: `In background script, received message from content script: ${m.greeting}`}); }); } browser.runtime.onConnect.addListener(connected); browser.browserAction.onClicked.addListener(() => { portFromCS.postMessage({greeting: "they clicked the button!"}); });
Несколько скриптов содержимого
Если у вас несколько скриптов содержимого, обменивающихся сообщениями одновременно, вам может потребоваться хранить соединения с ними в массиве.
// background-script.js let ports = [] function connected(p) { ports[p.sender.tab.id] = p // … } browser.runtime.onConnect.addListener(connected) browser.browserAction.onClicked.addListener(() => { ports.forEach((p) => { p.postMessage({greeting: "they clicked the button!"}) }) });
Выбор между одноразовыми сообщениями и обмен сообщениями на основе соединения
Выбор между одноразовыми и основанными на соединении методами обмена сообщениями зависит от того, как расширение планирует использовать обмены.
Рекомендуемые рекомендации:
-
Используйте одноразовые сообщения, когда…
- Ожидается только один ответ на сообщение.
- Небольшое количество скриптов прослушивают сообщения (
runtime.onMessageвызовы).
-
Используйте обмен сообщениями на основе соединения, когда…
- Скрипты участвуют в сессиях, где обмениваются несколькими сообщениями.
- Расширение нуждается в знании о прогрессе задач или о прерывании задачи, или хочет прервать задачу, начатую с помощью обмена сообщениями.
Взаимодействие со страницей веб-приложения
По умолчанию скрипты содержимого не получают доступ к объектам, созданным скриптами страницы. Однако они могут общаться со скриптами страницы, используя API DOM window.postMessage и window.addEventListener.
Например:
// page-script.js let messenger = document.getElementById("from-page-script"); messenger.addEventListener("click", messageContentScript); function messageContentScript() { window.postMessage({ direction: "from-page-script", message: "Message from the page" }, "*"); }
// content-script.js window.addEventListener("message", (event) => { if ( event.source === window && event?.data?.direction === "from-page-script" ) { alert(`Content script received message: "${event.data.message}"`); } });
Для получения полного работающего примера посетите страницу демо на GitHub и следуйте инструкциям.
Предупреждение: Будьте очень осторожны при взаимодействии с ненадежным веб-содержимым таким образом! Расширения — это привилегированный код, который может иметь мощные возможности, и вредоносные веб-страницы могут легко обмануть их, заставив получить доступ к этим возможностям.
Для примера, предположим, что код скрипта содержимого, который получает сообщение, делает что-то вроде этого:
// content-script.js window.addEventListener("message", (event) => { if ( event.source === window && event?.data?.direction === "from-page-script" ) { eval(event.data.message); } });
Теперь скрипт страницы может запускать любой код со всеми привилегиями скрипта содержимого.
Использование eval() в скриптах содержимого
Примечание: eval() недоступно в Manifest V3.
- В Chrome
-
evalвсегда выполняет код в контексте скрипта содержимого, а не в контексте страницы. - В Firefox
-
Если вы вызываете
eval(), он выполняет код в контексте скрипта содержимого.Если вы вызываете
window.eval(), он выполняет код в контексте страницы.
Например, рассмотрим скрипт содержимого такого вида:
// content-script.js window.eval('window.x = 1;'); eval('window.y = 2'); console.log(`In content script, window.x: ${window.x}`); console.log(`In content script, window.y: ${window.y}`); window.postMessage({ message: "check" }, "*");
Этот код просто создаёт некоторые переменные x и y с помощью window.eval() и eval(), выводит их значения и затем отправляет сообщение на страницу.
При получении сообщения скрипт страницы выводит те же переменные:
window.addEventListener("message", (event) => { if (event.source === window && event.data && event.data.message === "check") { console.log(`In page script, window.x: ${window.x}`); console.log(`In page script, window.y: ${window.y}`); } });
В Chrome это даёт вывод, похожий на этот:
In content script, window.x: 1 In content script, window.y: 2 In page script, window.x: undefined In page script, window.y: undefined
В Firefox, этот код даёт такой вывод:
In content script, window.x: undefined In content script, window.y: 2 In page script, window.x: 1 In page script, window.y: undefined
То же самое относится к setTimeout(), setInterval() и Function().
Предупреждение: Будьте очень внимательны, когда выполняете код в контексте страницы!
Окружение страницы контролируется потенциально вредоносными веб-страницами, которые могут переопределить объекты, с которыми вы взаимодействуете, чтобы они вели себя неожиданным образом:
// page.js redefines console.log let original = console.log; console.log = () => { original(true); }
// content-script.js calls the redefined version window.eval('console.log(false)');
© 2005–2023 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts