aboutsummaryrefslogtreecommitdiff
path: root/files/ru/web/api/intersection_observer_api/timing_element_visibility/index.html
blob: c4cab59903543926d3fab3c8d473307d6e0ede7a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
---
title: Синхронизация видимости элемента с Intersection Observer API
slug: Web/API/Intersection_Observer_API/Timing_element_visibility
tags:
  - API
  - Example
  - Intermediate
  - Intersection Observer
  - Intersection Observer API
  - Гайды
translation_of: Web/API/Intersection_Observer_API/Timing_element_visibility
---
<div>{{DefaultAPISidebar("Intersection Observer API")}}</div>

<p><a href="/en-US/docs/Web/API/Intersection_Observer_API">Intersection Observer API</a> позволяет в асинхронном режиме уведомлять приложение о том, что какой-то интересующий нас элемент в той или иной степени перекрыл родительский или другой элемент, в том числе {{domxref("Document")}}. <span class="seoSummary">В этой статье мы построим пример блога, в котором в DOM динамически встраиваются рекламные блоки. Затем, с помощью Intersection Observer API, мы выясним, сколько времени показывается каждая отдельная реклама пользователю. Когда такая реклама показывается дольше, чем одну минуту, мы заменяем её на новую.</span></p>

<p>Хотя многие элементы в нашем примере слабо связаны с реальным миром, этого будет достаточно для понимания API. В реальном мире статьи чаще всего отличаются между собой и хранятся не в клиенте, а загружаются из базы данных; да и реклама не состоит из одной только строчки текста.</p>

<p>Есть важная причина, почему мы используем отслеживание видимости рекламы. Вышло так, что наиболее частое употребление Flash или скриптов в Web рекламе нужно для того, чтобы оценивать эффективность рекламы, а значит, её стоимость. Без Intersection Observer API эта задача свелась бы к повсеместному применению setTimeout и setInterval для каждой отдельной рекламы. Такие техники могут драматически ухудшить производительность страницы. Использование API в этом случае может позволит браузеру взять на себя обработку сложной логики и не только ускорит приложение, но и спасёт вас от ошибок, которые обязательно появятся при использовании setTimeout / setInterval.</p>

<p>Начнём!</p>

<div id="fullpage_example">
<h2 id="Структура_приложения_HTML">Структура приложения: HTML</h2>

<p>Структура Web-приложений не очень сложна. Мы будем использовать <a href="/en-US/docs/Web/CSS/CSS_Grid_Layout">CSS Grid</a> для стилизации и макетирования, так что всё достаточно очевидно:</p>

<pre class="brush: html">&lt;div class="wrapper"&gt;
  &lt;header&gt;
    &lt;h1&gt;A Fake Blog&lt;/h1&gt;
    &lt;h2&gt;Showing Intersection Observer in action!&lt;/h2&gt;
  &lt;/header&gt;

  &lt;aside&gt;
    &lt;nav&gt;
      &lt;ul&gt;
        &lt;li&gt;&lt;a href="#link1"&gt;A link&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href="#link2"&gt;Another link&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href="#link3"&gt;One more link&lt;/a&gt;&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/nav&gt;
  &lt;/aside&gt;

  &lt;main&gt;
  &lt;/main&gt;
&lt;/div&gt;</pre>

<p>Это заготовка для приложения. В верхней части приложения находится блок {{HTMLElement("header")}}. Ниже - боковая панель {{HTMLElement("aside")}}, заполненная ссылками. В самом конце структуры - основное тело приложения. Приложение стартует с пустым элементом {{HTMLElement("main")}} -  он будет заполнен позже с помощью JavaScript.</p>

<h2 id="Стилизация_приложения_с_помощью_CSS">Стилизация приложения с помощью CSS</h2>

<p>После определения структуры приложения мы переходим к стилизации. Давайте рассмотрим каждый компонент по отдельности.</p>

<h3 id="Основа">Основа</h3>

<p>Мы создаём стили для {{HTMLElement("body")}} и {{HTMLElement("main")}} так, чтобы определить фоновый цвет и сеточную систему.</p>

