JavaScript | Подгрузка HTML-разметки по доскроллу (прокрутке)

JavaScript | Подгрузка HTML-разметки по доскроллу (прокрутке)

Задача

Нам нужно отрисовывать HTML-разметку в открытом документе в браузере только в тот момент, когда скролл (прокрутка) достигнет определённого HTML-элемента (блока).

По задумке хочется иметь несколько HTML-элементов под одним классом, в которые мы будем подгружать контент в момент долистывания.

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

Как это сделать?

 

Зачем это нужно делать? Где это применяется?

Первый момент

Страница с данными может быть очень большой. И высока вероятность того, что пользователь просто физически не будет просматривать её всю. Но если мы будем отдавать её целиком, то на слабом устройстве такая страница может не загрузиться быстро. Пользователь может получить негативный опыт и вообще покинуть сайт.

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

Второй момент

Также это очень полезно делать если у нас подгружаются рекламные баннеры, которые участвуют в аукционах рекламных сетей.

Нет смысла проводить аукцион, если пользователь не долистает до рекламного блока и не увидит его. Такой блок не будет учтён в доходах площадки. А хуже всего то, что его показатели тоже начнут снижаться и стоимость рекламного места будет падать. Это никому не интересно.

 

Введение

Чтобы не изобретать велосипед, предлагаю сразу ссылаться на правильные источники официальных документаций.

Нас будет интересовать стандарт объектной модели документа DOMhttps://dom.spec.whatwg.org

В этом стандарте описана работа интерфейса Element, который умеет взаимодействовать с HTML-элементами на странице.

Также нас будет интересовать стандарт CSSOM View Modulehttps://www.w3.org/TR/cssom-view-1/

В этом стандарте описана работа расширения для интерфейса Element. Нас будет интересовать метод getBoundingClientRect(). Этот метод наследуется всеми экземплярами класса Element. Это значит, что его можно вызвать на любом HTML-элементе, который мы получим при помощи JavaScript.

За дополнительной информацией будет полезно обратиться к стандарту Geometry Interfaces Module Level 1https://www.w3.org/TR/geometry-1/

 

Тестовый HTML-документ

Давайте создадим HTML-документ для экспериментов. Его содержимое будет примерно таким:

    <style>

        p {

            border: 1px solid black;

            height: 200px;

        }

    </style>

    <h1>Событие подгрузки по доскроллу</h1>

    <p>Какой-то текст № 01</p>

    <p>Какой-то текст № 02</p>

    <p>Какой-то текст № 03</p>

    <p>Какой-то текст № 04</p>

    <p>Какой-то текст № 05</p>

    <p>Какой-то текст № 06</p>

    <p>Какой-то текст № 07</p>

    <p>Какой-то текст № 08</p>

    <p class=«efimrtb» data-efimrtb=«1»>Какой-то HTML-блок</p>

    <p>Какой-то текст № 09</p>

    <p>Какой-то текст № 10</p>

    <p>Какой-то текст № 11</p>

    <p>Какой-то текст № 12</p>

    <p>Какой-то текст № 13</p>

    <p>Какой-то текст № 14</p>

    <p class=«efimrtb» data-efimrtb=«2»>Какой-то HTML-блок</p>

    <p>Какой-то текст № 15</p>

    <p>Какой-то текст № 16</p>

    <p>Какой-то текст № 17</p>

    <p>Какой-то текст № 18</p>

    <p>Какой-то текст № 19</p>

    <p>Какой-то текст № 20</p>

    <p>Какой-то текст № 21</p>

    <p class=«efimrtb» data-efimrtb=«3»>Какой-то HTML-блок</p>

    <p>Какой-то текст № 22</p>

    <p>Какой-то текст № 23</p>

Я сделал высоту в 200 пикселей, чтобы точно появился вертикальный скролл.

За стиль отвечает HTML-элемент style, в котором прописан стиль. Отдельные CSS-файлы не подгружаются.

 

Метод getBoundingClientRect()

Он возвращает объект — экземпляр класса DOMRect

Если наш скролл находится в самом верху, то мы получаем объект примерно такого содержания:

Получили экземпляр класса DOMRect элемента p с class уашькеи - JavaScript
Получили экземпляр класса DOMRect элемента p с class уашькеи — JavaScript

Мы скормили ему первый элемент с class=»efimrtb». Этот элемент НЕ ВИДЕН (НЕ ОТОБРАЖАЕТСЯ) в текущем положении экрана над документом.

Давайте подробнее рассмотрим, что нам вернулось:

{

bottom: 2025.875
height: 202
left: 8
right: 767
top: 1823.875
width: 759
x: 8
y: 1823.875

}

 

Всего получается 8 ключей:

bottom

