Оригинал: Crafting beautiful UX with API requests, автор Ryan Baker

При создании веб-приложений на первое место выходит создание красивого и отзывчивого интерфейса.

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

Создаем красивый интерфейс с API-запросами

Паттерн 1: Тайм-аут

Отмените мой запрос, если вы отвечаете дольше, чем я хочу.

Когда использовать

Тайм-ауты следует использовать для установки верхней границы времени, в течение которого должен выполняться запрос.

Например, сервер обращается к базе данных. База данных падает, но на сервере установлен таймаут в 30 секунд, чтобы решить, что он не может соединиться с сервером базы данных. Это означает, что ваши пользователи ждут 30 секунд!

Или вы используете AWS load balancer, и сервер за ним по какой-то причине не работает. Время ожидания балансировщика по умолчанию составляет 60 секунд, и он пытается подключиться к серверу задолго до сбоя.

Когда не использовать

Не следует использовать тайм-ауты, если время отклика API может отличаться. Хорошим примером служит API, возвращающий данные за некоторый период времени. Запрос за один день выполняется быстро (менее секунды), но запрос за восемь месяцев занимает около 12 секунд.

Не используйте тайм-ауты, если Вы не можете точно установить верхнюю границу допустимого времени запроса.

Как использовать

Предположим, в вашем приложении есть подобный метод:

И вы знаете, что ваш API в 99.99% случаев отвечает в течение 3 секунд. Используйте промисы, чтобы получить данные:

class TimeoutError extends Error {}

export const requestWithTimeout = (apiPromise, timeout = 3000) => {
  Promise.race([
    apiPromise,
    new Promise((_, reject) => {
      setTimeout(reject, timeout, new TimeoutError(`Запрос выполняется дольше ${timeout} мс`))
    })
  })
}

const getMetricsWithTimeout = requestWithTimeout(getMetrics())

Обратите внимание: большинство библиотек для API-вызовов уже имеют подобную функциональность. В этом случае используйте встроенные инструменты.

Паттерн 2: минимальное время ожидания

Тоже очень простой паттерн, противоположный предыдущему: он защищает ваше приложение от слишком быстрых ответов API.

Когда использовать

Минимальное ожидание необходимо, если вы хотите показать пользователю состояние загрузки, но API может ответить слишком быстро. Пользователь едва успеет увидеть ваш лоадер, как данные внезапно появятся на странице, сбивая фокус зрения.

Это не очень хороший пользовательский опыт. Если вы показываете состояние загрузки, вы говорите пользователю:

Подожди немного, я кое-что сделаю и вернусь

Это дает ему возможность передохнуть, возможно, проверить свой телефон. Иначе говоря, пользователь, видя лоадер, готов к ожиданию. Если вы сделаете все слишком быстро и резко, он будет сбит с толку и напряжен.

Когда не использовать

Не стоит применять паттерн минимального ожидания, если от вашего API ожидаются очень быстрые ответы. В этом случае не отображайте состояние загрузки, не давайте пользователю понять, что нужно ждать, если на самом деле это не требуется.

Как использовать

Ваш код должен говорить: «не делай ничего, пока обе эти вещи не закончатся».

export const minWait = (promise, ms) => {
  Promise.all([
    promise,
    new Promise(resolve => setTimeout(resolve, ms))
  ])
  .then(values => values[0])
}

export const getMetricsWithMinWait = () => minWait(getMetrics(), 350)

Паттерн 3: повторная попытка

Этот паттерн сложнее, чем предыдущие. Основная идея заключается в том, что мы хотим повторить отправку запроса пару раз, если получен отрицательный ответ. Это кажется простым, но есть несколько подводных камней.

Когда использовать

Паттерн пригодится, если ваше API работает с периодическими сбоями, и вы знаете, что время от времени запрос будет уходить «в молоко» по независящей от вас или пользователя причине. Если вы не можете моментально устранить проблему с API, то хотя бы «наложите заплатку» и повторите запрос еще раз.

Когда не использовать

Если ваш API надежен, не следует повторять неудавшийся запрос. В этой ситуации вероятность того, что что-нибудь изменится практически равна нулю. Большинство API надежны, поэтому будьте внимательны, применяя этот паттерн.

Как использовать

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

Чтобы определить, как долго ждать, используем экспоненциальную функцию, и повторим запрос 3 раза. Вот примеры с четырьмя разными функциями:

Первый запрос отправляется немедленно. Не получилось. Далее нужно определить, как долго ждать прежде чем повторить попытку. Приравняем X к количеству уже совершенных повторных попыток и взглянем на графики.

Из квадратичной (y = x²) и линейной (y = x) функций мы получаем 0, т. е. мы следующий запрос нужно отправить немедленно. Это нам не подходит.

Экспоненциальная (y = 2^x) и постоянная (y = 1) функции устанавливают время ожидания в 1 секунду.

Константа не дает никакой гибкости: сколько бы попыток мы не сделали, время ожидания не меняется.

Остается только экспонента. Давайте напишем функцию, которая определяет, сколько секунд ждать в зависимости от того, сколько попыток уже было сделано:

const exponentialBackoffStrategy = x => 2 ** x

Прежде чем написать функцию для повторных запросов, нужно определиться, какой запрос считать неудачным. Можно оценивать по коду статуса: если он больше или равен 500, значит, запрос провалился.

class BadResponseError extends Error {}

const ensureStatus = (res, ceiling = 500) => {
  if (res.status >= ceiling) {
    throw new BadResponseError(`Сервер вернул код статуса ${res.status}`)
  }

  return res
}

У вас могут быть другие критерии для определения сбоя. Наконец, можно написать функцию повтора со стратегией экспоненциального отката:

class BadResponseError extends Error {}

const ensureStatus = (res, ceiling = 500) => {
  if (res.status >= ceiling) {
    throw new BadResponseError(`Сервер вернул код статуса ${res.status}`)
  }

  return res
}

const _retryWithBackoff = async (requestFn, retries, strategy, iteration) => {
  try {
    const res = await requestFn()
    return ensureStatus(res)
  } catch (err) {
    if (retries > iteration) {
      await new Promise(resolve => setTimeout(resolve, strategy(iteration) * 1000))
      return retryWithBackoff(requestFn, retries, strategy, iteration + 1)
    } else {
      throw err
    }
  }
}

Есть много отличных защитных шаблонов, которые обеспечивают хороший пользовательский опыт. Эти три вы можете использовать прямо сейчас!

0 комментариев

Оставить комментарий

*Доступные HTML-теги: a, abbr, blockquote, code, pre, del, i, em, strong, b, strike
*Не будет опубликован