<pre class="brush: css">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;
}</pre>

<p>Элемент приложения {{HTMLElement("body")}} сконфигурирован так, чтобы использовать общеупотребимый шрифт из семейства sans-serif и цвет <code>"aliceblue"</code> в качестве фона. Класс <code>"wrapper"</code> оборачивает всё приложение, включая header, sidebar и body content.</p>

<p>Обёртка определяет также CSS Grid сетку, как сетку из двух колонок и двух строк. Первая колонка (размер вычисляется автоматически на основе контента) используется для боковой панели, а вторая колонка (будет использоваться для основного содержимого) имеет ширину, достаточную, чтобы вместить содержимое и занять всю доступную площадь.</p>

<p>Первая строка будет использована для заголовка сайта. Размеры строк определяются схожим образом - размер первой вычисляется на основе контента, а второй - занимает всё доступной пространство, но не меньше размера, необходимого для показа содержимого.</p>

<p>Ширина обёртки зафиксирована - 700px, так что её размер будет удобен для представления приложения в MDN.</p>

<p>The wrapper's width is fixed at 700px so that it will fit in the available space when presented inline on MDN below.</p>

<h3 id="Заголовок">Заголовок</h3>

<p>Заголовок достаточно прост, так как в нашем примере он содержит небольшой текст.</p>

<pre class="brush: css">header {
  grid-column: 1 / -1;
  grid-row: 1;
  background-color: aliceblue;
}</pre>

<p>Значение {{cssxref("grid-row")}} равно 1, так как мы хотим поместить заголовок в верхнюю строку сеточной системы. Более интересно использование {{cssxref("grid-column")}}; Мы указываем здесь, что блок занимает пространство с первой колонки до первой с конца (то есть последней).</p>

<h3 id="Боковая_панель">Боковая панель</h3>

<p>Боковая панель используется для показа ссылок на другие страницы приложения. Ни одна из них не будет работать в нашем примере. Вся боковая панель нужна, чтобы просто приблизить внешний вид приложения к реальному блогу. Боковая панель создаётся с помощью элемента {{HTMLElement("aside")}}.</p>

<pre class="brush: css">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;
}</pre>

<p>Важно отметить, что значение {{cssxref("grid-column")}} здесь установлено в 1 для того, чтобы поместить панель в левую часть экрана. Если вы поменяете это значение на "-1", то панель переместится вправо, однако, в этом случае, вам понадобится немного изменить стили внутренних элементов. Значение {{cssxref("grid-row")}} равно 2, чтобы боковая панель заняла область вдоль области контента.</p>

<h3 id="Область_контента">Область контента</h3>

<p>Контент будет содержаться в элементе {{HTMLElement("main")}}.</p>

<pre class="brush: css">main {
  grid-column: 2;
  grid-row: 2;
  margin: 0;
  margin-left: 16px;
  font-size: 16px;
}</pre>

<p>Главная особенность здесь - контент занимает вторую колонку и вторую строку.</p>

<h3 id="Статьи">Статьи</h3>

<p>Каждая статья состоит из элемента {{HTMLElement("article")}}:</p>

<pre class="brush: css">article {
  background-color: white;
  padding: 6px;
}

article:not(:last-child) {
  margin-bottom: 8px;
}

article h2 {
  margin-top: 0;
}</pre>

<p>Эти стили создают область с белым фоном с небольшими отступами как внутри области, так и между областями.</p>

<h3 id="Рекламные_блоки">Рекламные блоки</h3>

<p>Наконец, рекламные блоки. Нужно заметить, что каждый отдельный рекламный блок может изменять свои стили динамически (мы увидим это позже):</p>

<pre class="brush: css">.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: rgba(255, 255, 255, 0.5);
}</pre>

<p>Здесь нет никакой магии. Простой CSS.</p>

<h2 id="Совмещаем_с_JavaScript">Совмещаем с JavaScript</h2>

<p>Перейдём к JavaScript-коду, который всё оживит. Начнём с глобальных переменных:</p>

<pre class="brush: js">let contentBox;

