Как правильно внедрить content script в браузерное расширение?
Представьте: вы разработали расширение для Chrome, которое подсвечивает цены на странице интернет-магазина. Content script работает на первой загрузке, но когда пользователь переходит в корзину через pushState, скрипт перестаёт реагировать — MutationObserver не срабатывает. Такая ситуация — классика SPA-ловушек. Мы сталкиваемся с подобными кейсами на каждом втором проекте. В этой статье разберём, как корректно внедрить content script, обработать SPA-навигацию, организовать обмен данными с background и избежать типичных ошибок.
Content script — это механизм инъекции кода в страницу. Мы интегрируем content script — JavaScript-файл, который браузер внедряет в контекст целевой страницы в изолированном окружении, как указано в документации Chrome Extensions. Это даёт доступ к DOM, но не к переменным страницы, что является и защитой, и ограничением. Наш опыт — 5+ лет и 30+ успешных проектов в области браузерных расширений. Content script предназначен для модификации DOM целевой страницы.
Как браузер загружает content script
В manifest.json (MV3) объявляете список скриптов и условия их запуска. Параметр run_at определяет момент внедрения. Ниже — сравнение значений:
| run_at | Момент внедрения | Когда использовать |
|---|---|---|
document_start |
До построения DOM | Для перехвата начальных запросов |
document_end |
DOM готов, ресурсы ещё грузятся | Для модификации структуры до рендеринга |
document_idle |
После DOMContentLoaded | Безопасный дефолт для большинства задач |
{ "manifest_version": 3, "content_scripts": [ { "matches": ["https://*.example.com/*"], "js": ["content/injected.js"], "css": ["content/injected.css"], "run_at": "document_idle", "world": "ISOLATED" } ] } Если нужно внедрить скрипт динамически — из service worker или по требованию — используйте chrome.scripting.executeScript:
// background/service-worker.js chrome.action.onClicked.addListener(async (tab) => { await chrome.scripting.executeScript({ target: { tabId: tab.id, allFrames: false }, files: ['content/injected.js'], world: 'ISOLATED' }); }); Сравнение ISOLATED и MAIN world
| world | Доступ к DOM | Доступ к JS страницы | Изоляция | Когда использовать |
|---|---|---|---|---|
| ISOLATED | Полный | Нет | Высокая | По умолчанию |
| MAIN | Полный | Полный | Низкая | Monkey-patching, перехват вызовов |
world: 'MAIN' даёт доступ к переменным страницы, но теряет изоляцию — используйте только когда это реально нужно (перехват вызовов нативных API).
Почему content script не срабатывает на SPA-страницах?
При клиентской навигации браузер не перезагружает content script. MutationObserver на
let lastUrl = location.href; const urlObserver = new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; onNavigate(location.href); } }); urlObserver.observe(document.querySelector('title') ?? document.head, { subtree: true, characterData: true, childList: true }); Работа с DOM
Content script видит полный DOM, включая Shadow DOM. Для обработки динамического контента (SPA) обязателен MutationObserver. Пример — подсветка всех цен на странице:
function highlightPrices() { const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode(node) { return /\$[\d,]+\.?\d{0,2}/.test(node.textContent) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; } } ); const nodes = []; while (walker.nextNode()) nodes.push(walker.currentNode); nodes.forEach(node => { const span = document.createElement('span'); span.innerHTML = node.textContent.replace( /(\$[\d,]+\.?\d{0,2})/g, '<mark class="ext-price-highlight">$1</mark>' ); node.parentNode.replaceChild(span, node); }); } const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.addedNodes.length > 0) highlightPrices(); } }); observer.observe(document.body, { childList: true, subtree: true }); highlightPrices(); В проектах мы тестируем до 10 целевых сайтов, обрабатывая до 1000 DOM-узлов за один прогон.
Общение с background service worker
Content script не имеет прямого доступа к chrome.tabs, поэтому используйте messaging. Для двустороннего потока — установка порта:
// content/injected.js const port = chrome.runtime.connect({ name: 'content-stream' }); port.onMessage.addListener((msg) => { if (msg.type === 'DATA_CHUNK') appendChunk(msg.data); }); port.postMessage({ type: 'START_STREAM', url: location.href }); Основной сценарий — однократный запрос через chrome.runtime.sendMessage. Background должен вернуть true в колбэке, если ответ асинхронный.
Как избежать конфликтов стилей при внедрении content script?
Два подхода: Shadow DOM для полной изоляции интерфейса расширения или CSS с высокой специфичностью и уникальными префиксами классов (например, ext-). Если страница использует строгую CSP, добавляйте стили через chrome.scripting.insertCSS из service worker.
Передача данных из страницы в content script
Так как JS-контексты изолированы, используйте window.postMessage из скрипта страницы (или MAIN world) и ловите сообщения в content script с проверкой event.source === window.
Что входит в работу
Разработка content script под ключ включает:
- Настройку манифеста и декларацию скриптов.
- Обработку SPA-навигации и динамического контента.
- Интеграцию с background service worker через messaging.
- Изоляцию стилей и работу с CSP.
- Тестирование на целевых страницах (до 10 сайтов).
- Документацию по использованию и API расширения.
Типичные проблемы и их решения
Частая проблема — content script не срабатывает после SPA-перехода. Решение — MutationObserver на
Дополнительные сложности
- Если страница использует Shadow DOM, убедитесь, что ваш content script корректно проникает в открытые shadow roots. Для закрытых — без доступа.
- При работе с iframe для внедрения во фреймы укажите
allFrames: trueв настройках.
Мы гарантируем стабильную работу расширения в Chrome, Edge и Opera. Свяжитесь с нами для оценки вашего проекта или закажите разработку content script под ключ. Получите консультацию бесплатно — наши инженеры помогут реализовать content script любой сложности.







