Поліпшення шляхів взаємодії користувача (user flow) через переходи по сторінці

Пост є перекладом статті "Improving User Flow Through Page Transitions" з Smashing Magazine про створення плавних переходів. Автор цієї статті, Луїджі Де Роза, є фронт-енд розробником EPIC. Далі розповідь буде йти від особи автора статті. Приємного читання.
Кожен раз, коли у користувача виникають проблеми з досвідом взаємодії (UX), підвищується шанс його відходу. Зміна сторінок від однієї до іншої часто викликає переривання у вигляді білого мерехтіння без змісту, викликаючи довге завантаження, або вириваючи з контексту сторінки, яка була відкрита раніше.
Переходи між сторінками можуть поліпшити цей досвід шляхом збереження (або навіть поліпшення) користувальницького контексту, зберігаючи їх увагу та надаючи візуальне продовження. У той же час, переходи між сторінками можуть бути приємні оку і бути цікаві при хорошому виконанні.
У цій статті ми, крок за кроком, створимо переходи між сторінками. Також ми поговоримо про плюси і мінуси цієї техніки і про те, як використовувати її по максимуму.
Приклади
Безліч мобільних додатків використовують відмінні переходи між видами. У цьому прикладі, який слідує рекомендаціям Google material design, ми бачимо, як анімація передає ієрархічні і просторові відносини між сторінками.
Чому б нам не застосувати подібний підхід до наших веб-сайтів? Чому ми погоджуємося з тим, що користувач відчуває, ніби його переміщують при кожній зміні сторінки?
Як зв'язати переходи між сторінками
SPA фреймфорки
Перед тим, як бруднити руки, я повинен що-небудь сказати про фреймворках для односторінкових додатків (single-page application, SPA). Якщо ви використовуєте SPA фреймворк (такий як AngularJS, Backbone.js або Ember), то створити переходи буде набагато легше, тому що всі шляхи обробляються JavaScript. У цьому випадку вам варто звернутися до відповідної документації, щоб подивитися на реалізацію переходів між сторінками у фреймворку на ваш вибір, тому що там, можливо, є хороші приклади та інструкції.
Поганий спосіб
Моя перша спроба створити перехід між сторінками виглядала приблизно так:
document.addEventListener. ('DOMContentLoaded', function() {
// Animate in
});

document.addEventListener. ('beforeunload', function() {
// Animate out
});

Концепція проста: Використовувати анімацію, коли користувач залишає сторінку, і іншу анімацію, коли нова сторінки завантажується.
Однак, незабаром я помітив, що у цього рішення є ряд обмежень:
  • Ми не знаємо, скільки часу буде завантажуватися сторінка, так що анімація може не виглядати плавною.
  • Ми не можемо створювати переходи, які поєднують вміст з попередньої і наступної сторінок.
Фактично, єдиний шлях досягти плавного переходу — отримати повний контроль над процесом зміни сторінок і, отже, не змінювати сторінку.
Таким чином, нам потрібно змінити підхід до проблеми.
Правильний спосіб
Давайте поглянемо на кроки, які беруть участь у створенні простого плавного переходу між сторінками правильним способом. Тут присутнє щось, назване
pushState
AJAX (або PJAX) навігацією, яка, по суті, перетворить наш сайт в щось на зразок односторінкового сайту.
Це не тільки метод досягнення плавних і приємних переходів, але ми також скористаємося деякими іншими перевагами, які ми детально покриємо пізніше в цій статті.
Запобігання поведінки посилань за замовчуванням
Першим кроком ми створимо обробник події click для всіх посилань, запобігаючи їх від стандартного поведінки і змінюючи спосіб обробки зміни сторінок.
// Зверніть увагу, ми навмисно прив'язуємо наш обробник до об'єкту документа
// так, щоб ми змогли перехопити будь якоря, додані в майбутньому.
document.addEventListener. ('click', function(e) {
var el = e.target;

// Йдемо вгору за списком нод, поки не знайдемо ноду .href (HTMLAnchorElement)
while (el && !el.href) {
el = el.parentNode;
}

if (el) {
e.preventDefault();
return;
}
});

Цей метод додавання до обробника батьківського елементу, замість додавання до опреденной ноде, називається делегуванням подій, які можливі завдяки природі бульбашкових подій HTML DOM API.
Отримання сторінки
Тепер, коли ми перервали завантаження сторінки браузером, ми можемо вручну отримати сторінку використовуючи Fetch API. Давайте подивимося на наступну функцію, яка отримує вміст HTML сторінки при отриманні її URL.
function loadPage(url) {
return fetch(url {
method: 'GET'
}).then(function(response) {
return response.text();
});
}