let nextArticleID = 1;
let visibleAds = new Set();
let previouslyVisibleAds = null;

let adObserver;
let refreshIntervalID = 0;</pre>

<p>Вот что здесь используется:</p>

<dl>
 <dt><code>contentBox</code></dt>
 <dd>Ссылка на элемент {{HTMLElement("main")}}. Это место, куда мы будем вставлять статьи и рекламу.</dd>
 <dt><code>nextArticleID</code></dt>
 <dd>Каждая статья получает уникальный цифровой ID. Эта переменная позволяет понять, какой следующий ID использовать.</dd>
 <dt><code>visibleAds</code></dt>
 <dd>{{jsxref("Set")}} используется для отслеживания текущих видимых на экране рекламных блоков.</dd>
 <dt><code>previouslyVisibleAds</code></dt>
 <dd>Используется для временного хранения списка рекламных блоков в то время, как документ невидим (например, если пользователь переключился на другой таб)</dd>
 <dt><code>adObserver</code></dt>
 <dd>Содержит экземпляр {{domxref("IntersectionObserver")}}, используемый для вычисления наложения рекламных блоков и границ элемента <code>&lt;main&gt;</code>.</dd>
 <dt><code>refreshIntervalID</code></dt>
 <dd>Переменная для хранения ID интервала, который возвращается функцией {{domxref("WindowOrWorkerGlobalScope.setInterval", "setInterval()")}}. Этот интервал будет использоваться для запуска периодических обновлений рекламных блоков.</dd>
</dl>

<h3 id="Установка">Установка</h3>

<p>Для первичного запуска приложения мы вызовем функцию <code>startup()</code>:</p>

<pre class="brush: js">window.addEventListener("load", startup, false);

function startup() {
  contentBox = document.querySelector("main");

  document.addEventListener("visibilitychange", handleVisibilityChange, false);

  let observerOptions = {
    root: null,
    rootMargin: "0px",
    threshold: [0.0, 0.75]
  };

  adObserver = new IntersectionObserver(intersectionCallback,
                    observerOptions);

  buildContents();
  refreshIntervalID = window.setInterval(handleRefreshInterval, 1000);
}</pre>

<p>Вначале мы получаем элемент {{HTMLElement("main")}}, в который можем вставлять содержимое. Затем мы устанавливаем обработчик на событие {{event("visibilitychange")}}. Это событие срабатывает, когда документ меняет состояние между видим/невидим, например, когда пользователь переключается между табами. Intersection Observer API не должен засчитывать пересечение с элементом Main, если пользователь не будет в это время смотреть на вкладку. Таким образом, мы должны останавливать наши таймеры каждый раз, когда пользователь уходит со страницы. С помощью этого обработчика.</p>

<p>Затем мы устанавливаем параметры для {{domxref("IntersectionObserver")}}. Параметры определяют, что IntersectionObserver должен отслеживать перекрытия с областью видимости документа (параметр <code>root</code> в значении <code>null</code>). У нас нет отступов для модификации корневой области; мы хотим отслеживать совпадение границ элементов и видимого документа именно для целей обнаружения перекрытий.</p>

<p>Параметр "порог" (<code>threshold</code>) содержит массив со значениями 0.0 и 0.75; Это заставит обработчик вызываться каждый раз, когда целевой элемент становится полностью обёрнут или только начинает выходить из зоны перекрытия (коэффициент перекрытия 0.0) или проходит порог в 75% видимости в обоих направлениях (коэффициент перекрытия 0.75).</p>

<p>Наблюдатель <code>adObserver</code> создаётся с помощью конструктора <code>IntersectionObserver</code>. В аргументы конструктора мы передаём колбэк-функцию (<code>intersectionCallback</code>) и ранее определённый объект параметров.</p>

<p>После этого мы вызываем функцию <code>buildContents()</code>. Её мы напишем чуть позже. Функция генерирует и вставляет в контейнер статьи и рекламные блоки.</p>