Атрибут «bottom» при получении должен возвращать max(координата y, координата y + размер высоты).

 

height

Атрибут «height» при получении должен возвращать значение измерения высоты. Для интерфейса DOMRect установка атрибута «height» должна установить новое значение измерения высоты.

 

left

Атрибут «left» при получении должен возвращать min(координата x, координата x + размер ширины).

 

right

Атрибут «right» при получении должен возвращать max(координата x, координата x + размер ширины).

 

top

Атрибут «top» при получении должен возвращать min(координата y, координата y + размер высоты).

 

width

Атрибут «width» при получении должен возвращать значение измерения ширины. Для интерфейса DOMRect установка атрибута «width» должна установить для измерения ширины новое значение.

 

x

Атрибут «x» при получении должен возвращать значение координаты x. Для интерфейса DOMRect установка атрибута «x» должна установить координату x в новое значение.

 

y

Атрибут «y» при получении должен возвращать значение координаты y. Для интерфейса DOMRect установка атрибута «y» должна установить координату y в новое значение.

 

Решение задачи

В объекте DOMRect нас будет интересовать значение атрибута top и значение атрибута bottom.

top — это через сколько пикселей вниз наш блок прилипнет к верхней границе окна. То есть когда блок в самом верху, тогда top = 0. Верхняя граница блока упирается в верх окна.

HTML-блок почти прилип к верху--top почти равен 0 - JavaScript
HTML-блок почти прилип к верху—top почти равен 0 — JavaScript

Когда скролл находится в самом верху, тогда наш элемент имеет значение, отличное от нуля в большую сторону. Для нас это говорит о том, что элемент ЕЩЁ не виден. Мы ещё не долистали до него.

Когда скролл находится в самом низу, тогда наш элемент имеет значение, отличное от нуля в меньшую сторону. Для нас это говорит о том, что элемент УЖЕ не виден. Мы ещё не долистали до него.

 

Значение БОЛЬШЕ 0 — не долистали

Значение МЕНЬШЕ 0 — пролистали

Но это всё условно. Тут всегда важно учитывать какой высоты может быть сам блок для отрисовки. Блок может быть больше высоты окна.

bottom — это через сколько пикселей вниз нижняя граница блока прилипнет к верхней границе окна. То есть когда блок будет сразу над окном в верху.

Блок почти скрылся вверху - bottom почти 0 - JavaScript
Блок почти скрылся вверху — bottom почти 0 — JavaScript

 

Зона видимости элемента для отрисовки

Наш элемент НЕ ВИДЕН при нескольких условиях:

  • Когда top больше высоты окна
  • Когда bottom меньше нуля

Во всех остальных случаях блок виден.

 

Написание кода

Нам нужна функция, которая принимает сам элемент и проверяет виден ли он или нет. Функция должна возвращать true или false.

 

Функция сообщающая виден ли HTML-элемент в окне браузера или нет

function isElementOnVisible(element){

    let win_H = window.innerHeight;

    let elemDOMRect = element.getBoundingClientRect();

    let el_top = elemDOMRect.top;

    let el_bottom = elemDOMRect.bottom;

    if(el_top > win_H) return false;

    if(el_bottom < 0) return false;

    //console.log(element);

    return true;

};

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

Предположим, что нам нужно рисовать блок, когда его верхняя граница будет строго по центру экрана.

