Як правильно впровадити 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 будь-якої складності.







