Недавно мы начали использовать SpeedCurve для отслеживания производительности Unsplash. Чтобы заставить его Мониторинг реального пользователя (также известный как LUX) работать с нашим приложением, нам потребовалось проделать некоторую дополнительную работу для обработки навигации на стороне клиента. В настоящее время документации по этому вопросу довольно мало, поэтому вот (подробное) руководство о том, как это сделать с помощью Redux.

ПРИМЕЧАНИЕ. за исключением небольшого шага, связанного с React, это руководство в значительной степени не зависит от фреймворка, если у вас есть Redux.

Проблема

Вот последовательность событий, с которыми мы работаем (согласно документации SpeedCurve):

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

Мы собираемся использовать redux-observable, так как это упростит прослушивание потоков действий и выполнение действий соответственно.

наблюдаемое сокращение

Большая часть нашей работы будет внутри redux-observable «эпоса». Эпики имеют следующую сигнатуру типа:

type Epic = (action$: Observable<Action>, state$: Observable<State>) => Observable<Action>

Это означает, что они получают поток действий redux и поток состояния redux и выводят поток действий redux. Подробнее об этом читайте в их документации.

ПРИМЕЧАНИЕ. В этом руководстве я кратко объясню, что делает каждый оператор Observable. Однако, если вы новичок в Observables, вам, вероятно, будет полезно, если эти ресурсы будут открыты для справки:

Изменения URL

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

В нашем конкретном случае мы используем response-router с connected-response-router, что позволяет нам считывать состояние react-router из Redux, что упрощает идентификацию изменений URL. (Если вы не используете эти библиотеки, все будет в порядке, если вы сохраните свой путь в Redux или у вас будут какие-то Observable, генерирующие пути)

  • Используя state$, мы читаем поток состояния redux. Каждый раз, когда состояние изменяется, генерируется новое значение, которым будут управлять все операторы внутри вызова .pipe().
  • map и filter ведут себя с наблюдаемыми объектами так же, как их аналоги в массивах ведут себя с массивами.

Затем мы получаем pathname$: наблюдаемый объект, который испускает все допустимые pathname значения. Наконец, distinctUntilChanged() будет выдавать новое значение только в том случае, если оно отличается от предыдущего. Это необходимо, потому что location$ может выдавать новые значения, даже если location.pathname не изменился.

Теперь у нас есть newPathname$, поток различных путевых имен, каждое из которых представляет новую навигацию на стороне клиента. Затем мы можем вызывать LUX.init после каждого:

(tap - это оператор, который вы используете, когда хотите вызвать побочные эффекты.)

Получение и рендеринг данных

Теперь самое сложное. Наши маршруты можно разделить на два:

  • те, которые требуют выборки данных перед рендерингом («динамические» маршруты)
  • те, которым не нужны данные и которые могут обрабатывать немедленно («статические» маршруты)

Нам нужно смоделировать эти сценарии в Redux. Мы сделаем это с помощью трех действий: ROUTE_DATA_FETCHED, DYNAMIC_ROUTE_COMPONENT_UPDATED и STATIC_ROUTE_COMPONENT_UPDATED. (Это единственный бит, специфичный для React)

PS: Для удобства мы написали trackRouteUpdates HOC, который отправляет componentDidUpdate действия, и обернули им все наши компоненты маршрута. Мы настоятельно рекомендуем вам сделать то же самое, чтобы не повторять эту логику в каждом компоненте.

Теперь, когда у нас есть действия, давайте вернемся к нашему эпику и воспользуемся ими:

Действия внутри componentDidUpdate должны быть отправлены несколько раз, но нас интересует только первое. takeOneAction будет прослушивать конкретное действие и завершить наблюдаемое после того, как действие будет обнаружено один раз в action$ потоке (когда наблюдаемое «завершено», оно перестает выдавать значения).

Давайте объединим эти три наблюдаемых действия:

Для динамических маршрутов мы должны дождаться получения данных и затем обновления маршрута в указанном порядке. Это то, что делает concat, а takeLast(1) захватывает последнее значение, выданное объединенным потоком. Это удобный способ узнать, что обе внутренние наблюдаемые завершились.

Подведение итогов (почти)

Теперь мы можем связать это с наблюдаемым newPathname$, которое мы написали ранее:

(Обратите внимание, как приведенный выше код полностью отражает последовательность событий, описанную в начале статьи).

mergeMap сопоставит вашу newPathname$ наблюдаемую с внутренним наблюдаемым действием. Вариант mergeMapTo используется, когда вам не нужны значения предыдущей наблюдаемой, как здесь (значения newPathname не нужны в waitForDynamicOrStaticRouteUpdated$).

Обработка ожидающих отслеживания LUX

Готово! ... ну, не совсем. Одно предостережение - это случай, когда пользователь быстро переходит от страницы A к B и C до того, как страница B завершит рендеринг. В конечном итоге мы вызовем LUX.init дважды подряд: один раз для страницы B и еще раз для страницы C.

Этого не может быть: каждому LUX.init() нужно соответствовать LUX.send(). Мы решили исправить это, убедившись, что LUX.send всегда вызывается перед выполнением нового LUX.init вызова.

Нам не особенно это решение (это одна из нескольких изменяемых переменных во всей нашей кодовой базе). Однако решение этой проблемы неизменным «RxJS-способом» оказалось настолько сложным, что мы в конечном итоге остались довольны вышеизложенным. Если у вас есть более чистый способ сделать это, сообщите нам об этом!

Конечный результат ✨

Наконец-то мы закончили. 🤯

Если вам нравится то, что вы читаете, и вы думаете, что вам понравится работать с нами, мы ищем! https://unsplash.com/hiring/job-posts/4/react-engineer