Для браузерів, які не підтримують Fetch API, варто додати полифилл або XMLHttpRequest.
Зміна поточного URL
У HTML5 є фантастичне API, назване
pushState
, яке дозволяє веб-сайтів звертатися і змінювати історію браузера без завантаження будь-яких сторінок. Нижче ми використовуємо це для того, щоб змінити поточний URL URL наступної сторінки. Зауважте, що це — модифікація оголошеного раніше обробника click.
if (el) {
e.preventDefault();
history.pushState null, null, el.href);
changePage();

return;
}

Як ви могли помітити, ми також додали виклик функції
changePage
, на яку ми поглянемо трохи детальніше. Схожа функція також буде викликана в подію
popstate
, яке буде наступати при зміні активної історії браузера (наприклад, коли користувач натисне кнопку назад):
window.addEventListener. ('popstate', changePage);

Таким чином, ми будуємо дуже примітивну систему маршрутизації, в якій у нас є активний і пасивний режими.
Активний режим настає тоді, коли користувач натискає на посилання, і ми змінюємо URL використовуючи
pushState
, в той час як пасивний режим настає при зміні URL, і ми отримуємо повідомлення від події
popstate
. У будь-якому випадку, ми збираємося викликати
changePage
, яка подбає про читанні нашого нового URL і завантаженні сторінки.
Розбір і додавання нового вмісту
Звичайно, сторінок, за якими здійснюється перехід, є такі основні елементи, як header і footer. Ми будемо використовувати наступну DOM структуру на всіх наших сторінках (яка, сама по собі, є структурою Smashing Magazine):
<header>
...
</header>

<main>
<div class="cc">
...
</div>
</main>

<footer>
...
</footer>

Єдина частина, яку нам потрібно змінювати на кожній сторінці — вміст контейнера
cc
. Таким чином, ми можемо побудувати нашу функцію
changePage
приблизно так:
var main = document.querySelector('main');

function changePage() {
// Зауважте, що URL вже змінився
var url = window.location.href;

loadPage(url).then(function(responseText) {
var wrapper = document.createElement('div');
wrapper.innerHTML = responseText;

var oldContent = document.querySelector('.cc');
var newContent = wrapper.querySelector('.cc');

main.appendChild(newContent);
animate(oldContent, newContent);
});
}

Анімація!
Коли користувач натискає на посилання, функція
changePage
одержує HTML цієї сторінки, потім витягує
cc
в контейнер додає елемент
main
. На даний момент у нас є два контейнера
cc
на нашій сторінці, перший належить попередньої сторінки, а другий наступної.
Наступна функція
animate
, піклується про плавний перекритті двох контейнерів приховуючи старий, проявляючи новий і видаляючи старий контейнер. У цьому прикладі я використовую Web Animations API для створення анімації появи, але ви можете використовувати будь-який інший спосіб або бібліотеку, яка вам подобається.
function animate(oldContent, newContent) {
oldContent.style.position = 'absolute';

var fadeOut = oldContent.animate({
opacity: [1, 0]
}, 1000);

var fadeIn = newContent.animate({
opacity: [0, 1]
}, 1000);

fadeIn.onfinish = function() {
oldContent.parentNode.removeChild(oldContent);
};
}

Підсумковий код доступний на Github.
Все це лише основи переходів по сторінках!
Застереження та обмеження
Невеликий приклад, який ми тільки що створили, далекий від ідеалу. За фактом, ми не взяли до уваги ряд речей:
  • Переконатися, що ми торкнулися правильні посилання.
    Перед зміною поведінки посилання, нам потрібно додати перевірку, щоб переконатися, що вона повинна бути змінена. Наприклад, нам потрібно ігнорувати всі посилання з
    target="_blank"
    (яке відкриває сторінку в новій вкладці), всі посилання на зовнішні домени і деякі інші особливі випадки, такі як
    Control/Command + click
    (які також відкривають сторінку в новій вкладці).
  • Оновлення елементів за межами головного контейнера.
    В даний момент, поки сторінка змінюється, всі елементи за межами контейнера
    cc
    залишаються колишніми. Однак, деякі з цих елементів повинні бути змінені (зараз це можливо тільки змінюючи вручну), включаючи
    title
    документа, елемент меню з класом
    активний
    та, потенційно, безліч інших залежностей на нашому сайті.
  • Управління життєвим циклом JavaScript.
    Зараз наша сторінка веде себе приблизно також, як і SPA, в якому браузер не змінює сторінку самостійно. Так от, нам потрібно вручну подбати про життєвому циклі JavaScript, наприклад, пов'язуючи (binding) і розв'язуючи певні події, виконуючи плагіни, включаючи полифиллы і сторонній код.
