Сложное управление потоком побочных эффектов и тестирование

Создание приложения с помощью React может немного запутать, когда данные распределяются между компонентами, а разные состояния приводят к слишком большой сложности и трудностям. Redux - это легкий инструмент управления состоянием, который можно использовать с любой инфраструктурой или библиотекой JavaScript для поддержания согласованного и предсказуемого контейнера состояния. Сохраняя состояние приложения в одном глобальном хранилище, а не на уровне компонентов, каждый отдельный компонент может получить доступ к любому состоянию, в котором он нуждается, в любое время независимо от формы дерева компонентов, если он подключен к хранилищу через Redux.

Предсказуемая природа Redux исходит из неизменяемого состояния, которое никогда не меняется, а также чистых редукторов функций. Поскольку редукторы являются функциональными, обычно используемым промежуточным программным обеспечением для обработки логики побочных эффектов и асинхронных вызовов является redux-thunk. Thunk позволяет создателям действий возвращать функцию вместо действия.

Я предполагаю, что вы имеете базовое представление о React и Redux, но это будет руководство по другому промежуточному программному обеспечению Redux для обработки побочных эффектов: Redux Sagas

Перейти к пошаговому руководству с примером кода здесь.

Почему Redux Saga?

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

Redux Saga использует для этого функции генератора ES6. Генераторы позволяют синхронно писать асинхронный код. Генератор автоматически приостанавливает - или уступает - при каждом асинхронном вызове, пока он не завершится, прежде чем продолжить. Эта парадигма позволяет создавать гораздо более простой и читаемый код за счет централизации асинхронной логики для более управляемых и сложных асинхронных потоков.

Функции генератора саги немного напоминают мне async/await, с некоторыми незначительными изменениями, такими как yield и put(). Некоторые из этих различий обеспечивают весомые преимущества, например takeLatest() обеспечение того, чтобы до завершения выполнялся только последний вызов выборки, несмотря на то, что было отправлено несколько одновременных действий выборки. Однако асинхронные вызовы, которые обычно находятся непосредственно внутри создателя действия в преобразователе, будут иметь четкое разделение в Redux Sagas.

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

Redux Saga становится наиболее полезным, когда API или другие асинхронные вызовы выполняются со сложными потоками, в которых вызовы зависят от следующего.

Плюсы:
→ Более читаемый код
→ Подходит для обработки сложных сценариев
→ Тестовые случаи становятся простыми без необходимости имитировать асинхронное поведение
Минусы:
→ усложняет код
→ дополнительная зависимость
→ много концепций для изучения
Заключение: < br /> → Подходит для сложных асинхронных частей приложения, требующих сложных модульных тестов.

Небольшая заметка о Thunks:

Учитывая, что Redux Saga стремится организовать сложные асинхронные операции с Redux, это альтернатива Thunks. Однако саги предоставляют больше возможностей. Преобразователи хорошо подходят для простых случаев использования, но могут быть не лучшим выбором для более сложных сценариев.

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

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

Генераторы

Обозначаемые *, генераторы используют ключевое слово yield для приостановки функции. В то время как async/await можно преобразовать в генераторы, обратное невозможно. Более того, takeLatest() поведение Sagas и отмена функции генератора - это дополнительные атрибуты, предоставляемые Redux Saga.

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

Прохождение:

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

Аккорд Проект (AP)

AP Github

Репозиторий Template Studio

В настоящее время разрабатывается новый дизайн Template Studio. Детали в основном не важны, достаточно сказать, что часть, которую я буду проходить, вызывает API-вызов для сбора массива шаблонов и отображения их в компоненте. Этот редизайн будет состоять из множества взаимосвязанных компонентов React, расположенных в одном приложении и управляемых магазином Redux. Поскольку это начиналось сложно и будет только дальше, мы решили продолжить Redux Saga, чтобы справиться с этой сложностью.

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

Это будет руководство по следованию логике Redux Saga в Template Studio для Accord Project. Надеюсь, это окажется для вас полезным ресурсом.

Настраивать

Общие методы Redux Saga (называемые Эффектами):

fork → Выполняет неблокирующую операцию для переданной функции.

take → Пауза до получения действия.

race → Запускает эффекты одновременно, а затем отменяет их все, когда один из них завершается.

call → Запускает функцию. Если он возвращает обещание, сага приостанавливается до разрешения.

put → Отправляет действие.

select → Запускает функцию выбора для получения данных из состояния.

takeLatest → Выполняет операцию, возвращает только результаты последнего вызова.

takeEvery → Вернет результаты для всех инициированных вызовов.

Общая структура потока данных в приложении будет выглядеть примерно так:

Для начала мы настраиваем основной рендер приложения и применяем магазин к Provider, заданному react-redux:

Магазин

Используя метод createSagaMiddleware из Redux Saga, мы создаем sagaMiddleware и запускаем его на нашем rootSaga, что мы увидим ниже. Более того, мы объединяем все наши редукторы и включаем их в магазин при создании.

Подобно редукторам, Sagas будет зарегистрирован с rootSaga. Использование промежуточного программного обеспечения rootSaga позволяет успешно отправлять действия.

