Контекст

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

  • третья сторона перенаправляет на наш интерфейс с токеном в URL-адресе, FE извлекает этот токен, делает асинхронный вызов API, чтобы получить некоторую другую соответствующую информацию на основе этого извлеченного токена.
  • Затем FE отправляет информацию на серверную часть, и серверная часть в ответ устанавливает пользовательский JWT. Это само по себе требует нескольких асинхронных вызовов от FE к BE.
  • После этого FE необходимо сделать еще несколько вызовов API, чтобы получить некоторые важные сведения о пользователе, установить их в глобальном состоянии и т. д., прежде чем отображать пользовательский интерфейс.

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

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

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

Проблемы с текущей реализацией

Текущая реализация функции login выглядит примерно так:

function login(onProgress, ...) {
  try {
    const step1Result = await step1();
    onProgress(0.3);
    const step2Result = await step2(step1Result);
    onProgress(0.65);
    const step3Result = await step3(step2Result);
		onProgress(1);
  } catch(e) {
    ...
  }
  ...
}

Функция входа внутри использует promises/async await, но вызывающая функция должна передать обратный вызов для onProgress. Это не является серьезной проблемой само по себе, особенно если вы владеете и вызывающей функцией, и вызываемой функцией.

Одним из основных неудобств, с которыми я столкнулся при этой настройке в нашем случае, было то, что функция onProgress передавалась от компонента реакции — LoginLoader, который устанавливал некоторое внутреннее состояние компонента в этой функции. Это немного тревожит по причинам, которые я не могу точно определить на данный момент, но это просто кажется нечистым/грязным/неуклюжим. Это похоже на передачу функции setState службе/утилите, которая находится вне компонента. Сама служба / утилита может быть протестирована изолированно и является «чистой» с точки зрения понимания, но теперь компонент передал управление чему-то внешнему, чтобы напрямую изменить свое состояние. Было бы намного проще рассуждать о том, как прогресс может быть возвращен как значение, вызвав функцию входа в систему, чтобы компонент мог использовать его как возвращаемое значение из обычной функции. (Дайте мне знать в комментариях, если у вас есть конкретные мысли о том, почему это кажется неуклюжим)

const LoginLoader = (props) => {
  const [progress, setProgress] = useState(0);
  ...
  useEffect(() => {
    ...
    login(setProgress);
    ...
    }, [...]
  )
  ...
  return (
    ...
    <div 
      styleName="progress-bar-inner" 
      style={{ transform: `scaleX(${props.progress})` }} 
    />
    ...
  )
}

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

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

Лучшая реализация

Повторяем наши опасения:

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

Все эти проблемы могут быть решены индивидуально или коллективно с помощью обратных вызовов или обещаний. Я всегда использовал обещания, чтобы заменить обратные вызовы и предоставить больше контроля вызывающей стороне. В этом случае нам нужно прекратить управление вызывающей стороной несколько раз (каждый раз, когда мы сообщаем о прогрессе), поэтому нет (простого) способа обещать эту настройку. Один паттерн, который очень естественно сочетается с этим, — это генераторы. нам нужно вернуть несколько значений, и нам нужно прекратить управление вызывающей стороной. Нам нужно ожидать несколько функций внутри генератора, поэтому нам понадобится асинхронный генератор.

Вот как выглядит новая реализация:

async function* login() {
  try {
    const step1 = await stepOne();
    yield 0.3;
    const step2 = await stepTwo();
    yield 0.65;
    const step3 = await stepThree();
    yield 1;
  } catch (e) {
    console.log(e);
  }
}

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

Это решает все проблемы, которые у нас были с реализацией обратного вызова:

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

Рекомендации