Браузерна підтримка
Єдина вимога для цього режиму навігації —
pushState
API, який доступний у всіх сучасних браузерах. Цей метод працює повністю в якості прогресивного поліпшення. Сторінки за раніше доступні звичайним способом, і web-сайт продовжить нормально працювати, якщо відключений JavaScript.
Якщо ви використовуєте SPA фреймворк, подумайте над використанням PJAX навігації замість цього, для прискорення навігації. Натомість ви отримаєте підтримку старих браузерів і створите більш доброзичливий до SEO сайт.
Просуваючись далі
Ми можемо продовжити вичавлювати максимум з цього способу шляхом оптимізації деяких аспектів. Наступні пару трюків прискорять навігацію, значно покращують користувальницький досвід.
Використання кешу
Трохи змінивши нашу функцію
loadPage
, ми можемо додати простий кеш, який дозволить переконатися, що сторінки, які вже були відвідані, не будуть завантажені знову.
var cache = {};
function loadPage(url) {
if (cache[url]) {
return new Promise(function(resolve) {
resolve(cache[url]);
});
}

return fetch(url {
method: 'GET'
}).then(function(response) {
cache[url] = response.text();
return cache[url];
});
}

Як ви могли здогадатися, ми можемо використовувати більш довготривалий кеш Cache API або інше постійне сховище на стороні користувача (наприклад IndexedDB).
Анімація поточної сторінки
Наш ефект загасання вимагає, щоб наступна сторінка була завантажена і готова перед тим, як перехід буде завершено. Нам хотілося б почати анімацію на старій сторінці відразу після натискання на посилання, яка дасть користувачу миттєву чуйність і сприйняття продуктивності.
Використовуючи обіцянки (promises), обробка таких ситуацій може здатися дуже простою. Метод
.all
створює нове обіцянку, яка виконається після того, як всі обіцянки, передані у вигляді аргументів, будуть виконані.
// після дозволу animateOut() і loadPage()
Promise.all[animateOut(), loadPage(url)]
.then(function(values) {
...

Попередня завантаження наступної сторінки
Використовуючи навігацію PJAX, сторінка змінюється майже в два рази швидше навігації за замовчуванням, тому що браузеру не доводиться розбирати й вираховувати якісь скрипти та стилі на новій сторінці.
Проте, ми можемо піти далі, почавши попередню завантаження наступної сторінки, коли користувач наводить курсор на посилання.
Як ви можете бачити, затримка між натисканням і наведенням курсору зазвичай становить від 200 до 300 мілісекунд. Цього часу зазвичай достатньо для завантаження наступної сторінки.
Але це легко може вийти нам боком. Приміром, якщо у вас є довгий список посилань, і користувач гортає сторінку крізь них, цей спосіб буде виконувати попередню завантаження всіх сторінок, тому що посилання виявляються під курсором.
Інший момент, який ми могли помітити і прийняти в увагу, полягає в передбаченні швидкості з'єднання користувача. (Можливо це стане можливо в майбутньому з Network Information API.)
Частковий висновок
В нашій функції
loadPage
ми отримуємо весь HTML документ, хоча нам потрібен тільки
cc
контейнер. Якщо б ми використовували мову на стороні сервера, ми могли б виявити, чи приходить запит від певного користувача виклику AJAX і, якщо так, виводити тільки потрібний контейнер. Використовуючи Headers API ми можемо відправити користувальницький HTTP заголовок у нашому запиті.
function loadPage(url) {
var myHeaders = new Headers();
myHeaders.append('x-pjax', 'yes');

return fetch(url {
method: 'GET',
headers: myHeaders,
}).then(function(response) {
return response.text();
});
}

Потім, на стороні сервера (використовуючи PHP в цьому випадку), ми можемо визначити, чи існує наш користувацький заголовок перед виведенням необхідного контейнера:
if (isset($_SERVER['HTTP_X_PJAX'])) {
// Висновок контейнера
}

Це зменшить розмір HTTP повідомлень, а також зменшить навантаження на сервер.
Підбиваючи підсумки
Після впровадження даної техніки в ряд проектів, мені здалося, що багаторазова бібліотека буде надзвичайно корисною. Це дозволило б зберегти час в наступний раз, дозволивши зосередитися на самих ефекти переходів.
Таким чином народилася Barba.js — невелика бібліотека (4 KB в стислому стані), яка абстрагує всю цю складність і надає приємний, чистий і простий API для розробників. Вона також враховує різні погляди і поставляється з готовими переходами, кешуванням, попередньою завантаженням і подіями. Бібліотека має відкритий вихідний код і доступна на GitHub.
Висновок
Ми щойно подивилися на те, як створити плавний ефект перекриття, плюси і мінуси використання PJAX навігації для ефективного перетворення нашого сайту в SPA. По мимо переваг самого переходу, ми також розглянули впровадження простого кешування і механізми попереднього завантаження для прискорення завантаження сторінок.
Вся ця стаття заснована на моєму особистому досвіді і те, що я дізнався під час впровадження переходів у проектах, над якими я працював.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.