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







