Как создать расширение для отслеживания изменений на странице
Traffic Cardinal Traffic Cardinal  написал 15.10.2026

Как создать расширение для отслеживания изменений на странице

Traffic Cardinal Traffic Cardinal  написал 15.10.2026
8 мин
0
21
Содержание

Пожалуй, каждый интернет-пользователь нет-нет да сталкивался с ситуацией, когда искомый товар или услугу уводили буквально из-под носа. Например, подобное часто бывает с ж/д- или авиабилетами. Ну или с билетами на концерт Канье Уэста. Либо же при продаже/раздаче каких-либо лимитированных предметов. Либо когда кто-то по кругу бронирует место в отеле, при этом не выкупая его, но лишая вас возможности заселиться. В общем, примеров может быть много.

banner banner

И можно, конечно, часами сидеть, обновлять страницу и надеяться успеть сделать заветный «жамк» по кнопке до того, как вас опередят, но зачем? Куда комфортнее просто ввести искомую комбинацию символов (читай — целевое слово) и просто подойти к компу в момент его появления. Именно так и работает наше сегодняшнее расширение. Вы просто вводите запрос, например «Доступно», задаете регулярность обновления страницы и уходите заниматься своими делами, ожидая «пилик».

Самый простой способ проверить работоспособность: обратите внимание на статус «Поиск и автоперезагрузка включены»
Самый простой способ проверить работоспособность: обратите внимание на статус «Поиск и автоперезагрузка включены»

Напомним, что ранее мы уже рассказывали, как сделать:

На скрине не слышно, но оно еще делает бесконечный пилик-пилик с интервалом в 1 секунду, пока не нажмешь «остановить». Также видим, что статус изменился на «Ожидание»
На скрине не слышно, но оно еще делает бесконечный пилик-пилик с интервалом в 1 секунду, пока не нажмешь «остановить». Также видим, что статус изменился на «Ожидание»

Что ж — давайте создадим своими руками расширение для отслеживания изменений на странице прямо сейчас!

Важные нюансы

Впрочем, сначала немного словоблудия — увы, без этого никак. Во-первых, советуем изучить этот материал — это первая статья цикла, и в ней описаны базовые принципы работы расширений. В других статьях цикла ее нет, чтоб не писать одно и то же каждый раз. Во-вторых, расширение для отслеживания изменений на странице будет состоять из вот таких файлов:

  • manifest.json — стандартный и обязательный для любого расширения файл, описывающий, что это, собственно, вообще расширение, а не просто папка с файлами.

  • watcher.js — файл с основными скриптами нашего расширения.

  • popup.html — файл с графикой интерфейса.

  • popup.js — файл с логикой интерфейса.

Создаем расширение для отслеживания изменений на странице

Итак, создаем в любой папке с названием на латинице (вообще, можно и на кириллице — современные браузеры ее переваривают, но лучше на латинице) четыре файла — описанные выше manifest.json, watcher.js, popup.html и popup.js. Затем следуем таким шагам:

  1. Открываем файл manifest.json и вставляем в него вот это:

{

"manifest_version": 3,

"name": "Text Beep",

"version": "2.0",

"description": "Поиск текста, звуковое уведомление и автоперезагрузка",

"permissions": [

"activeTab",

"scripting",

"storage"

],

"host_permissions": [

"<all_urls>"

],

"content_scripts": [

{

"matches": ["<all_urls>"],

"js": ["watcher.js"],

"run_at": "document_idle"

}

],

"action": {

"default_popup": "popup.html",

"default_title": "Поиск текста"

}

}

  1. Закрываем файл manifest.json с сохранением изменений.

  2. Открываем файл watcher.js и вставляем в него вот это:

(() => {

window.stopTextWatcher?.();

let pageMutationObserver = null;

let notificationAudioContext = null;

let pageReloadTimeout = null;

let repeatingBeepInterval = null;

let textWasFoundPreviously = false;

let watcherWasStopped = false;

window.startTextWatcher = (

searchText,

intervalSeconds

) => {

window.stopTextWatcher?.();

watcherWasStopped = false;

textWasFoundPreviously = false;

const normalizedSearchText =

searchText.toLocaleLowerCase();

notificationAudioContext =

new AudioContext();

notificationAudioContext

.resume()

.catch(() => {});

function playNotificationBeep() {

if (

watcherWasStopped ||

!notificationAudioContext ||

notificationAudioContext.state === "closed"

) {

return;

}

notificationAudioContext

.resume()

.catch(() => {});

const notificationOscillator =

notificationAudioContext.createOscillator();

const notificationGain =

notificationAudioContext.createGain();

notificationOscillator.type = "sine";

notificationOscillator.frequency.value =

880;

notificationGain.gain.value =

0.25;

notificationOscillator.connect(

notificationGain

);

notificationGain.connect(

notificationAudioContext.destination

);

notificationOscillator.start();

notificationOscillator.stop(

notificationAudioContext.currentTime +

0.25

);

}

function beginContinuousNotification() {

if (

textWasFoundPreviously ||

watcherWasStopped

) {

return;

}

textWasFoundPreviously = true;

clearTimeout(pageReloadTimeout);

pageReloadTimeout = null;

playNotificationBeep();

repeatingBeepInterval =

setInterval(

playNotificationBeep,

1000

);

}

function checkCurrentPageText() {

if (

watcherWasStopped ||

textWasFoundPreviously ||

!document.body

) {

return;

}

const currentVisiblePageText =

document.body.innerText

.toLocaleLowerCase();

if (

currentVisiblePageText.includes(

normalizedSearchText

)

) {

beginContinuousNotification();

}

}

pageMutationObserver =

new MutationObserver(

checkCurrentPageText

);

pageMutationObserver.observe(

document.documentElement,

{

childList: true,

subtree: true,

characterData: true

}

);

checkCurrentPageText();

if (!textWasFoundPreviously) {

pageReloadTimeout =

setTimeout(() => {

if (

!watcherWasStopped &&

!textWasFoundPreviously

) {

location.reload();

}

}, intervalSeconds * 1000);

}

window.stopTextWatcher = () => {

watcherWasStopped = true;

pageMutationObserver?.disconnect();

pageMutationObserver = null;

clearTimeout(

pageReloadTimeout

);

clearInterval(

repeatingBeepInterval

);

pageReloadTimeout = null;

repeatingBeepInterval = null;

if (

notificationAudioContext &&

notificationAudioContext.state !== "closed"

) {

notificationAudioContext.close();

}

notificationAudioContext = null;

};

};

chrome.storage.local

.get("textBeepConfig")

.then(({ textBeepConfig }) => {

if (

textBeepConfig?.enabled &&

textBeepConfig.searchText

) {

window.startTextWatcher(

textBeepConfig.searchText,

Math.max(

1,

Number(

textBeepConfig.intervalSeconds

) || 60

)

);

}

});

})();

  1. Закрываем файл watcher.js с сохранением изменений.

  2. Открываем файл popup.html и вставляем в него вот это:

<!DOCTYPE html>

<html lang="ru">

<head>

<meta charset="UTF-8">

<style>

body {

width: 260px;

padding: 12px;

font-family: Arial, sans-serif;

box-sizing: border-box;

}

input,

button {

box-sizing: border-box;

width: 100%;

padding: 9px;

margin-top: 8px;

}

input {

border: 1px solid #999;

border-radius: 4px;

background: #fff;

}

button {

border: 1px solid #888;

border-radius: 4px;

cursor: pointer;

}

button:hover {

opacity: 0.9;

}

label {

display: block;

margin-top: 10px;

font-size: 12px;

}

#status {

margin-top: 10px;

font-size: 12px;

line-height: 16px;

min-height: 16px;

word-break: break-word;

}

</style>

</head>

<body>

<b>Поиск текста на странице</b>

<input

id="searchText"

type="text"

placeholder="Что ищем?"

autocomplete="off"

>

<label for="interval">

Перезагрузка каждые (секунды):

</label>

<input

id="interval"

type="number"

min="1"

step="1"

value="60"

>

<button id="start" type="button">

Начать поиск

</button>

<button id="stop" type="button">

Остановить

</button>

<div id="status">Ожидание</div>

