От автора: в этом уроке мы улучшим свои знания в History Web API. Сегодня мы создадим UX шаблон, который как любят, так и ненавидят: бесконечную прокрутку.
Бесконечная прокрутка это шаблон интерфейса, в котором новый контент подгружается по достижении конца страницы. Если все сделать правильно, то такой шаблон может удерживать внимание пользователя. Из лучших примеров можно привести социальные сети Facebook*, Twitter и Pinterest.
Также стоит отметить, что по сравнению с предыдущим уроком Красивый плавный переход между страницами при помощи History Web API мы сделаем большой шаг вперед. В этом уроке мы будем работать с прокруткой страницы пользователем, что может происходить очень часто. Если в нашем коде будут ошибки, это может существенно повлиять на производительность сайта. Настоятельно рекомендую прочитать предыдущие статьи, чтобы вы поняли, что мы сейчас будем делать.
Создаем демо сайт
Сайт будет представлять собой статичный блог. Его можно написать на чистом HTML или же воспользоваться генераторами статических сайтов типа Jekyll, Middleman или Hexo. Демо урока будет выглядеть вот так:
Стоит сказать пару слов про HTML структуру.
1 2 3 4 5 6 |
<!-- site хедер --> <div class="content" id="main"> <article class="article" id="article-5" data-article-id="5"> <!—контент --> </article> <!-- футер --> |
Из кода выше видно, что тег article должен быть внутри HTML тега с уникальным ID. Для блока-обертки можно взять DIV или тег section. Идентификатор можно назвать как угодно.
Также по ходу статьи нужно будет добавить атрибут data-article-id, в котором будет храниться соответствующий id тега article.
Стили можете написать любые; сделайте страницу поцветнее, более привлекательной, или же можно добавить побольше контента.
Загружаем JavaScript
Первым делом загрузите следующие JS библиотеки именно в таком порядке на все страницы блога.
jquery.js: с помощью этой библиотеки мы будем выбирать элементы, вставлять новый контент, добавлять классы и выполнять AJAX запросы.
history.js: полифил для поддержки History API в браузере.
Наш JQuery плагин
Помимо библиотек нам нужно будет подгружать наш JS файл, где мы будем писать скрипты для бесконечной прокрутки. Для этого мы завернем наш JS код в JQuery плагин, а не будем писать все с нуля, как в предыдущем уроке.
Плагин мы будем создавать с помощью сервиса jQuery Plugin Boilerplate. Сервис похож на HTML5 Boilerplate – коллекция шаблонов, макетов и лучших практик для создания JQuery плагинов.
Скачайте Boilerplate, поместите его в папку сайта со всеми JS файлами (к примеру, /assets/js/) и переименуйте файл в «keepscrolling.jquery.js» (имя придумала Дори из мультика В поисках Немо, ее любимая фраза «плывем дальше»).
Плагин будет очень гибок на варианты и настройки.
Обзор структуры JQuery плагина
Для написания плагина на JQuery необходимо мыслить нестандартно. Прежде чем писать код, давайте рассмотрим структуру плагина. Ниже я разбил код на 4 секции:
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 |
;( function( $, window, document, undefined ) { "use strict"; // 1. var pluginName = "keepScrolling", defaults = {}; // 2. function Plugin ( element, options ) { this.element = element; this.settings = $.extend( {}, defaults, options ); this._defaults = defaults; this._name = pluginName; this.init(); } // 3. $.extend( Plugin.prototype, { init: function() { console.log( "Plugin initialized" ); }, } ); // 4. $.fn[ pluginName ] = function( options ) { return this.each( function() { if ( !$.data( this, "plugin_" + pluginName ) ) { $.data( this, "plugin_" + pluginName, new Plugin( this, options ) ); } } ); }; } )( jQuery, window, document ); |
В первой секции кода мы задаем название плагина «keepScrolling» в верблюжьем стиле. Также у нас есть переменная defaults, в которой будут храниться настройки по умолчанию для плагина.
Дальше идет главная функция плагина Plugin(). Функция сравнима с конструктором, и в нашем случае именно она занимается инициализацией плагина и совмещением настроек по умолчанию с переданными.
В третьем разделе мы будем писать свои функции для бесконечной прокрутки.
Последняя секция заворачивает все в JQuery плагин.
Теперь можно перейти к написанию своего JS кода. А начнем мы с объявления настроек по умолчанию.
Опции
1 2 3 4 5 6 7 8 9 10 11 12 13 |
;( function( $, window, document, undefined ) { "use strict"; var pluginName = "keepScrolling", defaults = { floor: null, article: null, data: {} }; ... } )( jQuery, window, document ); |
В коде выше, как видно, мы задали три опции:
floor: селектор id — #floor или #footer – нижняя часть веб-сайта или контента. Обычно это футер
article: селектор класса тега article.
data: так как у нас нет доступа к внешним API (наш сайт статический), данные о теге article и его содержимом, например, URL, ID и заголовок необходимо передавать в JSON строке.
Функции
Ниже представлена функция init(). В эту функцию мы будем добавлять ряд функций, которые должны запускаться сразу же после инициализации. К примеру, вы выбрали футер сайта.
1 2 3 4 5 6 7 |
$.extend( Plugin.prototype, { // Функция `init()` init: function() { this.siteFloor = $( this.settings.floor ); // выбирает элемент, который отвечает за футер. }, } ); |
Также есть пара функций, который мы будем запускать не из функции инициализации. Мы создадим и добавим их после функции init. Первые функции будут вытягивать или возвращать что-то: что-то из строки, объекта или числа, что можно будет задействовать в других функциях плагина. Среди функций:
Получение всех статей на странице:
1 2 3 4 5 6 7 |
/** * Find and returns list of articles on the page. * @return {jQuery Object} List of selected articles. */ getArticles: function() { return $( this.element ).find( this.settings.article ); }, |
Получение адреса статьи. В WordPress адрес называют «post slug».
1 2 3 4 5 6 7 8 9 10 11 12 |
/** * Returns the article Address. * @param {Integer} i The article index. * @return {String} The article address, e.g. `post-two.html` */ getArticleAddr: function( i ) { var href = window.location.href; var root = href.substr( 0, href.lastIndexOf( "/" ) ); return root + "/" + this.settings.data[ i ].address + ".html"; }, |
Получить идентификатор и адрес следующей статьи.
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 |
/** * Return the "next" article. * @return {Object} The `id` and `url` of the next article. */ getNextArticle: function() { // Select the last article. var $last = this.getArticles().last(); var articlePrevURL; /** * This is a simplified way to determine the content ID. * * Herein, we substract the last post ID by `1`. * Ideally, we should be calling call an API endpoint, for example: * //www.techinasia.com/wp-json/techinasia/2.0/posts/329951/previous/ */ var articleID = $last.data( "article-id" ); var articlePrevID = parseInt( articleID, 10 ) - 1; // Previous ID // Loop into the Option `data`, and get the correspending Address. for ( var i = this.settings.data.length - 1; i >= 0; i-- ) { if ( this.settings.data[ i ].id === articlePrevID ) { articlePrevURL = this.getArticleAddr( i ) ; } } return { id: articlePrevID, url: articlePrevURL }; }, |
Далее пойдут утилиты плагина: функции, которые будут заниматься чем-то одним. Среди них:
Функция, которая будет показывать, в каком месте элемент входит в область просмотра (становится виден пользователю). Ее мы в основном будем использовать для того, чтобы знать, достигли мы футера или нет.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/** * Detect whether the target element is visible. * //stackoverflow.com/q/123999/ * * @return {Boolean} `true` if the element in viewport, and `false` if not. */ isVisible: function() { if ( target instanceof jQuery ) { target = target[ 0 ]; } var rect = target.getBoundingClientRect(); return rect.bottom > 0 && rect.right > 0 && rect.left < ( window.innerWidth || document.documentElement.clientWidth ) && rect.top < ( window.innerHeight || document.documentElement.clientHeight ); }, |
Функция, останавливающая выполнение другой функции, также известна как функция для устранения повторных нажатий клавиш. Как уже говорилось, мы будем работать с прокруткой страницы, а пользователь может это делать слишком часто. Т.е. функция внутри события scroll будет запускаться очень часто, приводя к торможению.
Данная функция будет убирать лишние срабатывания прокрутки. Она будет ждать при помощи параметра wait, когда пользователь прекратит прокручивать страницу, и только потом будет запускать функцию.
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 |
/** * Returns a function, that, as long as it continues to be invoked, will not b * triggered. * The function will be called after it stops being called for N milliseconds. * If immediate is passed, trigger the function on the leading edge, instead of * the trailing. * * @link //davidwalsh.name/function-debounce * @link //underscorejs.org/docs/underscore.html#section-83 * * @param {Function} func Function to debounce * @param {Integer} wait The time in ms before the Function run * @param {Boolean} immediate * @return {Void} */ isDebounced: function( func, wait, immediate ) { var timeout; return function() { var context = this, args = arguments; var later = function() { timeout = null; if ( !immediate ) { func.apply( context, args ); } }; var callNow = immediate && !timeout; clearTimeout( timeout ); timeout = setTimeout( later, wait ); if ( callNow ) { func.apply( context, args ); } }; }, |
Функция для продолжения или прекращения операции.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/** * Whether to proceed ( or not to ) fetching a new article. * @return {Boolean} [description] */ isProceed: function() { if ( articleFetching // check if we are currently fetching a new content. || articleEnding // check if no more article to load. || !this.isVisible( this.siteFloor ) // check if the defined "floor" is visible. ) { return; } if ( this.getNextArticle().id <= 0 ) { articleEnding = true; return; } return true; }, |
Функцию isProceed() мы будем использовать для проверки прохождения всех условий перед тем, как вытягивать весь контент. Если все нормально, то функция будет запущена, она вытянет новый контент и добавить под крайнюю статью.
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 |
/** * Function to fetch and append a new article. * @return {Void} */ fetch: function() { // Shall proceed or not? if ( !this.isProceed() ) { return; } var main = this.element; var $articleLast = this.getArticles().last(); $.ajax( { url: this.getNextArticle().url, type: "GET", dataType: "html", beforeSend: function() { articleFetching = true; } } ) /** * When the request is complete and it successly * retrieves the content, we append the content. */ .done( function( res ) { $articleLast .after( function() { if ( !res ) { return; } return $( res ).find( "#" + main.id ).html(); } ); } ) /** * When the function is complete, whether it `fail` or `done`, * always set the `articleFetching` to false. * It specifies that we are done fetching the new content. */ .always( function() { articleFetching = false; } ); }, |
Добавьте эту функцию внутрь init. Так функция будет запускаться сразу после инициализации плагина и вытягивать новый контент при соблюдении условий.
1 2 3 4 |
init: function() { this.siteFloor = $( this.settings.floor ); // выбирает элемент, отвечающий за футер. this.fetch(); }, |
Дальше мы добавим функцию, которая будет менять историю браузера. Вот тут нам и понадобится History Web API. Эта функция сложнее предыдущих. Сложный момент заключается в том, как определить момент для изменения истории во время прокрутки, когда менять заголовок документа, а также URL. Ниже представлена иллюстрация, чтобы вы поняли идею:
На рисунке видны три лини: “roof-line”, “mid-line” и “floor-line”, они показывают позицию статьи в окне просмотра. На рисунке видно, что низ первый статьи и верх второй находятся на средней линии. Непонятно на какую статью смотреть: это первый пост или второй? Поэтому нам нельзя менять историю браузера, когда в этой области находятся сразу две статьи.
Мы будем переписывать историю браузера на следующий пост, когда верхушка статьи проходит верхнюю линию, когда она занимает наибольшую часть видимой области.
Переписывать историю на предыдущий пост будем, когда нижняя граница статьи проходит нижнюю линию, и также когда статья занимает большую часть видимой области просмотра.
Добавьте код ниже:
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 |
init: function() { this.roofLine = Math.ceil( window.innerHeight * 0.4 ); // set the roofLine; this.siteFloor = $( this.settings.floor ); this.fetch(); }, /** * Change the browser history. * @return {Void} */ history: function() { if ( !window.History.enabled ) { return; } this.getArticles() .each( function( index, article ) { var scrollTop = $( window ).scrollTop(); var articleOffset = Math.floor( article.offsetTop - scrollTop ); if ( articleOffset > this.threshold ) { return; } var articleFloor = ( article.clientHeight - ( this.threshold * 1.4 ) ); articleFloor = Math.floor( articleFloor * -1 ); if ( articleOffset < articleFloor ) { return; } var articleID = $( article ).data( "article-id" ); articleID = parseInt( articleID, 10 ); var articleIndex; for ( var i = this.settings.data.length - 1; i >= 0; i-- ) { if ( this.settings.data[ i ].id === articleID ) { articleIndex = i; } } var articleURL = this.getArticleAddr( articleIndex ); if ( window.location.href !== articleURL ) { var articleTitle = this.settings.data[ articleIndex ].title; window.History.pushState( null, articleTitle, articleURL ); } }.bind( this ) ); }, |
И последнее, мы создадим функцию, которая будет запускать функции fetch() и history, когда пользователь начинает скролить страницу. Для этого создадим функцию scroller() и будем запускать ее сразу после инициализации плагина.
1 2 3 4 5 6 7 8 9 10 |
/** * Functions to run during the scroll. * @return {Void} */ scroller: function() { window.addEventListener( "scroll", this.isDebounced( function() { this.fetch(); this.history(); }, 300 ).bind( this ), false ); } |
Как видно из кода, эту функцию мы тоже проверяем на двойное нажатие. Функция отправляет AJAX запросы и меняет историю браузера, а это очень затратные операции.
Добавляем плейсхолдер
Плейсхолдер необязателен, но лучше его добавить. Позаботимся о пользовательском опыте. Плейсхолдер «говорит» с пользователем, показывая ему, что сейчас появится новая статья. Сперва необходимо создать шаблон. Обычно, такие шаблоны вставляются после футера.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<script type="text/template" id="tmpl-placeholder"> <div class="placeholder placeholder--article" id="placeholder-article"> <div class="container"> <div class="placeholder__header animated"> <h1></h1> </div> <div> <p class="placeholder__p-1 animated"></p> <p class="placeholder__p-2 animated"></p> </div> </div> </div> </script> |
Не забывайте, что это плейсхолдер статьи, его структура должна напоминать реальный контент блога. Подстройте структуру под свой код.
Со стилями для плейсхолдера нет ничего сложного. Берутся все базовые стили от реальной статьи, @keyframe анимация, симулирующая загрузку, а также добавляются стили для смены видимости (по умолчанию плейсхолдер скрыт; он показывается только, когда у родительского элемента есть класс fetching).
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 |
.placeholder { color: @gray-light; padding-top: 60px; padding-bottom: 60px; border-top: 6px solid @gray-lighter; display: none; .fetching & { display: block; } p { display: block; height: 20px; background: @gray-light; } &__header { animation-delay:.1s; h1 { height: 30px; background-color: @gray-light; } } &__p-1 { animation-delay:.2s; width: 80%; } &__p-2 { animation-delay:.3s; width: 70%; } } |
Затем обновляем пару строк и показываем плейсхолдер, пока выполняется AJAX запрос.
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 |
/** * Initialize. * @return {Void} */ init: function() { this.roofLine = Math.ceil( window.innerHeight * 0.4 ); this.siteFloor = $( this.settings.floor ); this.addPlaceholder(); this.fetch(); this.scroller(); }, /** * Append the addPlaceholder. * Placeholder is used to indicate a new post is being loaded. * @return {Void} */ addPlaceholder: function() { var tmplPlaceholder = document.getElementById( "tmpl-placeholder" ); tmplPlaceholder = tmplPlaceholder.innerHTML; $( this.element ).append( tmplPlaceholder ); }, /** * Function to fetch and append a new article. * @return {Void} */ fetch: function() { ... // выбираем блок-обертку статьи. var main = this.element; $.ajax( { ... beforeSend: function() { ... // Добавляем класс 'fetching' $( main ).addClass( function() { return "fetching"; } ); } } ) ... .always( function() { ... // Удаляем класс 'fetching' $( main ).removeClass( function() { return "fetching"; } ); } ); |
Вот и весь плейсхолдер! Плагин готов, осталось его запустить.
Запускаем плагин
С запуском плагина нет ничего сложного. Необходимо найти блок-обертку статьи в блоге и вызвать плагин с заданными настройками.
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 |
$( document ).ready( function() { $( "#main" ).keepScrolling({ floor: "#footer", article: ".article", data : [{ "id": 1, "address": "post-one", "title": "Post One" }, { "id": 2, "address": "post-two", "title": "Post Two" }, { "id": 3, "address": "post-three", "title": "Post Three" }, { "id": 4, "address": "post-four", "title": "Post Four" }, { "id": 5, "address": "post-five", "title": "Post Five" }] }); } ); |
Бесконечная прокрутка должна заработать.
Проблема с кнопкой назад
В этом уроке мы создали бесконечную прокрутку. Хотя данный метод и удерживает внимание пользователя, у него есть свои минусы: он ломает кнопку назад в браузере. Если кликнуть на нее, она не всегда будет вас возвращать на предыдущий пост или страницу.
Данная проблема устраняется несколькими способами; сайт Quartz, к примеру, переправляет вас на связанный URL; последний посещенный URL, а не записанный с помощью Web History API. Сайт TechInAsia просто возвращается на домашнюю страницу.
Автор: Thoriq Firdaus
Источник: //webdesign.tutsplus.com/
Редакция: Команда webformyself.
* Признана экстремистской организацией и запрещена в Российской Федерации.