<p>В конце мы устанавливаем интервал, который каждую секунду запускает проверку - нужно ли что-то обновить. Нам необходимо второе обновление, так как в каждом отдельном рекламном блоке мы показываем таймер. В реальном приложении это не понадобится.</p>

<h3 id="Обработка_изменения_видимости_документа">Обработка изменения видимости документа</h3>

<p>Давайте рассмотрим обработчик события {{event("visibilitychange")}}. Это событие срабатывает, когда документ становится видим или невидим. Как правило, это случается, когда пользователь переключается между табами. Так как Intersection Observer отслеживает только перекрытия элемента с корневым элементом, нам необходимо отдельно позаботиться о детекции видимости документа. Для этого мы используем <a href="/en-US/docs/Web/API/Page_Visibility_API">Page Visibility API</a>.</p>

<pre class="brush: js">function handleVisibilityChange() {
  if (document.hidden) {
    if (!previouslyVisibleAds) {
      previouslyVisibleAds = visibleAds;
      visibleAds = [];
      previouslyVisibleAds.forEach(function(adBox) {
        updateAdTimer(adBox);
        adBox.dataset.lastViewStarted = 0;
      });
    }
  } else {
    previouslyVisibleAds.forEach(function(adBox) {
      adBox.dataset.lastViewStarted = performance.now();
    });
    visibleAds = previouslyVisibleAds;
    previouslyVisibleAds = null;
  }
}</pre>

<p>Так как событие само по себе не указывает, стал ли документ видимым или, наоборот, невидимым, мы должны вручную проверить свойство {{domxref("document.hidden")}}. В теории, это событие может сработать несколько раз, поэтому нам нужно обрабатывать только те рекламные блоки, учёт которых ещё не был приостановлен.</p>

<p>Для остановки таймеров нам нужно удалить ссылки на рекламные блоки из коллекции <code>visibleAds</code> и пометить их как неактивные. Чтобы это сделать, мы начинаем с сохранения ссылок на текущие видимые элементы в переменную<code> previouslyVisibleAds</code>. Это нужно, чтобы в дальнейшем можно было восстановить счётчики для этих блоков. Так мы указываем приложению, что эту рекламу не надо считать активной. Затем, если пользователь вернулся в документ, мы вызываем функцию  <code>updateAdTimer()</code> для каждого отложенного элемента. Эта функция обновляет общее время видимости элемента. После этого мы присваиваем переменной <code>dataset.lastViewStarted</code> значение 0, что означает, что таймер не запущен.</p>

<p>Если документ стал видимым, мы выполняем обратный процесс: сначала мы проходим через коллекцию <code>previouslyVisibleAds</code>. Для каждого элемента мы присваиваем  <code>dataset.lastViewStarted</code> значение, соответствующее текущему времени документа (в миллисекундах с момента создания документа). Это время можно узнать с помощью  метода {{domxref("Performance.now", "performance.now()")}}. Затем мы присваиваем переменной  <code>visibleAds</code> закешированное ранее значение <code>previouslyVisibleAds</code>, с обнулением последней переменной. Теперь рекламные блоки перезапущены и настроены, так что время простоя не будет учитываться.</p>

<h3 id="Обработчик_изменений_наложения">Обработчик изменений наложения</h3>

<p>При каждой итерации в браузерном event loop, каждый наблюдатель  {{domxref("IntersectionObserver")}} проверяет, не прошёл ли какой-либо из элементов-целей через пороговые значения наблюдателя.  Для каждого наблюдателя список таких целей собирается в один список и отправляется в колбэк-функцию наблюдателя. Каждый элемент списка - это {{domxref("IntersectionObserverEntry")}} объект. В нашем приложении <code>intersectionCallback()</code> выглядит так:</p>

<pre class="brush: js">function intersectionCallback(entries) {
  entries.forEach(function(entry) {
    let adBox = entry.target;

    if (entry.isIntersecting) {
      if (entry.intersectionRatio &gt;= 0.75) {
        adBox.dataset.lastViewStarted = entry.time;
        visibleAds.add(adBox);
      }
    } else {
      visibleAds.delete(adBox);
      if ((entry.intersectionRatio === 0.0) &amp;&amp; (adBox.dataset.totalViewTime &gt;= 60000)) {
        replaceAd(adBox);
      }
    }
  });
}</pre>