<script src="popup.js"></script>

</body>

</html>

  1. Закрываем файл popup.html с сохранением изменений.

  2. Открываем файл popup.js и вставляем в него вот это:

const searchTextInputElement =

document.getElementById("searchText");

const reloadIntervalInputElement =

document.getElementById("interval");

const currentStatusTextElement =

document.getElementById("status");

const startSearchButtonElement =

document.getElementById("start");

const stopSearchButtonElement =

document.getElementById("stop");

startSearchButtonElement.addEventListener("click", async () => {

const enteredSearchText =

searchTextInputElement.value;

const enteredReloadInterval =

Number(reloadIntervalInputElement.value);

if (!enteredSearchText.trim()) {

currentStatusTextElement.textContent =

"Введи текст для поиска";

return;

}

if (

!Number.isFinite(enteredReloadInterval) ||

enteredReloadInterval < 1

) {

currentStatusTextElement.textContent =

"Интервал должен быть не меньше 1 секунды";

return;

}

try {

const currentlyActiveTabs =

await chrome.tabs.query({

active: true,

currentWindow: true

});

const currentlySelectedTab =

currentlyActiveTabs[0];

if (

!currentlySelectedTab?.id ||

!/^https?:/.test(

currentlySelectedTab.url || ""

)

) {

currentStatusTextElement.textContent =

"Открой обычную веб-страницу";

return;

}

const textBeepConfiguration = {

enabled: true,

searchText: enteredSearchText,

intervalSeconds: enteredReloadInterval,

tabId: currentlySelectedTab.id

};

await chrome.storage.local.set({

textBeepConfig: textBeepConfiguration

});

await chrome.scripting.executeScript({

target: {

tabId: currentlySelectedTab.id

},

files: ["watcher.js"]

});

currentStatusTextElement.textContent =

"Поиск и автоперезагрузка включены";

} catch (startupError) {

currentStatusTextElement.textContent =

"Ошибка запуска: " + startupError.message;

}

});

stopSearchButtonElement.addEventListener("click", async () => {

try {

const currentlyActiveTabs =

await chrome.tabs.query({

active: true,

currentWindow: true

});

const currentlySelectedTab =

currentlyActiveTabs[0];

const storedConfigurationResult =

await chrome.storage.local.get(

"textBeepConfig"

);

const storedTextBeepConfiguration =

storedConfigurationResult.textBeepConfig;

const disabledTextBeepConfiguration = {

...(storedTextBeepConfiguration || {}),

enabled: false

};

await chrome.storage.local.set({

textBeepConfig:

disabledTextBeepConfiguration

});

if (

currentlySelectedTab?.id &&

/^https?:/.test(

currentlySelectedTab.url || ""

)

) {

await chrome.scripting.executeScript({

target: {

tabId: currentlySelectedTab.id

},

func: () => {

window.stopTextWatcher?.();

}

});

}

currentStatusTextElement.textContent =

"Поиск и автоперезагрузка остановлены";

} catch (stopError) {

currentStatusTextElement.textContent =

"Ошибка остановки: " + stopError.message;

}

});

chrome.storage.local

.get("textBeepConfig")

.then(({ textBeepConfig }) => {

if (textBeepConfig) {

searchTextInputElement.value =

textBeepConfig.searchText || "";

reloadIntervalInputElement.value =

textBeepConfig.intervalSeconds || 60;

}

});

  1. Закрываем файл popup.js с сохранением изменений.

Затем в своем браузере находим «три точки», жмем на них, после — на «Расширения», на «Управления расширениями» и далее на «Загрузить распакованное расширение».

Выбираем нашу папку с файлами расширения, загружаем ее в браузер как расширение, ну и не забываем все протестировать, чтобы убедиться, что оно работает как надо.

Саммари

Как видите, сидеть и часами тыкать F5 ручками не обязательно. Достаточно создать расширение, которое отслеживало бы изменения на странице за вас и подавало звуковой сигнал, когда появится то, что вы искали. Главное — не забыть выкрутить звук компа на максимум.

Здравствуйте! У вас включен блокировщик рекламы, часть сайта не будет работать!