Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Сегодня мало кто помнит, что веб-приложения могут работать без единого XHR-запроса. AJAX (Asynchronous Javascript and XML) дает классную возможность — подгружать данные без перезагрузки страницы. Эта концепция лежит в основе большинства современных SPA.
Но ничто не дается просто так, за все нужно платить. Концепция AJAX кажется предельно простой, но даже на уровне запроса данных с сервера можно встретить кучу проблем.
Для начала давайте напишем самое простое SPA-приложение с AJAX:
initApp();
function initApp() {
document.body.innerHTML = `
<h3>Employees list</h3>
<ul id="employees-list"></ul>
<button id="load-employees">Load employee</button>
`;
document.getElementById('load-employees').addEventListener('click', loadEmployee);
}
function loadEmployee() {
fetch('http://dummy.restapiexample.com/api/v1/employee/1')
.then(res => res.json()).then(({employee_name}) => addEmployee(employee_name));
}
function addEmployee(employeeName) {
const employeeElement = document.createElement('li');
employeeElement.innerText = employeeName;
document.getElementById('employees-list').appendChild(employeeElement);
}
Все предельно просто: при клике на кнопку запрашиваем данные с сервера и при их получении добавляем элемент в список.
Как я говорил, на этом этапе многое может пойти не по плану и, чтобы раскрыть тему глубже, сначала разберем немного теории.
Две философии построения отказоустойчивых интерфейсов
Graceful degradation
Это философия проектирования интерфейсов, при которой пользователю изначально предоставляется максимально возможное количество функций. И только в случае отказа какой-либо из частей системы отключаются зависимые от нее функции. Звучит сложно, но ниже мы разберем это на примере — так будет гораздо понятнее.
Progressive enhancement
Существует альтернативная/параллельная философия — progressive enhancement. В ней движение идет в другую сторону: изначально пользователю предоставляется минимальный (либо средний) набор функциональностей. А для инициализации остальных сначала проверяется поддержка необходимых для их работы частей системы.
Обычно, когда говорят про graceful degradation и progressive enhancement в контексте браузерных приложений, имеют в виду кроссбраузерность или адаптивность. Есть популярный пример, объясняющий эти концепции. Допустим, в вашем приложении есть функция печати страницы, и если вы делаете так:
<body>
<a href="javascript:window.print()" id="print-btn">Print</a>
<script>
const printButton = document.getElementById('print-btn');
if (printButton && typeof window.print !== 'function') {
printButton.parentNode.appendChild(document.createTextNode('Печать не поддерживается'));
printButton.parentNode.removeChild(printButton);
}
</script>
</body>
то это graceful degradation, потому что вы сразу показываете кнопку печати, но когда понимаете, что печать не поддерживается браузером, то удаляете функциональность.
P.S.: В оригинальном примере при демонстрации graceful degradation использовался тег noscript, но, мне кажется, это сильно устарело.
Если же вы делаете так:
if(typeof window.print === 'function') {
const printButton = document.createElement('a');
printButton.innerText = 'Print';
printButton.addEventListener('click', () => window.print());
document.body.appendChild(printButton);
}
это уже progressive enhancement, потому что сначала вы проверяете поддержку необходимого API и только потом добавляете функцию.
Примеры демонстрируют самое примитивное применение философий построения отказоустойчивых интерфейсов.
Вернемся к AJAX и HTTP-запросам.
Что же может пойти не так при использовании AJAX?
Неожиданный HTTP-статус-код
Самым простым случаем будет, если сервер вернет не тот статус-код, что вы ожидали, допустим 500. Это частый сценарий, и вы наверняка имеете некоторые инструменты для его обработки. Например, показываете пользователю уведомление «Произошла ошибка сервера». Это явно degradation, но на сколько он graceful? А можно ли тут применить progressive enhancement? Нет, это точно не место для progressive enhancement — функциональность уже деградировала. Можно только красиво обыграть эту неприятность:
- Узнать, что эта ситуация вообще произошла у клиента, чтобы предотвратить ее в будущем. Для этого обычно используются логгеры ошибок, например sentry.io.
- Кешировать полученные данные, если это возможно. Круто, если уже был вызов аналогичного запроса и вы закешировали данные. В таком случае даже при получении неожиданного статус-кода от сервера вы сможете отобразить интерфейс, хоть и не с самыми актуальными данными.
- Попробовать повторить запрос позже. Возможно, это временный сбой сервера и через несколько секунд его «отпустит». Можно делать повторный запрос автоматически либо предоставить эту возможность пользователю.
- Не блокировать работу остальной части приложения. Если перед вызовом HTTP-запроса вы показываете спиннер или скелетон, то не забудьте скрыть его при любом завершении запроса, успешном или нет. Это может показаться очевидным, но я сталкивался с таким достаточно часто.
- Вообще, неожиданных статус-кодов может быть много, например, когда истекла сессия пользователя и сервер ответил кодом 403. Для этой ошибки необходим отдельный обработчик, который перевыпустит сессионный токен или отправит пользователя на авторизацию. В отказоустойчивом приложении должны быть обработчики для всех возможных ответов сервера.
Невалидный ответ
Никогда не доверяйте бэкэнду! Сервер может ответить кодом 200, но в теле ответа вернет не те данные, которые вам нужны. В этой ситуации можно делать то же самое, что и при неожиданном статус-коде, но сложность в том, чтобы определить, что ответ действительно невалидный.
Если вы пишите на typescript, то для вас есть классный инструмент — typescript-json-schema. С его помощью можно генерировать json-схемы из интерфейсов typescript и использовать их для валидации данных в runtime.
Долгий ответ
Это тот удар, которого мало кто ожидает. Если об ошибках или даже о невалидных данных ответа мы помним, то про тайм-ауты вспоминаем редко. Виновником ситуации может быть не только серверное приложение, но даже провайдер интернета или устройство клиента.
Не стоит об этом забывать, лучше уведомить пользователя, что запрос выполняется дольше обычного, чем оставить его один на один с крутящимся кружком на экране. При истечении времени, выделенного на выполнение запроса, можно проходить аналогичный сценарий, что и в двух предыдущих ситуациях.
Отсутствие интернета
Я был сильно впечатлен, узнав, что у «Гугл-документов» есть офлайн-режим. Это очень помогло мне, когда я решил дописать статью в самолете, где не было интернета.
Конечно, приложения бывают разные и многие из них практически бесполезны без интернета. Но даже в этих приложениях можно обрабатывать случай с отсутствием соединения и показывать информативное сообщение (хотя игра в тираннозавра в «Хроме» мне тоже нравится).
Кроме этого вы можете слушать события подключения/отключения интернет-соединения. И, например, автоматически перезагружать данные при событии online на window.
Отказоустойчивый интерфейс — это непросто
Итого, список действий, которые необходимо реализовать при вызове HTTP-запроса:
- Логировать ошибки.
- Кешировать данные и использовать их.
- Повторять неуспешные запросы.
- Не блокировать интерфейс.
- Обрабатывать все возможные ответы сервера.
- Валидировать ответы сервера.
- Устанавливать тайм-ауты.
- Офлайн-режим (отсутствие интернета).
То, что сначала казалось тривиальным, вылилось в целую философию с множеством проблем. Конечно, это не мастхэв. Но если ваше приложение достигло высокого уровня зрелости и вы хотите сделать действительно качественный интерфейс, то это то направление, в котором стоит развиваться.
Цель этой статьи — рассказать о возможных проблемах при работе с HTTP-запросами, но не о конкретных решениях. Сегодня существует большое количество библиотек и фреймворков, которые нацелены на решение этих проблем, например, HTTP interceptors в Angular.
Зная о возможных проблемах, будет гораздо легче найти для них решение в интернете.