<p>Как мы упоминали ранее, колбэк-функция {{domxref("IntersectionObserver")}}  получает на вход массив элементов, которые активировали наблюдателя. В нашей функции мы итерируемся по этому массиву. Если элемент пересекается с корневым элементом, мы знаем, что он стал видимым. Если он становится видимым более, чем на 75%, мы считаем, что реклама видима и мы запускаем таймер, выставляя значение  <code>dataset.lastViewStarted</code> равным времени изменения параметра перекрытия {{domxref("IntersectionObserverEntry.time", "entry.time")}}. Затем мы добавляем рекламный блок в набор <code>visibleAds</code>.</p>

<p>Если рекламный блок уходит из зоны видимости, мы удаляем его из набор видимых элементов. Затем, в зависимости от значения {{domxref("IntersectionObserverEntry.intersectionRatio", "entry.ratio")}}, мы либо меняем рекламу, либо ставим на паузу. Так, если значение равно 0.0 и реклама уже была видна минимум минуту, мы вызываем функцию <code>replaceAd()</code> . В этом случае пользователь видит разные рекламные блоки, но сама реклама меняется незаметно для пользователя.</p>

<h3 id="Обработка_периодический_событий">Обработка периодический событий</h3>

<p>Каждую секунду у нас срабатывает интервал <code>handleRefreshInterval()</code>, который мы задали в функции <code>startup()</code>. Главная задача этого интервала - обновлять таймеры каждую секунду и перерисовывать значение таймеров.</p>

<pre class="brush: js">function handleRefreshInterval() {
  let redrawList = [];

  visibleAds.forEach(function(adBox) {
    let previousTime = adBox.dataset.totalViewTime;
    updateAdTimer(adBox);

    if (previousTime != adBox.dataset.totalViewTime) {
      redrawList.push(adBox);
    }
  });

  if (redrawList.length) {
    window.requestAnimationFrame(function(time) {
      redrawList.forEach(function(adBox) {
        drawAdTimer(adBox);
      });
    });
  }
}</pre>

<p>Массив <code>redrawList</code> используется для хранения списка рекламных блоков, которые должны быть перерисованы в следующем цикле перерисовки. Это нужно, так как таймеры текущих рекламных блоков не всегда совпадают с реальными таймингами из-за прочих системных процессов. Или из-за того, что вы указали в качестве интервала промежуток не в 1000мс.</p>

<p>Затем, для каждого видимого рекламного блока, мы сохраняем значение <code>dataset.totalViewTime</code> (количество миллисекунд, которое текущая реклама была видима с момента последнего обновления этого значения). После этого вызываем функцию <code>updateAdTimer()</code> для обновления времени. Если оно изменилось, мы вставляем рекламный блок в список <code>redrawList</code>. Таким образом, при обработке следующего кадра приложение знает, что нужно перерисовать.</p>

<p>И, наконец, если существует хоть один элемент, который нужно перерисовать, мы будем используем {{domxref("window.requestAnimationFrame", "requestAnimationFrame()")}}, чтобы отложить отрисовку каждого элемента на тот момент, когда будет формироваться следующий кадр.</p>

<h3 id="Обновление_таймера_видимости_рекламы">Обновление таймера видимости рекламы</h3>

<p>Ранее мы уже видели, что если нам нужно обновить общее время видимости рекламы - мы вызываем функцию <code>updateAdTimer()</code>. Эта функция принимает в качестве аргумента объект {{domxref("HTMLDivElement")}}.</p>

<pre class="brush: js">function updateAdTimer(adBox) {
  let lastStarted = adBox.dataset.lastViewStarted;
  let currentTime = performance.now();

  if (lastStarted) {
    let diff = currentTime - lastStarted;

    adBox.dataset.totalViewTime = parseFloat(adBox.dataset.totalViewTime) + diff;
  }

  adBox.dataset.lastViewStarted = currentTime;
}</pre>

