Пожалуй, каждый интернет-пользователь нет-нет да сталкивался с ситуацией, когда искомый товар или услугу уводили буквально из-под носа. Например, подобное часто бывает с ж/д- или авиабилетами. Ну или с билетами на концерт Канье Уэста. Либо же при продаже/раздаче каких-либо лимитированных предметов. Либо когда кто-то по кругу бронирует место в отеле, при этом не выкупая его, но лишая вас возможности заселиться. В общем, примеров может быть много.
И можно, конечно, часами сидеть, обновлять страницу и надеяться успеть сделать заветный «жамк» по кнопке до того, как вас опередят, но зачем? Куда комфортнее просто ввести искомую комбинацию символов (читай — целевое слово) и просто подойти к компу в момент его появления. Именно так и работает наше сегодняшнее расширение. Вы просто вводите запрос, например «Доступно», задаете регулярность обновления страницы и уходите заниматься своими делами, ожидая «пилик».
Напомним, что ранее мы уже рассказывали, как сделать:
Что ж — давайте создадим своими руками расширение для отслеживания изменений на странице прямо сейчас!
Важные нюансы
Впрочем, сначала немного словоблудия — увы, без этого никак. Во-первых, советуем изучить этот материал — это первая статья цикла, и в ней описаны базовые принципы работы расширений. В других статьях цикла ее нет, чтоб не писать одно и то же каждый раз. Во-вторых, расширение для отслеживания изменений на странице будет состоять из вот таких файлов:
manifest.json — стандартный и обязательный для любого расширения файл, описывающий, что это, собственно, вообще расширение, а не просто папка с файлами.
watcher.js — файл с основными скриптами нашего расширения.
popup.html — файл с графикой интерфейса.
popup.js — файл с логикой интерфейса.
Создаем расширение для отслеживания изменений на странице
Итак, создаем в любой папке с названием на латинице (вообще, можно и на кириллице — современные браузеры ее переваривают, но лучше на латинице) четыре файла — описанные выше manifest.json, watcher.js, popup.html и popup.js. Затем следуем таким шагам:
Открываем файл 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": "Поиск текста"
}
}
Закрываем файл manifest.json с сохранением изменений.
Открываем файл 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
)
);
}
});
})();
Закрываем файл watcher.js с сохранением изменений.
Открываем файл 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>
Закрываем файл popup.html с сохранением изменений.
Открываем файл 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;
}
});
Закрываем файл popup.js с сохранением изменений.
Затем в своем браузере находим «три точки», жмем на них, после — на «Расширения», на «Управления расширениями» и далее на «Загрузить распакованное расширение».
Выбираем нашу папку с файлами расширения, загружаем ее в браузер как расширение, ну и не забываем все протестировать, чтобы убедиться, что оно работает как надо.
Саммари
Как видите, сидеть и часами тыкать F5 ручками не обязательно. Достаточно создать расширение, которое отслеживало бы изменения на странице за вас и подавало звуковой сигнал, когда появится то, что вы искали. Главное — не забыть выкрутить звук компа на максимум.