Саги

Саги работают в фоновом режиме и sagaMiddleware ими управляет. Будучи функциями генератора, Sagas контролирует каждый шаг функции. Мы передаем объекты sagaMiddleware, которые сообщают ему, что делать с заданными аргументами, которые он будет выполнять и возобновлять после завершения, таким образом, кажется, что он работает синхронно.

Саги разбиты на корень, наблюдателей и рабочих. Все остальные саги, которые вы пишете, объединены в корень.

→ Корневой
Все саги будут зарегистрированы с корневыми сагами. Объединенные в all() функцию, они могут запускаться каждый раз одновременно.

→ Наблюдатель
Позволяя Saga знать, когда начинать, эта функция генератора отслеживает действия (аналогично редюсерам) и вызывает worker Sagas для выполнения вызова API. Эта функция находится на Line 62 ниже:

Подобно takeLatest(), takeEvery() позволяет запускать несколько экземпляров саг одновременно. Оба они построены на take(), что является синхронным.

→ Рабочий
Эта сага (Lines 14, 31 и46 выше) вызовет побочный эффект. После загрузки данных метод put() используется для отправки другого действия. Это не отправляет напрямую, а скорее создает описание эффекта, которое сообщает Redux Saga о его отправке. Поскольку put() ожидает действия для аргумента, он служит создателем действия. Однако мы разбили эти действия на модули, как вы увидите ниже.

Редуктор

Подобно действиям, редукторы одинаковы для Redux Saga. Это просто функция, которая принимает состояние и действие в качестве аргументов и возвращает следующее состояние приложения. В то время как действие описывает только то, что произошло, редуктор описывает изменение состояния приложения.

Составная часть

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

Создатель действий

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

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

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

Резюме

  1. (TemplateLibrary.js)
    Когда компонент библиотеки монтируется, отправляется действие (getTemplatesAction).
  2. (templatesActions.js)
    Как мы видим, getTemplatesAction отправляет объект с type: ‘GET_AP_TEMPLATES’.
  3. (templatesSaga.js)
    Наблюдатель уловит действие типа ‘GET_AP_TEMPLATES’ и вызовет pushTemplatesToStore.
  4. (templatesSaga.js)
    Когда вызывается pushTemplatesToStore, происходит несколько вещей. Мы yield вызов API, сделанный TemplateLibrary, импортированным из @accordproject/cicero-core, и помещаем его в массив. Оттуда вызывается getTemplatesSuccess с массивом шаблонов в качестве аргумента.
  5. (templatesReducer.js)
    Это действие (GET_AP_TEMPLATES_SUCEEDED) завершается в редукторе, обновляя состояние с помощью массива шаблонов, который был прикреплен к действию.
  6. (TemplateLibrary.js)
    Поскольку этот компонент подписан на хранилище и с ним связаны реквизиты prop, массив шаблонов теперь применяется к этому компоненту через реквизиты.

Тесты

Приближение тестирования Redux Saga может быть пугающим. Общее правило эффективности Redux Sagas состоит в том, чтобы гарантировать, что Sagas делают как можно меньше, и вынести любую сложную логику в отдельную регулярную функцию. Я бы порекомендовал использовать несколько подходов:

Модульные тесты
В этом подходе эффекты доходности проходят индивидуально с помощью next() метода. Тест может проверить полученный эффект и сравнить его с ожидаемым эффектом с next().value. Хотя это просто, это приводит к хрупким испытаниям. Это связано с тем, что тесты так тесно связаны с реализацией и порядком эффектов. Рефакторинг кода, скорее всего, сломает тесты.

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

Использование recordSaga позволяет нам просматривать тип отправленного действия в данном тестовом примере.

Интеграционные тесты
Этот подход проверяет интересующие вас эффекты. В этом случае вы будете выполнять сагу до конца, попутно высмеивая эффекты. Поскольку это не выполняется изолированно, результаты более безопасны. Теперь рефакторинг не должен так легко ломать тесты. Чтобы упростить этот процесс, мы используем модуль от Джереми Фэйрбанка - redux-saga-test-plan, который помогает делать утверждения в эффектах, генерируемых Sagas.

Этот модуль содержит expectSaga, который возвращает API для подтверждения того, что сага дает определенные эффекты. Он принимает функцию генератора в качестве аргумента вместе с дополнительными аргументами для передачи генератору. Хотя expectSaga работает на runSaga, который мы использовали в sagaTest, он обеспечивает немного более простое использование. Это также означает, что expectSaga является асинхронным.

После вызова expectSaga с утверждениями начните сагу с run(). Это возвращает Promise, который затем можно использовать с платформой тестирования. Мы используем Jest. Если все утверждения пройдут, Promise разрешится.

Заключение

Redux Saga потрясающая. Они обеспечивают очень чистый способ выполнения асинхронных вызовов в Redux и поощряют ясный модульный код. Хотя можно сделать то же самое и без него, это будет довольно сложно, и я считаю, что стоит потратить время на то, чтобы научиться.

Не стесняйтесь обращаться ко мне с любыми вопросами или отзывами.