<p>Для отслеживания времени видимости элемента мы используем два data-атрибута на каждом рекламном блоке:</p>

<dl>
 <dt><code>lastViewStarted</code></dt>
 <dd>Время в миллисекундах относительно первоначальной загрузки страницы до момента, когда счётчик рекламного блока был обновлён или блок стал невидим. Если значение равно нулю - блок не был видим в последний раз, когда проверялся.</dd>
 <dt><code>totalViewTime</code></dt>
 <dd>Общее время видимости рекламного блока.</dd>
</dl>

<p>Значение этих атрибутов можно получить с помощью {{domxref("HTMLElement.dataset")}}. Значения - строки, но вы можете конвертировать их в числа. Фактически, JavaScript делает это автоматически, но нам всё равно придётся в одном месте сделать это вручную.</p>

<p>Функция начинается с выяснения времени, когда происходила последняя проверка видимости рекламы (<code>adBox.dataset.lastViewStarted</code>). Мы также получаем текущее время с момента создания документа с помощью {{domxref("Performance.now", "performance.now()")}} <code>currentTime</code>.</p>

<p>Если время последней проверки lastStarted не равно нулю - это значит, что таймер сейчас уже запущен. В этом случае мы вычисляем разницу между текущим временем и временем старта проверки. Это значение покажет, сколько реклама была видима с момента последнего старта детекции. Затем это значение прибавляем к уже имеющемуся <code>totalViewTime</code>. Обратите внимание не вызов {{jsxref("parseFloat()")}}: так как все значения из Dataset - строки, JavaScript пытается соединить строки вместо того, чтобы просуммировать числа.</p>

<p>В конце мы присваиваем <code>lastViewStarted</code> текущее значение. Это делается вне зависимости от того, был ли элемент видим во время вызова функции или нет - это позволяет таймеру рекламных блоков срабатывать всегда, когда эта функция вызывается. Это имеет смысл, потому что вызов может сработать ровно в тот момент, когда реклама только появилась.</p>

<h3 id="Показываем_таймер_рекламы">Показываем таймер рекламы</h3>

<p>Внутри каждого рекламного блока мы отображаем текущее значение общего времени видимости в формате мин:сек. Для этого мы передаём в функцию <code>drawAdTimer</code> контейнер:</p>

<pre class="brush: js">function drawAdTimer(adBox) {
  let timerBox = adBox.querySelector(".timer");
  let totalSeconds = adBox.dataset.totalViewTime / 1000;
  let sec = Math.floor(totalSeconds % 60);
  let min = Math.floor(totalSeconds / 60);

  timerBox.innerText = min + ":" + sec.toString().padStart(2, "0");
}</pre>

<p>Функция находит внутри переданного контейнера блок с классом <code>timer</code>. Затем забирает данные о текущем общем времени видимости блока. С помощью деления на 1000, 60 и 60 мы преобразуем результат в нужный формат (миллисекунды -&gt; секунды -&gt; минуты / секунды)</p>

<p>Метод {{jsxref("String.padStart()")}} используется для того, чтобы убедиться, что число секунд всегда состоят из двух цифр.</p>

<h3 id="Строим_содержимое_страницы">Строим содержимое страницы</h3>

<p>Функция <code>buildContents()</code> вызывается при старте приложения. Она формирует тело статьи и добавляет рекламные блоки:</p>

<pre class="brush: js">let loremIpsum = "&lt;p&gt;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.&lt;/p&gt;";

function buildContents() {
  for (let i=0; i&lt;5; i++) {
    contentBox.appendChild(createArticle(loremIpsum));

    if (!(i % 2)) {
      loadRandomAd();
    }
  }
}
</pre>

<p>Переменная <code>loremIpsum</code> содержит текст, который мы используем как тело статьи. Разумеется, в реальном мире вы будете забирать статьи из какой-то базы данных. Но это тема другой статьи, поэтому мы пошли простым путём.</p>

