Контекст
В нашем продукте мы позволяем пользователям входить в систему с помощью сторонних приложений, с которыми мы интегрировались, и для этого у нас есть многоэтапный асинхронный процесс входа.
- третья сторона перенаправляет на наш интерфейс с токеном в 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, поэтому ему не нужно выставлять установщик состояния чему-то вне себя.
- вызывающий может контролировать, когда приостанавливать и возобновлять шаги, и, следовательно, может добавлять ложные задержки или принимать пользовательские данные между шагами.
- После такой реализации функция входа в систему не должна заботиться о каких-либо будущих требованиях, которые могут возникнуть у потребителей для воздействия на поток (пауза, добавление задержки и т. д.).