Улучшение путей взаимодействия пользователя (user flow) через переходы по странице

в 13:49, , рубрики: api, javascript, pjax, transitions, UX, Клиентская оптимизация, перевод

Пост является переводом статьи "Improving User Flow Through Page Transitions" со Smashing Magazine о создании плавных переходов. Автор этой статьи, Луиджи Де Роза, является фронт-энд разработчиком в EPIC. Далее повествование будет идти от лица автора статьи. Приятного чтения.

Каждый раз, когда у пользователя возникают проблемы с опытом взаимодействия (UX), повышается шанс его ухода. Смена страниц от одной до другой часто вызывает прерывания в виде белого мерцания без содержания, вызывая долгую загрузку, или вырывая пользователя из контекста страницы, которая была открыта ранее.

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

В этой статье мы, шаг за шагом, создадим переходы между страницами. Мы также поговорим о плюсах и минусах этой техники и о том, как использовать ее по максимуму.

Примеры

Множество мобильных приложений используют отличные переходы между видами. В этом примере ниже, который следует рекомендациям Google material design, мы видим, как анимация передает иерархические и пространственные отношения между страницами.

Почему бы нам не применить похожий подход к нашим web-сайтам? Почему мы соглашаемся с тем, что пользователь чувствует, будто его телепортируют при каждой смене страницы?

Как связать переходы между страницами

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 документа, элемент меню с классом active и, потенциально, множество других зависимостей на нашем сайте.
  • Управление жизненным циклом 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. По мимо преимуществ самого перехода, мы также рассмотрели внедрение простого кэширования и механизмы предварительной загрузки для ускорения загрузки новых страниц.

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

Автор: kirillbykov2

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js