Отслеживание видимости элементов с помощью API Intersection Observer
В этой статье мы создадим макет блога с несколькими объявлениями, вставленными среди контента страницы, а затем воспользуемся API Intersection Observer API, чтобы отслеживать время видимости каждого объявления для пользователя. Когда время видимости объявления превысит одну минуту, оно будет заменено новым.
Хотя многие аспекты этого примера не соответствуют реальному использованию (в частности, статьи имеют одинаковый текст и не загружаются из базы данных, и есть всего несколько простых текстовых объявлений, выбранных из массива), этого должно быть достаточно, чтобы понять API и быстро научиться применять его на вашем сайте.
Есть веская причина, по которой в этом примере используется отслеживание видимости объявлений. Оказывается, что одно из самых распространённых применений Flash или других скриптов в рекламе в Интернете заключается в записи того, как долго каждое объявление было видно, для целей выставления счетов и оплаты доходов. Без API Intersection Observer это делается с помощью интервалов и таймаутов для каждого отдельного объявления или с помощью других методов, которые, как правило, замедляют страницу. Использование этого API позволяет браузеру оптимизировать все процессы, значительно снизив влияние на производительность.
Давайте начнем!
Создание сайта
Структура сайта: HTML
Структура сайта не слишком сложная. Мы будем использовать CSS Grid для стилизации и выравнивания сайта, поэтому здесь можно быть довольно простыми:
<div class="wrapper">
<header>
<h1>A Fake Blog</h1>
<h2>Showing Intersection Observer in action!</h2>
</header>
<aside>
<nav>
<ul>
<li><a href="#link1">A link</a></li>
<li><a href="#link2">Another link</a></li>
<li><a href="#link3">One more link</a></li>
</ul>
</nav>
</aside>
<main>…</main>
</div>
Это основа всего сайта. Вверху находится заголовок сайта, заключенный в блок <header>. Ниже этого мы определяем боковую панель сайта как список ссылок в блоке <aside>.
Наконец, идёт основная часть. Мы начинаем с пустого элемента <main>. Этот блок будет заполнен с помощью скрипта позже.
Стилизация сайта с помощью CSS
Определив структуру сайта, переходим к стилизации. Давайте рассмотрим стиль каждого компонента страницы индивидуально.
Основы
Мы предоставим стили для элементов <body> и <main>, чтобы определить фон сайта, а также сетку, в которой будут размещены различные его части.
body {
font-family: "Open Sans", "Arial", "Helvetica", sans-serif;
background-color: aliceblue;
}
.wrapper {
display: grid;
grid-template-columns: auto minmax(min-content, 1fr);
grid-template-rows: auto minmax(min-content, 1fr);
max-width: 700px;
margin: 0 auto;
background-color: aliceblue;
}
Элемент <body> сайта настроен здесь для использования одного из ряда стандартных шрифтов без засечек и использования "aliceblue" в качестве цвета фона. Затем определяется класс "wrapper"; он охватывает весь блог, включая заголовок, боковую панель и основной контент (статьи и объявления).
Обёртка устанавливает сетку CSS с двумя колонками и двумя строками. Первая колонка (размер автоматически определяется по её содержимому) используется для боковой панели, а вторая колонка (которая будет использоваться для основного контента) имеет размер, не меньший ширины содержимого колонки и не больший оставшегося доступного места.
Первая строка будет использоваться специально для заголовка сайта. Размеры строк соответствуют размерам колонок: первая имеет автоматический размер, а вторая использует оставшееся место, но не меньше необходимого для размещения всех элементов в ней.
Ширина обёртки фиксирована и составляет 700px, чтобы она помещалась в доступном пространстве при представлении на MDN ниже.
Заголовок
Заголовок довольно простой, так как в этом примере он содержит только текст. Его стиль выглядит так:
header {
grid-column: 1 / -1;
grid-row: 1;
background-color: aliceblue;
}
grid-row устанавливается в 1, так как мы хотим разместить заголовок в верхней строке сетки сайта. Более интересным является наше использование grid-column здесь; здесь мы указываем, что хотим, чтобы колонка начиналась в первой колонке и заканчивалась в первой колонке после последней линии сетки — другими словами, заголовок охватывает все колонки в сетке. Идеально подходит для наших целей.
Боковая панель
Наша боковая панель используется для отображения ссылок на другие страницы сайта. Ни одна из них не работает в нашем примере, но они существуют для создания впечатления от блога. Боковая панель представлена элементом <aside> и стилизована следующим образом:
aside {
grid-column: 1;
grid-row: 2;
background-color: cornsilk;
padding: 5px 10px;
}
aside ul {
padding-left: 0;
}
aside ul li {
list-style: none;
}
aside ul li a {
text-decoration: none;
}
Самое важное, что grid-column установлено в 1, чтобы поместить боковую панель слева. Если изменить это на -1, она появится справа (хотя для получения правильного отступа потребуются некоторые изменения в отступах других элементов).
grid-row установлено в 2, чтобы разместить её рядом с основным контентом сайта.
Основной контент
Говоря о основном контенте сайта: основной контент сайта хранится в элементе <main>. К нему применяется следующий стиль:
main {
grid-column: 2;
grid-row: 2;
margin: 0;
margin-left: 16px;
font-size: 16px;
}
Основной особенностью здесь является то, что положение в сетке устанавливается для размещения основного контента во второй колонке, второй строке.
Статьи
Каждая статья заключена в элемент <article>, стилизованный следующим образом:
article {
background-color: white;
padding: 6px;
}
article:not(:last-child) {
margin-bottom: 8px;
}
article h2 {
margin-top: 0;
}
Это создаёт блоки статей с белым фоном, которые наложены на синий фон, со небольшим отступом вокруг статьи. У каждой статьи, которая не является последним элементом в контейнере, есть нижний отступ 8px для разделения элементов.
Объявления
Наконец, объявления имеют следующий начальный стиль. Отдельные объявления могут несколько изменить стиль, как мы увидим позже.
.ad {
height: 96px;
padding: 6px;
border-color: #555;
border-style: solid;
border-width: 1px;
}
.ad:not(:last-child) {
margin-bottom: 8px;
}
.ad h2 {
margin-top: 0;
}
.ad div {
position: relative;
float: right;
padding: 0 4px;
height: 20px;
width: 120px;
font-size: 14px;
bottom: 30px;
border: 1px solid black;
background-color: rgb(255 255 255 / 50%);
}
Здесь нет ничего особенного. Это довольно базовый CSS.
Связывание всего с JavaScript
Это приводит нас к коду JavaScript, который заставляет все работать. Начнём с глобальных переменных:
let contentBox; let nextArticleID = 1; const visibleAds = new Set(); let previouslyVisibleAds = null; let adObserver; let refreshIntervalID = 0;
Они используются следующим образом:
contentBox-
Ссылка на объект
<main>элемента в DOM. Именно сюда мы будем вставлять статьи и объявления. nextArticleID-
Каждой статье присваивается уникальный идентификационный номер; эта переменная отслеживает следующий доступный ID, начиная с 1.
visibleAds-
Set, который используется для отслеживания объявлений, которые в данный момент отображаются на экране. previouslyVisibleAds-
Используется для временного хранения списка видимых объявлений, пока документ не отображается (например, если пользователь переключился на другую вкладку).
adObserver-
Будет содержать наш
IntersectionObserver, используемый для отслеживания пересечения между объявлениями и границами элемента<main>. refreshIntervalID-
Используется для хранения идентификатора интервала, возвращённого функцией
setInterval(). Этот интервал будет использоваться для периодического обновления содержимого объявлений.
Настройка
Для настройки мы выполняем функцию startup() ниже при загрузке страницы:
window.addEventListener("load", startup, false);
function startup() {
contentBox = document.querySelector("main");
document.addEventListener("visibilitychange", handleVisibilityChange, false);
const observerOptions = {
root: null,
rootMargin: "0px",
threshold: [0.0, 0.75],
};
adObserver = new IntersectionObserver(intersectionCallback, observerOptions);
buildContents();
refreshIntervalID = setInterval(handleRefreshInterval, 1000);
}
Сначала получаем ссылку на элемент, содержащий элемент <main>, чтобы мы могли вставить в него наше содержимое. Затем мы устанавливаем обработчик события visibilitychange. Это событие отправляется, когда документ скрыт или виден, например, когда пользователь переключается между вкладками в браузере. API Intersection Observer не учитывает это при обнаружении пересечения, так как пересечение не зависит от видимости страницы. Поэтому нам нужно приостанавливать таймеры, когда страница скрыта; отсюда и этот обработчик события.
Далее мы настраиваем параметры для IntersectionObserver, которые будут отслеживать изменения пересечения целевых элементов (в нашем случае — объявлений) относительно документа. Параметры настроены для наблюдения за пересечением с видимой областью документа (установив root на null). У нас нет отступов для расширения или сужения прямоугольника корня пересечения; мы хотим точно соответствовать границам видимой области документа для целей пересечения. А threshold установлено в массив, содержащий значения 0,0 и 0,75; это заставит наш обратный вызов выполняться всякий раз, когда целевой элемент полностью скрывается или начинает проявляться (отношение пересечения 0,0) или проходит через 75% видимости в любом направлении (отношение пересечения 0,75).
Наблюдатель adObserver создаётся путём вызова конструктора IntersectionObserver, передавая функцию обратного вызова intersectionCallback и наши параметры.
Затем мы вызываем функцию buildContents(), которую мы определим позже, чтобы фактически сгенерировать и вставить в документ статьи и объявления, которые мы хотим отобразить.
Наконец, мы устанавливаем интервал, который срабатывает один раз в секунду, чтобы обработать любые необходимые обновления. Нам нужен обновление раз в секунду, так как в этом примере во всех видимых объявлениях отображаются таймеры. Вам может вообще не потребоваться интервал, или вы можете сделать это по-другому или с другим интервалом.
Обработка изменений видимости документа
Давайте рассмотрим обработчик события visibilitychange. Наш скрипт получает это событие, когда сам документ становится видимым или невидимым. Самый важный сценарий здесь — когда пользователь переключается между вкладками. Поскольку Intersection Observer заботится только о пересечении между целевыми элементами и корнем пересечения, а не о видимости вкладки (это совершенно другая проблема), нам нужно использовать API видимости страницы, чтобы обнаруживать эти переключения вкладок и отключать наши таймеры на это время.
function handleVisibilityChange() {
if (document.hidden) {
if (!previouslyVisibleAds) {
previouslyVisibleAds = visibleAds;
visibleAds = [];
previouslyVisibleAds.forEach((adBox) => {
updateAdTimer(adBox);
adBox.dataset.lastViewStarted = 0;
});
}
} else {
previouslyVisibleAds.forEach((adBox) => {
adBox.dataset.lastViewStarted = performance.now();
});
visibleAds = previouslyVisibleAds;
previouslyVisibleAds = null;
}
}
Поскольку само событие не указывает, переключился ли документ из видимого в невидимый или наоборот, проверяется свойство document.hidden, чтобы увидеть, не отображается ли в данный момент документ. Поскольку теоретически это может вызываться несколько раз, мы продолжаем только в том случае, если таймеры ещё не приостановлены и состояния видимости существующих объявлений не сохранены.
Для приостановки таймеров нам нужно только удалить объявления из набора видимых объявлений (visibleAds) и пометить их как неактивные. Для этого мы начинаем с сохранения набора видимых объявлений в переменную, известную как previouslyVisibleAds, чтобы убедиться, что мы можем восстановить их, когда пользователь вернётся на страницу, а затем очищаем набор visibleAds, чтобы они больше не обрабатывались как видимые. Затем для каждого из приостанавливаемых объявлений мы вызываем нашу функцию updateAdTimer(), которая обрабатывает обновление счётчика общего времени видимости объявления, а затем устанавливает их свойство dataset.lastViewStarted в 0, что указывает на то, что таймер вкладки не работает.
Если документ только что стал видимым, мы повторяем этот процесс: сначала мы проходим по previouslyVisibleAds и устанавливаем для каждого свойство dataset.lastViewStarted на текущее время документа (в миллисекундах с момента создания документа) с использованием метода performance.now(). Затем мы устанавливаем visibleAds обратно на previouslyVisibleAds и устанавливаем последнее на null. Теперь объявления все перезапущены и настроены так, чтобы знать, что они стали видимыми в текущее время, поэтому они не будут суммировать время, в течение которого страница была скрыта, при следующем обновлении.
Обработка изменений пересечения
Каждые IntersectionObserver раз в цикле событий браузера проверяет, не пересекли ли какие-либо из его целевых элементов пороги отношения пересечения наблюдателя. Для каждого наблюдателя собирается список целевых элементов, которые это сделали, и передаётся в функцию обратного вызова наблюдателя в виде массива объектов IntersectionObserverEntry. Наш обратный вызов, intersectionCallback(), выглядит следующим образом:
function intersectionCallback(entries) {
entries.forEach((entry) => {
const adBox = entry.target;
if (entry.isIntersecting) {
if (entry.intersectionRatio >= 0.75) {
adBox.dataset.lastViewStarted = entry.time;
visibleAds.add(adBox);
}
} else {
visibleAds.delete(adBox);
if (
entry.intersectionRatio === 0.0 &&
adBox.dataset.totalViewTime >= 60000
) {
replaceAd(adBox);
}
}
});
}
Как упоминалось ранее, функция обратного вызова IntersectionObserver получает на вход массив всех целевых элементов наблюдателя, которые стали более или менее видимыми, чем одно из отношений пересечения наблюдателя. Мы перебираем каждый из этих элементов — которые являются объектами типа IntersectionObserverEntry. Если целевой элемент пересекается с корнем, мы знаем, что он только что перешёл от состояния скрытия к состоянию видимости. Если он стал хотя бы на 75% видимым, то мы считаем объявление видимым и запускаем таймер, установив атрибут dataset.lastViewStarted объявления на время перехода в entry.time, а затем добавляем объявление в набор visibleAds, чтобы мы знали, что будем обрабатывать его со временем.
Если объявление перешло в состояние отсутствия пересечения, мы удаляем объявление из набора видимых объявлений. Затем у нас есть одна специальная функция: мы проверяем, равен ли entry.intersectionRatio 0,0; если это так, это означает, что элемент полностью скрылся. Если это так, и объявление было видно в течение как минимум одной минуты, мы вызываем функцию replaceAd(), которую мы создадим, чтобы заменить существующее объявление новым. Таким образом, пользователь видит различные объявления со временем, но объявления заменяются только тогда, когда они не видны, что обеспечивает плавную работу.
Обработка периодических действий
Обработчик интервала handleRefreshInterval() вызывается примерно один раз в секунду благодаря вызову setInterval(), сделанному в функции startup() описанной выше. Его основная задача — обновлять таймеры каждую секунду и планировать перерисовку для обновления таймеров, которые будут отображаться в каждом объявлении.
function handleRefreshInterval() {
const redrawList = [];
visibleAds.forEach((adBox) => {
const previousTime = adBox.dataset.totalViewTime;
updateAdTimer(adBox);
if (previousTime !== adBox.dataset.totalViewTime) {
redrawList.push(adBox);
}
});
if (redrawList.length) {
window.requestAnimationFrame((time) => {
redrawList.forEach((adBox) => {
drawAdTimer(adBox);
});
});
}
}
Массив redrawList будет использоваться для хранения списка всех объявлений, которые необходимо перерисовать в этом цикле обновления, так как он может не совпадать точно с истекшим временем из-за активности системы или потому, что вы установили интервал на значение, отличное от 1000 миллисекунд.
Затем для каждого видимого объявления мы сохраняем значение dataset.totalViewTime (общее количество миллисекунд, в течение которого объявление было видимым на момент последнего обновления) и вызываем updateAdTimer() для обновления времени. Если оно изменилось, мы добавляем объявление в redrawList, чтобы мы знали, что оно должно быть обновлено в следующем кадре анимации.
Наконец, если перерисовать хотя бы один элемент, мы используем requestAnimationFrame(), чтобы запланировать функцию, которая перерисует каждый элемент в redrawList в следующем кадре анимации.
Обновление таймера видимости объявления
Ранее (см. Обработка изменений видимости документа и Обработка периодических действий) мы видели, что при необходимости обновить счётчик «общего времени видимости» объявления мы вызываем функцию с именем updateAdTimer() для этого. Эта функция принимает на вход объект HTMLDivElement объявления. Вот она:
function updateAdTimer(adBox) {
const lastStarted = adBox.dataset.lastViewStarted;
const currentTime = performance.now();
if (lastStarted) {
const diff = currentTime - lastStarted;
adBox.dataset.totalViewTime =
parseFloat(adBox.dataset.totalViewTime) + diff;
}
adBox.dataset.lastViewStarted = currentTime;
}
Для отслеживания времени видимости элемента мы используем два пользовательских атрибута данных (см. data-*) на каждом объявлении:
lastViewStarted-
Время в миллисекундах, относительное к времени создания документа, когда счётчик видимости объявления был последний раз обновлён или объявление стало видимым в последний раз. 0, если объявление не было видно при последней проверке.
totalViewTime-
Общее количество миллисекунд, в течение которого объявление было видно.
К ним можно получить доступ через атрибут HTMLElement.dataset каждого объявления, который предоставляет отображение DOMStringMap имени каждого пользовательского атрибута и его значения. Значения являются строками, но мы можем легко преобразовать их в числа — на самом деле JavaScript обычно делает это автоматически, хотя у нас будет один случай, когда мы должны сделать это сами.
Мы начинаем с извлечения времени последней проверки видимости объявления (adBox.dataset.lastViewStarted) в локальную переменную с именем lastStarted. Мы также получаем текущее значение времени с момента создания, используя performance.now(), в currentTime.
Если lastStarted не равно нулю — то есть таймер в данный момент работает, мы вычисляем разницу между текущим временем и начальным временем, чтобы определить количество миллисекунд, в течение которых таймер был видим с момента последнего изменения видимости. Это значение добавляется к текущему значению totalViewTime объявления, чтобы обновить общую сумму. Обратите внимание на использование parseFloat() здесь; поскольку эти значения являются строками, JavaScript пытается выполнить конкатенацию строк вместо сложения без него.
Наконец, время последнего просмотра для объявления обновляется до текущего времени. Это делается независимо от того, был ли таймер объявления запущен, когда эта функция была вызвана или нет; это гарантирует, что таймер объявления всегда работает, когда эта функция возвращает значение. Это логично, поскольку эта функция вызывается только если объявление видно, даже если оно только что стало видимым.
Отображение таймера объявления
Внутри каждого объявления для демонстрационных целей мы отображаем текущее значение его totalViewTime, преобразованное в минуты и секунды. Это обрабатывается путем передачи элемента объявления в функцию drawAdTimer():
function drawAdTimer(adBox) {
const timerBox = adBox.querySelector(".timer");
const totalSeconds = adBox.dataset.totalViewTime / 1000;
const sec = Math.floor(totalSeconds % 60);
const min = Math.floor(totalSeconds / 60);
timerBox.innerText = `${min}:${sec.toString().padStart(2, "0")}`;
}
Этот код находит таймер объявления по его идентификатору "timer", и вычисляет количество прошедших секунд, разделив totalViewTime объявления на 1000. Затем он рассчитывает количество минут и секунд, прошедших, прежде чем установить innerText таймера на строку, представляющую это время в формате m:ss. Метод String.padStart() используется для обеспечения того, что количество секунд дополняется до двух цифр, если оно меньше 10.
Создание содержимого страницы
Функция buildContents() вызывается начальным кодом начального кода для выбора и вставки в документ статей и объявлений, которые будут представлены:
const loremIpsum =
"<p>Lorem ipsum dolor sit amet, consectetur adipiscing" +
" elit. Cras at sem diam. Vestibulum venenatis massa in tincidunt" +
" egestas. Morbi eu lorem vel est sodales auctor hendrerit placerat" +
" risus. Etiam rutrum faucibus sem, vitae mattis ipsum ullamcorper" +
" eu. Donec nec imperdiet nibh, nec vehicula libero. Phasellus vel" +
" malesuada nulla. Aliquam sed magna aliquam, vestibulum nisi at," +
" cursus nunc.</p>";
function buildContents() {
for (let i = 0; i < 5; i++) {
contentBox.appendChild(createArticle(loremIpsum));
if (!(i % 2)) {
loadRandomAd();
}
}
}
Переменная loremIpsum содержит текст, который мы будем использовать для тела всех наших статей. Очевидно, в реальном мире у вас был бы код для извлечения статей из базы данных или аналогичных источников, но это выполняет задачу в наших целях. Каждая статья использует одинаковый текст; конечно, вы можете легко это изменить.
buildContents() создаёт страницу с пятью статьями. После каждой нечётной статьи загружается и вставляется объявление. Статьи вставляются в область содержимого (то есть элемент <main>, который содержит всё содержимое сайта) после их создания с помощью метода createArticle(), который мы рассмотрим далее.
Объявления создаются с помощью функции loadRandomAd(), которая создает объявление и вставляет его на страницу. Позже мы увидим, что эта же функция также может заменить существующее объявление, но сейчас мы добавляем объявления в существующее содержимое.
Создание статьи
Для создания элемента <article> статьи (а также всего её содержимого) мы используем функцию createArticle(), которая принимает в качестве входных данных строку, содержащую полный текст статьи, которую нужно добавить на страницу.
function createArticle(contents) {
const articleElem = document.createElement("article");
articleElem.id = nextArticleID;
const titleElem = document.createElement("h2");
titleElem.innerText = `Article ${nextArticleID} title`;
articleElem.appendChild(titleElem);
articleElem.innerHTML += contents;
nextArticleID += 1;
return articleElem;
}
Сначала создаётся элемент <article>, и его идентификатор устанавливается в уникальное значение nextArticleID (которое начинается с 1 и увеличивается для каждой статьи). Затем мы создаём и добавляем элемент h2 для заголовка статьи, а затем добавляем HTML из contents в него. Наконец, nextArticleID увеличивается (чтобы следующий элемент получил новый уникальный идентификатор), и мы возвращаем новый элемент <article> вызывающей стороне.
Создание объявления
Функция loadRandomAd() имитирует загрузку объявления и добавление его на страницу. Если вы не передаёте значение для replaceBox, создаётся новый элемент для размещения объявления; затем объявление добавляется на страницу. Если вы указываете replaceBox, эта область обрабатывается как существующий элемент объявления; вместо создания нового, существующий элемент изменяется для размещения нового стиля, содержимого и других данных объявления. Это предотвращает длительные операции по макетированию при обновлении объявления, которые могли бы произойти, если бы вы сначала удалили старый элемент, а затем вставили новый.
function loadRandomAd(replaceBox) {
const ads = [
{
bgcolor: "#cec",
title: "Eat Green Beans",
body: "Make your mother proud—they're good for you!",
},
{
bgcolor: "aquamarine",
title: "MillionsOfFreeBooks.whatever",
body: "Read classic literature online free!",
},
{
bgcolor: "lightgrey",
title: "3.14 Shades of Gray: A novel",
body: "Love really does make the world go round…",
},
{
bgcolor: "#fee",
title: "Flexbox Florist",
body: "When life's layout gets complicated, send flowers.",
},
];
let adBox, title, body, timerElem;
const ad = ads[Math.floor(Math.random() * ads.length)];
if (replaceBox) {
adObserver.unobserve(replaceBox);
adBox = replaceBox;
title = replaceBox.querySelector(".title");
body = replaceBox.querySelector(".body");
timerElem = replaceBox.querySelector(".timer");
} else {
adBox = document.createElement("div");
adBox.className = "ad";
title = document.createElement("h2");
body = document.createElement("p");
timerElem = document.createElement("div");
adBox.appendChild(title);
adBox.appendChild(body);
adBox.appendChild(timerElem);
}
adBox.style.backgroundColor = ad.bgcolor;
title.className = "title";
body.className = "body";
title.innerText = ad.title;
body.innerHTML = ad.body;
adBox.dataset.totalViewTime = 0;
adBox.dataset.lastViewStarted = 0;
timerElem.className = "timer";
timerElem.innerText = "0:00";
if (!replaceBox) {
contentBox.appendChild(adBox);
}
adObserver.observe(adBox);
}
Сначала — массив ads. Этот массив содержит данные, необходимые для создания каждого объявления. У нас есть четыре варианта для случайного выбора. В реальном сценарии, конечно, объявления будут поступать из базы данных или, скорее, из рекламной службы, из которой вы получаете объявления с помощью API. Однако наши потребности просты: каждое объявление представляется объектом с тремя свойствами: цветом фона (bgcolor), заголовком (title) и строкой текста тела (body).
Затем мы определяем несколько переменных:
adBox-
Это будет элемент, представляющий объявление. Для новых объявлений, которые добавляются на страницу, это создается с помощью
Document.createElement(). При замене существующего объявления это устанавливается в указанный элемент объявления (replaceBox). title-
Будет содержать элемент h2, представляющий заголовок объявления.
body-
Будет содержать элемент
<p>, представляющий текст тела объявления. timerElem-
Будет содержать элемент
<div>, который содержит время, в течение которого объявление было видимым.
Случайное объявление выбирается путем вычисления Math.floor(Math.random() * ads.length); результат — значение от 0 до на единицу меньше, чем количество объявлений. Соответствующее объявление теперь известно как adBox.
Если значение указано для replaceBox, мы используем его в качестве элемента объявления. Для этого мы начинаем с завершения наблюдения за элементом, вызвав IntersectionObserver.unobserve(). Затем локальные переменные для каждого из элементов, составляющих объявление (сам блок объявления, заголовок, тело и блок таймера), устанавливаются в соответствующие элементы в существующем объявлении.
Если для replaceBox не указано значение, создаётся новый элемент объявления. Создаётся новый элемент <div> объявления, и его свойства устанавливаются путем задания имени класса "ad". Затем создаются элемент заголовка объявления, тело и таймер видимости; это элементы h2, <p> и <div> соответственно. Эти элементы добавляются к элементу adBox.
После этого пути кода сходятся снова. Цвет фона объявления устанавливается в значение, указанное в записи нового объявления, а классы и содержимое элементов также устанавливаются соответствующим образом.
Затем пора настроить пользовательские свойства данных для отслеживания данных видимости объявления, установив adBox.dataset.totalViewTime и adBox.dataset.lastViewStarted в 0.
Наконец, мы устанавливаем идентификатор <div> для отображения таймера, который мы будем отображать в объявлении, чтобы показать, сколько времени оно было видимым, задав ему класс "timer". Начальный текст устанавливается на "0:00", чтобы отразить начальное время 0 минут и 0 секунд, и он добавляется к объявлению.
Если мы не заменяем существующее объявление, нам нужно добавить элемент в область содержимого страницы с помощью Document.appendChild(). Если мы заменяем объявление, оно уже там, а его содержимое заменено на содержимое нового объявления. Затем мы вызываем метод observe() для нашего наблюдателя пересечений adObserver, чтобы начать отслеживать объявление на предмет изменений в его пересечении с областью просмотра. С этого момента, каждый раз, когда объявление полностью скрывается или становится видимым хотя бы на один пиксель, или объявление проходит через видимость в 75%, выполняется обратный вызов наблюдателя обработчик событий пересечения.
Замена существующего объявления
Наш обработчик событий пересечения следит за объявлениями, которые полностью скрываются и имеют общее время видимости не менее одной минуты. Когда это происходит, вызывается функция replaceAd() с элементом этого объявления в качестве входного параметра, чтобы старое объявление можно было заменить новым.
function replaceAd(adBox) {
updateAdTimer(adBox);
const visibleTime = adBox.dataset.totalViewTime;
console.log(
`Replacing ad: ${
adBox.querySelector("h2").innerText
} - visible for ${visibleTime}`,
);
loadRandomAd(adBox);
}
replaceAd() начинается с вызова updateAdTimer() для существующего объявления, чтобы убедиться, что его таймер обновлён. Это гарантирует, что когда мы читаем его totalViewTime, мы видим точное конечное значение того, сколько времени объявление было видно пользователю. Затем мы передаём эти данные; в этом случае — выведением их в консоль, но в реальном мире вы бы отправили информацию в API рекламной службы или сохранили бы её в базе данных.
Затем мы загружаем новое объявление, вызвав loadRandomAd(), указав заменяемое объявление в качестве входного параметра. Как мы видели ранее, loadRandomAd() заменит существующее объявление содержимым и данными, соответствующими новому объявлению, если вы укажете элемент существующего объявления в качестве входного параметра.
Возвращается объект элемента нового объявления, если он нужен.
Результат
Полученная страница выглядит так. Попробуйте поэкспериментировать, прокручивая вверх и вниз, и обратите внимание, как изменения видимости влияют на таймеры в каждом объявлении. Также обратите внимание, что каждое объявление заменяется после одной минуты видимости (но объявление сначала должно быть скрыто из вида и снова показано), и как таймеры приостанавливаются, когда документ переключается на фоновый режим. Однако наложение другого окна на браузер не приостанавливает таймеры.
См. также
© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API/Timing_element_visibility