<p><code>buildContents()</code> создаёт страницу с пятью статьями. Каждая нечётная статья содержит рекламные блоки.  Статьи будут вставлены в блок контента {{HTMLElement("main")}}. после того, как будет вызван метод <code>createArticle()</code>, который мы разберём позже.</p>

<p>Рекламные блоки создаются с помощью функции <code>loadRandomAd()</code>. Эта функция создаёт и вставляет блоки одновременно. Как мы увидим позже, эта же функция может и заменить уже существующую рекламу. Но пока что просто добавим рекламу в существующий текст.</p>

<h3 id="Создаём_статью">Создаём статью</h3>

<p>Для создания элемента статьи {{HTMLElement("article")}} и её содержимого мы используем функцию <code>createArticle()</code>, которая в качестве входных данных принимает строку-текст статьи.</p>

<pre class="brush: js">function createArticle(contents) {
  let articleElem = document.createElement("article");
  articleElem.id = nextArticleID;

  let titleElem = document.createElement("h2");
  titleElem.id = nextArticleID;
  titleElem.innerText = "Article " + nextArticleID + " title";
  articleElem.appendChild(titleElem);

  articleElem.innerHTML += contents;
  nextArticleID +=1 ;

  return articleElem;
}</pre>

<p>Сперва, элемент <code>&lt;article&gt;</code> создаётся и ему присваивается уникальный ID <code>nextArticleID</code> (это просто счётчик от нуля до бесконечности). Затем мы создаём и добавляем элемент {{HTMLElement("h2")}} для заголовка и применяем HTML из переменной <code>contents</code>. Наконец, мы увеличиваем значение <code>nextArticleID</code> (таким образом, следующий элемент получит уникальный ID) и возвращаем элемент статьи обратно.</p>

<h3 id="Создание_рекламного_блока">Создание рекламного блока</h3>

<p>Функция <code>loadRandomAd()</code> имитирует загрузку рекламы и её добавление на страницу. Если вы не указываете значение для <code>replaceBox</code>, создаётся и применяется новый контейнер для рекламы. Если вы указали <code>replaceBox</code>, этот контейнер рассматривается, как уже существующий элемент. Вместо создания нового, существующий элемент изменяется, чтобы содержать актуальные данные. Это помогает избежать риска неэффективной перерисовки элементов, если вы сначала будете удалять элемент из DOM, а затем вставлять новый.</p>

<pre class="brush: js">function loadRandomAd(replaceBox) {
  let 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;

  let 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);
}</pre>

<p>Вначале мы определяем массив <code>ads</code>. Этот массив содержит данные, необходимые для создания рекламных блоков. В реальном приложении, конечно, мы будем загружать эти данные из базы или, что более вероятно, из рекламного сервиса, который будет использовать какой-то API. Тем не менее, наша простая задача решается: каждый рекламный блок представлен тремя свойствами: фоновым цветом (<code>bgcolor</code>), заголовком (<code>title</code>) и текстовым содержимым (<code>body</code>).</p>

<p>Затем мы определяем несколько переменных:</p>

<dl>
 <dt><code>adBox</code></dt>
 <dd>Определяет контейнер, содержащий рекламу. Вновь добавленные рекламные блоки будут добавлены к странице с помощью{{domxref("Document.createElement()")}}. Когда замещается существующая реклама, в этой переменной указан элемент (<code>replaceBox</code>).</dd>
 <dt><code>title</code></dt>
 <dd>Содержит ссылку на элемент {{HTMLElement("h2")}}.</dd>
 <dt><code>body</code></dt>
 <dd>Содержит ссылку на элемент {{HTMLElement("p")}}.</dd>
 <dt><code>timerElem</code></dt>
 <dd>Содержит ссылку на элемент таймера {{HTMLElement("div")}}.</dd>
</dl>

<p>Случайный рекламный блок вычисляется с помощью <code>Math.floor(Math.random() * ads.length)</code>. Результат этой функции - целое число между 0 и максимальным количеством рекламных блоков. Соответствующий рекламный блок теперь доступен нам из переменной <code>adBox</code>.</p>

