Як створити нескінченну прокрутку за допомогою History Web API

41

Від автора: у цьому уроці ми покращимо свої знання в History Web API. Сьогодні ми створимо UX шаблон, який люблять, так і ненавидять: нескінченну прокрутку.

Нескінченна прокрутка це шаблон інтерфейсу, у якому новий контент підвантажується по досягненні кінця сторінки. Якщо все зробити правильно, то такий шаблон може утримувати увагу користувача. З кращих прикладів можна привести соціальні мережі Facebook, Twitter і Pinterest.

Також варто відзначити, що порівняно з попереднім уроком Гарний плавний перехід між сторінками за допомогою History Web API ми зробимо великий крок вперед. У цьому уроці ми будемо працювати з прокруткою сторінок користувачем, що може відбуватися дуже часто. Якщо в нашому коді будуть помилки, це може суттєво вплинути на продуктивність сайту. Настійно рекомендую прочитати попередні статті, щоб ви зрозуміли, що ми зараз будемо робити.

Створюємо демо сайт

Сайт буде представляти собою статичний блозі. Його можна написати на чистому HTML або ж скористатися генераторами статичних сайтів типу Jekyll, Middleman або Hexo. Демо уроку буде виглядати ось так:

Як створити нескінченну прокрутку за допомогою History Web API

Варто сказати пару слів про HTML структуру.

З коду вище видно, що тег 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 (ім’я придумала Дорі з мультика У пошуках Немо, її улюблена фраза «пливемо далі»).

Як створити нескінченну прокрутку за допомогою History Web API

Плагін буде дуже гнучкий на варіанти і налаштування.

Огляд структури JQuery плагін

Для написання плагіна на JQuery необхідно мислити нестандартно. Перш ніж писати код, давайте розглянемо структуру плагіна. Нижче я розбив код на 4 секції:

;( 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 коду. А почнемо ми з оголошення налаштувань за замовчуванням.

Опції

;( 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(). В цю функцію ми будемо додавати ряд функцій, які повинні запускатися відразу ж після ініціалізації. Наприклад, ви вибрали футер сайту.

$.extend( Plugin.prototype, {
// Функція `init()`
init: function() {
this.siteFloor = $( this.settings.floor ); // вибирає елемент, який відповідає за футер.
},
} );

Також є пара функцій, який ми будемо запускати не з функції ініціалізації. Ми створимо і додамо їх після функції init. Перші функції будуть витягати або повертати що-то: що-то з рядка, об’єкта або числа, що можна буде задіяти в інших функціях плагіна. Серед функцій:

Отримання всіх статей на сторінці:

/**
* 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».

/**
* 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»;
},

Отримати код та адресу наступної статті.

/**
* 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:
* https://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
};
},

Далі підуть утиліти плагіна: функції, які будуть займатися чимось одним. Серед них:

Функція, яка буде показувати, в якому місці елемент входить у область перегляду стає видно користувачу). Її ми в основному будемо використовувати для того, щоб знати, досягли ми футера чи ні.

/**
* Detect whether the target element is visible.
* http://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, коли користувач припинить прокручувати сторінку, і тільки потім буде запускати.

/**
* 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 https://davidwalsh.name/function-debounce
* @link http://underscorejs.org/docs/underscore.html#section-83
*
* @param {Function} Function func 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 );
}
};
},

Функція для продовження або припинення операції.

/**
* 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() ми будемо використовувати для перевірки проходження всіх умов перед тим, як витягувати весь контент. Якщо все нормально, то функція буде запущена, вона витягне новий контент і додати під крайню статтю.

/**
* 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. Так функція буде запускатися відразу після ініціалізації плагіна і витягати новий контент при дотриманні умов.

init: function() {
this.siteFloor = $( this.settings.floor ); // вибирає елемент, що відповідає за футер.
this.fetch();
},

Далі ми додамо функцію, яка буде змінювати історію браузера. Ось тут нам і знадобиться History Web API. Ця функція складніше попередніх. Складний момент полягає в тому, як визначити момент для зміни історії під час прокручування, коли міняти заголовок документа, а також URL. Нижче представлена ілюстрація, щоб ви зрозуміли ідею:

Як створити нескінченну прокрутку за допомогою History Web API

На малюнку видно три лінії: «roof-line», «mid-line» і «floor-line», вони показують позицію статті у вікні перегляду. На малюнку видно, що низ перший статті і верх другий знаходяться на середній лінії. Незрозуміло на яку статтю дивитися: це перший пост чи другий? Тому нам не можна змінювати історію браузера, коли в цій області знаходяться відразу дві статті.

Ми будемо переписувати історію браузера на наступний пост, коли верхівка статті проходить верхню лінію, коли вона займає найбільшу частину видимої області.

Як створити нескінченну прокрутку за допомогою History Web API

Переписувати історію на попередній пост будемо, коли нижня межа статті проходить нижню лінію, і також коли стаття займає велику частину видимої області перегляду.

Як створити нескінченну прокрутку за допомогою History Web API

Додайте наступний код:

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 = 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() і будемо запускати її відразу після ініціалізації плагіна.

/**
* 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 запити і змінює історію браузера, а це дуже витратні операції.

Додаємо плейсхолдер

Плейсхолдер необов’язковий, але краще його додати. Подбаємо про інтерфейсу досвіді. Плейсхолдер «говорить» з користувачем, показуючи йому, що зараз з’явиться нова стаття. Спершу необхідно створити шаблон. Зазвичай, такі шаблони вставляються після футера.

Не забувайте, що це плейсхолдер статті, його структура повинна нагадувати реальний контент блогу. Підстройте структуру під свій код.

Зі стилями для плейсхолдера немає нічого складного. Беруться усі базові стилі від реальної статті, @keyframe анімація, симулює завантаження, а також додаються стилі для зміни видимості (за замовчуванням плейсхолдер прихований; він показується тільки, коли у батьківському елементу є клас fetching).

.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 запит.

/**
* 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»;
} );
} );

Ось і весь плейсхолдер! Плагін готовий, залишилося його запустити.

Запускаємо плагін

Із запуском плагіна немає нічого складного. Необхідно знайти блок-обгортку статті в блозі і викликати плагін з заданими налаштуваннями.

$( 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»
}]
});
} );

Нескінченна прокрутка повинна заробити.

Як створити нескінченну прокрутку за допомогою History Web API

Проблема з кнопкою назад

У цьому уроці ми створили нескінченну прокрутку. Хоча даний метод і утримує увагу користувача, у нього є свої мінуси: він ламає кнопку «назад» в браузері. Якщо клікнути на неї, вона не завжди буде вас повертати на попередній пост або сторінку.

Дана проблема усувається кількома способами; сайт Quartz, приміром, переправляє вас на пов’язаний URL; останній відвіданий URL, а не записаний з допомогою Web History API. Сайт TechInAsia просто повертається на домашню сторінку.