Нам поможет половина высоты окна (window.innerHeight / 2

Тогда функция немного изменится:

function isElementOnVisibleMid(element){

    let win_H = window.innerHeight;

    let win_H_mid = win_H/2;

    let elemDOMRect = element.getBoundingClientRect();

    let el_top = elemDOMRect.top;

    let el_bottom = elemDOMRect.bottom;

    if(el_top > win_H_mid) return false;

    if(el_bottom < 0) return false;

    //console.log(element);

    return true;

};

 

Отслеживание всех заменяемых элементов на странице

Нам нужен массив из подменяемых элементов:

let arr = [...document.querySelectorAll(".efimrtb")];

Тут будут лежать наши элементы. Они по сути являются маячками, куда мы будем загружать HTML-разметку с сервера по доскроллу.

 

Объект с данными для подмены

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

Представим, что данные мы уже получили.

        let rtb_blocks_obj = {

            1: ‘<div>Реклама1</div><div>Реклама1</div><div>Реклама1</div><div>Реклама1</div><div>Реклама1</div>’,

            2: ‘<div>Реклама2</div><div>Реклама2</div><div>Реклама2</div><div>Реклама2</div><div>Реклама2</div>’,

            3: ‘<div>Реклама3</div><div>Реклама3</div><div>Реклама3</div><div>Реклама3</div><div>Реклама3</div>’

        };

 

Обработчик события скроллинга HTML-страницы

Теперь нам нужно отлавливать событие изменения положения скролла относительно HTML-страницы.

        window.onscroll = function(){

            if(arr.length==0){return};

            let ff = function(el, index){

               if(isElementOnVisibleMid(el)){

                   let efim_rtb_name = arr[index].dataset.efimrtb;

                   console.log(el);

                   console.log(efim_rtb_name);

                   el.outerHTML = rtb_blocks_obj[efim_rtb_name];

                   arr=[…document.querySelectorAll(«.efimrtb»)];

               }

            }

            arr.map(ff);

        }

Мы каждый раз прогоняем наш массив с элементами при событии скроллинга.

Мы это делаем, пока в массиве есть элементы. Внутри выдёргиваем значения в data атрибутах, для нацеливания.

После отрисовки какого-нибудь элемента, массив вычисляется заново в документе. Так как наши подгружаемые элементы перетирают своих доноров, то классов «efimrtb» с каждым разом становится всё меньше. В конечном итоге если пользователь пролистает всю страницу контента, то все блоки будут заменены и массив опустеет. На этом всё завершится.

 

Весь код внутри одной HTML-страницы

Копируй код в HTML-файл и тестируй его работу!

<!DOCTYPE html>

<html lang=«en»>

<head>

    <meta charset=«UTF-8»>

    <meta http-equiv=«X-UA-Compatible» content=«IE=edge»>

    <meta name=«viewport» content=«width=device-width, initial-scale=1.0»>

    <title>Событие по доскроллу</title>

</head>

<body>

    <style>

        p {

            border: 1px solid black;

            height: 200px;

        }

    </style>

    <h1>Событие подгрузки по доскроллу</h1>

    <p>Какой-то текст № 01</p>

    <p>Какой-то текст № 02</p>

    <p>Какой-то текст № 03</p>

    <p>Какой-то текст № 04</p>

    <p>Какой-то текст № 05</p>

    <p>Какой-то текст № 06</p>

    <p>Какой-то текст № 07</p>

    <p>Какой-то текст № 08</p>

    <p class=«efimrtb» data-efimrtb=«1»>Какой-то HTML-блок</p>

    <p>Какой-то текст № 09</p>

    <p>Какой-то текст № 10</p>

    <p>Какой-то текст № 11</p>

    <p>Какой-то текст № 12</p>

    <p>Какой-то текст № 13</p>

    <p>Какой-то текст № 14</p>

    <p class=«efimrtb» data-efimrtb=«2»>Какой-то HTML-блок</p>

    <p>Какой-то текст № 15</p>

    <p>Какой-то текст № 16</p>

    <p>Какой-то текст № 17</p>

    <p>Какой-то текст № 18</p>

    <p>Какой-то текст № 19</p>

    <p>Какой-то текст № 20</p>

    <p>Какой-то текст № 21</p>

    <p class=«efimrtb» data-efimrtb=«3»>Какой-то HTML-блок</p>

    <p>Какой-то текст № 22</p>

    <p>Какой-то текст № 23</p>

    <script>

        let get_rtb_blocks = async function(){

            let arr = […document.querySelectorAll(«.efimrtb»)];

            let rtb_blocks_obj = {

                1: ‘<div>Реклама1</div><div>Реклама1</div><div>Реклама1</div><div>Реклама1</div><div>Реклама1</div>’,

                2: ‘<div>Реклама2</div><div>Реклама2</div><div>Реклама2</div><div>Реклама2</div><div>Реклама2</div>’,

                3: ‘<div>Реклама3</div><div>Реклама3</div><div>Реклама3</div><div>Реклама3</div><div>Реклама3</div>’

            };

            function isElementOnVisibleMid(element){

               let win_H = window.innerHeight;

               let win_H_mid = win_H/2;

               let elemDOMRect = element.getBoundingClientRect();

               let el_top = elemDOMRect.top;

               let el_bottom = elemDOMRect.bottom;

               if(el_top > win_H_mid) return false;

               if(el_bottom < 0) return false;

               console.log(element);

               return true;

           };

            window.onscroll = function(){

               if(arr.length==0){return};

               let ff = function(el, index){

                   if(isElementOnVisibleMid(el)){

                       let efim_rtb_name = arr[index].dataset.efimrtb;

                       console.log(el);

                       console.log(efim_rtb_name);

                       el.outerHTML = rtb_blocks_obj[efim_rtb_name];

                       arr=[…document.querySelectorAll(«.efimrtb»)];

                   }

               }

               arr.map(ff);

            }

        }

        get_rtb_blocks();

    </script>

</body>

</html>

 

Информационные ссылки

Стандарт HTMLhttps://html.spec.whatwg.org/multipage/

События пользовательского интерфейса