<p>Если <code>replaceBox</code> содержит какое-то значение, мы рассматриваем его как элемент рекламного блока. Мы завершаем наблюдение за элементом с помощью {{domxref("IntersectionObserver.unobserve()")}}. Затем собираем в локальные переменные данные из каждого свойства элемента: заголовок, тело и таймер.</p>

<p>Если никакое значение не указано для <code>replaceBox</code>, мы создаём новый элемент. Создаётся новый контейнер {{HTMLElement("div")}}. Его CSS-параметры задаются с помощью класса <code>"ad"</code>. Затем создаются заголовок рекламного блока, его текст и таймер.  Соотстветвенно, это {{HTMLElement("h2")}}, {{HTMLElement("p")}} и {{HTMLElement("div")}}. Эти элементы применяются к контейнеру <code>adBox</code>.</p>

<p>После этого разветвления наш код вновь возвращается к единому. Фоновый цвет рекламных блоков присваивается соответственно записям. Элементам присваиваются классы и содержимое.</p>

<p>Наступаем время присвоить data-параметры, чтобы отслеживать видимость рекламных блоков с помощью установки <code>adBox.dataset.totalViewTime</code> и <code>adBox.dataset.lastViewStarted</code> равными нулю.</p>

<p>Наконец, мы устанавливаем CSS-класс контейнеру таймера. С помощью этого класса приложение сможет с лёгкостью собирать данные и обновлять их для каждого таймера. По умолчанию, текст этого контейнера - "0:00".</p>

<p>Если мы создаём новую рекламу, мы должны применить элемент к страницы с помощью {{domxref("Node.appendChild", "Document.appendChild()")}}. Если мы лишь заменяем рекламный блок - он уже представлен в DOM и всё, что нам нужно сделать - это обновить его. Затем мы вызываем функцию {{domxref("IntersectionObserver.observe", "observe()")}}. <code>adObserver</code> начинает отслеживать изменения перекрытия элементов в видимой области приложения. С этого момента любой рекламный блок, который становится на 100% скрыт или хотя бы на один пиксель видим или преодолевает порог в 75% видимости в любом направлении, запускает вычисление таймингов и обновление содержимого таймеров.</p>

<h3 id="Замена_существующей_рекламы">Замена существующей рекламы</h3>

<p>Наша {{anch("Handling intersection changes", "функция обработки перекрытия")}} отслеживает рекламные блоки. Когда они становятся на 100% и общее время их видимости достаточное для того, рекламный блок заменяется на новый. Когда это происходит, вызывается функция <code>replaceAd()</code>.</p>

<pre class="brush: js">function replaceAd(adBox) {
  let visibleTime;

  updateAdTimer(adBox);

  visibleTime = adBox.dataset.totalViewTime
  console.log("  Replacing ad: " + adBox.querySelector("h2").innerText + " - visible for " + visibleTime)

  loadRandomAd(adBox);
}</pre>

<p><code>replaceAd()</code> начинается с вызова <code>updateAdTimer()</code> для существующего рекламного блока, чтобы убедиться, что таймер обновлён. С помощью этого вызова мы убеждаемся, что <code>totalViewTime</code>, который мы используем для обработки, действительно совпадает с тем, что видел пользователь. Мы логируем это значение и загружаем в рекламный блок новые данные. Помните, что в реальном мире вы не должны логировать подобные вещи, а скорее использовать API для сбор логов.</p>
</div>

<h2 id="Результат">Результат</h2>

<p>Вы можете увидеть результат в окне ниже. Попробуйте экспериментировать с прокруткой и понаблюдайте за тем, как изменение видимости затрагивает каждый таймер. Кроме того, обратите внимание, что каждый рекламный блок обновляется только в том случае, если он уже был видим в течение минуты.</p>

<p>{{EmbedLiveSample("fullpage_example", 750, 800)}}</p>

<h2 id="Смотрите_также">Смотрите также:</h2>

<ul>
 <li><a href="/en-US/docs/Web/API/Intersection_Observer_API">Intersection Observer API</a></li>
 <li><a href="/en-US/docs/Web/API/Page_Visibility_API">Page Visibility API</a></li>
</ul>