diff options
author | Peter Bengtsson <mail@peterbe.com> | 2020-12-08 14:42:52 -0500 |
---|---|---|
committer | Peter Bengtsson <mail@peterbe.com> | 2020-12-08 14:42:52 -0500 |
commit | 074785cea106179cb3305637055ab0a009ca74f2 (patch) | |
tree | e6ae371cccd642aa2b67f39752a2cdf1fd4eb040 /files/ru/web/api/intersection_observer_api | |
parent | da78a9e329e272dedb2400b79a3bdeebff387d47 (diff) | |
download | translated-content-074785cea106179cb3305637055ab0a009ca74f2.tar.gz translated-content-074785cea106179cb3305637055ab0a009ca74f2.tar.bz2 translated-content-074785cea106179cb3305637055ab0a009ca74f2.zip |
initial commit
Diffstat (limited to 'files/ru/web/api/intersection_observer_api')
-rw-r--r-- | files/ru/web/api/intersection_observer_api/index.html | 134 | ||||
-rw-r--r-- | files/ru/web/api/intersection_observer_api/timing_element_visibility/index.html | 565 |
2 files changed, 699 insertions, 0 deletions
diff --git a/files/ru/web/api/intersection_observer_api/index.html b/files/ru/web/api/intersection_observer_api/index.html new file mode 100644 index 0000000000..40fbfc7443 --- /dev/null +++ b/files/ru/web/api/intersection_observer_api/index.html @@ -0,0 +1,134 @@ +--- +title: Intersection Observer API +slug: Web/API/Intersection_Observer_API +tags: + - Intersection Observer API + - пересечение объектов +translation_of: Web/API/Intersection_Observer_API +--- +<p>{{DefaultAPISidebar("Intersection Observer API")}}</p> + +<p class="summary">Intersection Observer API позволяет веб-приложениям асинхронно следить за изменением пересечения элемента с его родителем или областью видимости документа {{Glossary("viewport")}}.</p> + +<p>Исторически обнаружение видимости отдельного элемента или видимости двух элементов по отношению друг к другу было непростой задачей. Варианты решения этой задачи были ненадежными и замедляли работу браузера. К несчастью, по мере того как веб "взрослел", потребность в решении этой проблемы только росла по многим причинам, таким как:</p> + +<ul> + <li>Отложенная загрузка изображений или другого контента по мере прокрутки страницы.</li> + <li>Реализация веб-сайтов с "бесконечным скроллом", где контент подгружается по мере того как страница прокручивается вниз, и пользователю не нужно переключаться между страницами.</li> + <li>Отчет о видимости рекламы с целью посчитать доходы от нее.</li> + <li>Принятие решения, запускать ли какой-то процесс или анимацию в зависимости от того, увидит пользователь результат или нет.</li> +</ul> + +<p>В прошлом реализация обнаружения пересечения элементов подразумевала использование обработчиков событий и циклов, вызывающих методы типа {{domxref("Element.getBoundingClientRect()")}}, чтобы собрать необходимую информацию о каждом затронутом элементе. Поскольку весь этот код работает в основном потоке, возникают проблемы с производительностью.</p> + +<p>Рассмотрим веб-страницу с бесконечным скроллом. На ней используется библиотека для управления периодически размещаемой по всей странице рекламой, повсюду анимированная графика, а также библиотека для отображения всплывающих окон. И все эти вещи используют свои собственные правила для обнаружения пересечений, и все они запущены в основном потоке. Автор сайта может даже не подозревать об этой проблеме, а также может не знать, как работают сторонние библиотеки изнутри. В то же время пользователь по ходу прокрутки страницы сталкивается с тем, что работа сайта замедляется постоянным срабатыванием обнаружения пересечения, что в итоге приводит к тому, что пользователь недоволен браузером, сайтом и своим компьютером.</p> + +<p>Intersection Observer API даёт возможность зарегистрировать callback-функцию, которая выполнится при пересечении наблюдаемым элементом границ другого элемента (или области видимости документа {{Glossary("viewport")}}), либо при изменении величины пересечения на опредённое значение. Таким образом, больше нет необходимости вычислять пересечение элементов в основном потоке, и браузер может оптимизировать эти процессы на своё усмотрение.</p> + +<p>Observer API не позволит узнать точное число пикселей или определить конкретные пиксели в пересечении; однако, его использование покрывает наиболее частые сценарии вроде "Если элементы пересекаются на N%, сделай то-то".</p> + +<h2 id="Основные_понятия">Основные понятия</h2> + +<p>Intersection Observer API позволяет указать функцию, которая будет вызвана всякий раз для элемента (<strong>target</strong>) при пересечении его с областью видимости документа (по-умолчанию) или заданным элементом (<strong>root</strong>).</p> + +<p>В основном, используется отслеживание пересечения элемента с областью видимости (необходимо указать <code>null</code> в качестве корневого элемента).</p> + +<p>Используете ли вы область видимости или другой элемент в качестве корневого, API работает одинаково, вызывая заданную вами функцию обратного вызова, всякий раз, когда видимость целевого элемента изменяет так, что она пересекает в нужной степени корневой элемент.</p> + +<p>Степень пересечения целевого и корневого элемента задается в диапазоне от 0.0 до 1.0, где 1.0 это полное пересечение целевого элемента границ корневого.</p> + +<h3 id="Пример_использования">Пример использования</h3> + +<p>Для начала с помощью конструктора нужно создать объект-наблюдатель, указать для него функцию для вызова и настройки отслеживания:</p> + +<pre class="brush: js">var options = { + root: document.querySelector('#scrollArea'), + rootMargin: '0px', + threshold: 1.0 +} +var callback = function(entries, observer) { + /* Content excerpted, show below */ +}; +var observer = new IntersectionObserver(callback, options);</pre> + +<p>Параметр threshold со значением 1.0 означает что функция будет вызвана при 100% пересечении объекта (за которым мы следим) с объектом root</p> + +<h3 id="Настройки">Настройки</h3> + +<dl> + <dt>root</dt> + <dd>Элемент который используется в качестве области просмотра для проверки видимости целевого элемента. Должен быть предком целевого элемента. По умолчанию используется область видимости браузера если не определён или имеет значение <font face="consolas, Liberation Mono, courier, monospace"><span style="background-color: rgba(220, 220, 220, 0.498039);">null</span></font>.</dd> + <dt>rootMargin </dt> + <dd>Отступы вокруг root. Могут иметь значения как свойство css margin: "<code>10px 20px 30px 40px" (top, right, bottom, left).</code> Значения можно задавать в процентах. По умолчанию все параметры установлены в нули.</dd> + <dt>threshold</dt> + <dd>Число или массив чисел, указывающий, при каком проценте видимости целевого элемента должен сработать callback. Например, в этом случае callback функция будет вызываться при появлении в зоне видимости каждый 25% целевого элемента: [0, 0.25, 0.5, 0.75, 1]</dd> +</dl> + +<h4 id="Целевой_элемент_который_будет_наблюдаться">Целевой элемент, который будет наблюдаться</h4> + +<p>После того, как вы создали наблюдателя, вам нужно дать ему целевой элемент для просмотра:</p> + +<pre class="brush: js">var target = document.querySelector('#listItem'); +observer.observe(target); +</pre> + +<p>Всякий раз, когда цель достигает порогового значения, указанного для <code>IntersectionObserver</code>, вызывается функция обратного вызова <code>callback</code>. Где <code>callback</code> получает список объектов {{domxref ("IntersectionObserverEntry")}} и наблюдателя:</p> + +<pre class="brush: js">var callback = function(entries, observer) { + entries.forEach(entry => { + entry.time; // a DOMHightResTimeStamp indicating when the intersection occurred. + entry.rootBounds; // a DOMRectReadOnly for the intersection observer's root. + entry.boundingClientRect; // a DOMRectReadOnly for the intersection observer's target. + entry.intersectionRect; // a DOMRectReadOnly for the visible portion of the intersection observer's target. + entry.intersectionRatio; // the number for the ratio of the intersectionRect to the boundingClientRect. + entry.target; // the Element whose intersection with the intersection root changed. + entry.isIntersecting; // intersecting: true or false + }); +}; +</pre> + +<p>Обратите внимание, что функция обратного вызова запускается в главном потоке и должна выполняться как можно быстрее, поэтому если что-то отнимает много времени, то используйте {{domxref("Window.requestIdleCallback()")}}.</p> + +<p>Также обратите внимание, что если вы указали опцию <code>root</code>, целевой элемент должен быть потомком корневого элемента.</p> + +<h2 id="Интерфейсы">Интерфейсы</h2> + +<dl> + <dt>{{domxref("IntersectionObserver")}}</dt> + <dd>Основной интерфейс для API Intersection Observer. Предоставляет методы для создания и управления observer, который может наблюдать любое количество целевых элементов для одной и той же конфигурации пересечения. Каждый observer может асинхронно наблюдать изменения в пересечении между одним или несколькими целевыми элементами и общим элементом-предком или с их верхним уровнем {{domxref("Document")}}'s {{Glossary('viewport')}}. Предок или область просмотра упоминается как <strong>root</strong>.</dd> + <dt>{{domxref("IntersectionObserverEntry")}}</dt> + <dd>Описывает пересечение между целевым элементом и его корневым контейнером в определенный момент перехода. Объекты этого типа могут быть получены только двумя способами: в качестве входных данных для вашего обратного вызова IntersectionObserver или путем вызова {{domxref ("IntersectionObserver.takeRecords()")}}.</dd> +</dl> + +<h2 id="Спецификации">Спецификации</h2> + +<table class="standard-table"> + <thead> + <tr> + <th scope="col">Спецификация</th> + <th scope="col">Статус</th> + <th scope="col">Комментарий</th> + </tr> + </thead> + <tbody> + <tr> + <td>{{SpecName('IntersectionObserver')}}</td> + <td>{{Spec2('IntersectionObserver')}}</td> + <td></td> + </tr> + </tbody> +</table> + +<h2 id="Совместимость_с_браузерами">Совместимость с браузерами</h2> + +<div class="hidden">Таблица совместимости на этой странице генерируется из структурированных данных. Если вы хотите внести свой вклад в эти данные, просмотрите https://github.com/mdn/browser-compat-data и отправьте нам запрос на извлечение.</div> + +<p>{{Compat("api.IntersectionObserver")}}</p> + +<h2 id="Смотрите_также">Смотрите также</h2> + +<ul> + <li><a href="https://github.com/w3c/IntersectionObserver">Intersection Observer polyfill</a></li> + <li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API/Timing_element_visibility">Timing element visibility with the Intersection Observer API</a></li> + <li>{{domxref("IntersectionObserver")}} and {{domxref("IntersectionObserverEntry")}}</li> +</ul> diff --git a/files/ru/web/api/intersection_observer_api/timing_element_visibility/index.html b/files/ru/web/api/intersection_observer_api/timing_element_visibility/index.html new file mode 100644 index 0000000000..ba2d788cd7 --- /dev/null +++ b/files/ru/web/api/intersection_observer_api/timing_element_visibility/index.html @@ -0,0 +1,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 notranslate"><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></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 notranslate">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 notranslate">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 notranslate">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 notranslate">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 notranslate">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 notranslate">.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 notranslate">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><main></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 notranslate">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 notranslate">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 notranslate">function intersectionCallback(entries) { + entries.forEach(function(entry) { + let 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); + } + } + }); +}</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 notranslate">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 notranslate">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 notranslate">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 мы преобразуем результат в нужный формат (миллисекунды -> секунды -> минуты / секунды)</p> + +<p>Метод {{jsxref("String.padStart()")}} используется для того, чтобы убедиться, что число секунд всегда состоят из двух цифр.</p> + +<h3 id="Строим_содержимое_страницы">Строим содержимое страницы</h3> + +<p>Функция <code>buildContents()</code> вызывается при старте приложения. Она формирует тело статьи и добавляет рекламные блоки:</p> + +<pre class="brush: js notranslate">let 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(); + } + } +} +</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 notranslate">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><article></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 notranslate">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 notranslate">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> |