Введение в популярный фреймворк JavaScript для обработки побочных эффектов

Команда внешнего интерфейса Takeaway.com усердно работала над переносом нашего основного веб-приложения на современный стек JavaScript. Мы создаем новое клиентское приложение с Next.js, React и Redux Saga. Когда мы начинали, мой опыт был в основном с Redux Thunk и Redux Promise. Redux Saga - совсем другой зверь, поэтому мне потребовалось некоторое время, чтобы понять основной контекст, общий подход, то, как функции генератора вписываются в поток, а также плюсы и минусы использования библиотеки.

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

В этой статье было бы полезно понять основные концепции Redux, такие как редукторы и действия, но это не обязательно.

Прежде чем мы перейдем к коду Redux Saga, давайте сначала рассмотрим некоторые фундаментальные концепции.

Что такое сага?

Не знаю, как вы, но я не знал более широкой концепции программирования саги до того, как погрузился в библиотеку Redux. Я опишу эту концепцию, поскольку это шаблон проектирования, на котором основана Redux Saga.

В мире микросервисов сага предназначена для помощи в реализации транзакции, охватывающей несколько сервисов или бизнес-доменов. Шаблон саги разработан как способ обработки побочных эффектов в приложении. Домашняя страница Redux Saga описывает фреймворк как:

Библиотека, которая призвана упростить управление побочными эффектами приложения (т. Е. Асинхронными вещами, такими как выборка данных и нечистыми вещами, такими как доступ к кешу браузера), повысить эффективность их выполнения, легкость тестирования и улучшить обработку сбоев.

Как разработчик JavaScript, который потратил немало времени на просеивание через ад обратных вызовов, это действительно звучит хорошо для меня!

Этот доклад Кейти МакКэффри очень подробно описывает паттерн саги.

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

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

Несколько слов о функции генератора

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

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

Базовый синтаксис функции генератора выглядит так:

function* myGenerator() {
     const result = yield myAsyncFunc1()
     yield myAsyncFunc2(result)
}

Функция генератора объявляется с использованием звездочки * после ключевого слова function. Ключевое слово yield указывает функции дождаться завершения выполнения этой строки. В этом случае myGenerator будет ждать (или уступить), пока myAsyncFunc1 не завершится, после чего функция перейдет к выполнению myAsyncFunc2, передав результат, возвращенный первой функцией.

Функции генератора - это гораздо больше, чем мы обсуждали, но этого достаточно, чтобы вы ознакомились с этой статьей и основами Redux Saga.

Наш пример

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

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

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

Вы еще не проголодались? Я тоже 😋 Хорошо, поехали!

Коренная сага

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

function* rootSaga() {
  yield all([
    menuSaga(),
    checkoutSaga(),
    userSaga()
  ])
}

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

rootSaga - наша основная сага в цепочке. Это сага, которая передается в sagaMiddleware.run (rootSaga). menuSaga, checkoutSaga и userSaga - это то, что мы называем сагами срезов. Каждый из них обрабатывает один раздел (или фрагмент) нашего дерева саг.

all() - это то, что redux-saga называется создателем эффекта. По сути, это функции, которые мы используем для совместной работы наших саг (вместе с нашими функциями генератора). Каждый создатель эффекта возвращает объект (называемый эффектом), который используется redux-saga промежуточным программным обеспечением. Вы должны отметить сходство именования с действиями Redux и создателями действий.

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

Саги о наблюдателях

Теперь давайте посмотрим на базовую структуру одной из наших подсаг.

import { put, takeLatest } from 'redux-saga/effects'
function* fetchMenuHandler() {
  try {
    // Logic to fetch menu from API
  } catch (error) {
    yield put(logError(error))
  }
}
function* menuSaga() {
  yield takeLatest('FETCH_MENU_REQUESTED', fetchMenuHandler)
}

Здесь мы видим нашу menuSaga, одну из наших предыдущих саг о ломтиках. Он отслеживает различные типы действий, отправляемых в магазин. Например, когда мы хотим получить меню из нашего API, где-то в нашем приложении, будет отправлено действие с типом FETCH_MENU_REQUESTED. menuSaga наблюдает за этим типом действия, используя takeLatest, и когда он видит этот тип действия, он запускает функцию-обработчик - fetchMenuHandler. По этой причине эти типы саг называются сагами для наблюдателей. Таким образом, саги-наблюдатели отслеживают действия и запускают саги-обработчики.

Мы оборачиваем тело наших функций-обработчиков блоками try / catch, чтобы мы могли обрабатывать любые ошибки, которые могут возникнуть во время наших асинхронных процессов. Здесь мы отправляем отдельное действие, используя put(), чтобы уведомить наш магазин о любых ошибках. put() в основном redux-saga эквивалент метода dispatch от Redux.

Вы, вероятно, добавите отдельные механизмы обработки ошибок в свои собственные приложения.

const logError = error => ({
  type: 'LOG_ERROR',
  payload: { error }
})

Давайте добавим логики в fetchMenuHandler.

function* fetchMenuHandler() {
  try {
    const menu = yield call(myApi.fetchMenu)
    yield put({ type: 'MENU_FETCH_SUCCEEDED', payload: { menu } ))
  } catch (error) {
    yield put(logError(error))
  }
}

Мы собираемся использовать наш HTTP-клиент, чтобы сделать запрос к нашему API данных меню. Поскольку нам нужно вызвать отдельную асинхронную функцию (а не действие), мы используем call(). Если нам нужно передать какие-либо аргументы, мы передадим их в качестве последующих аргументов в call(), то есть call(myApi.fetchMenu, authToken). Наша функция генератора fetchMenuHandler использует yield, чтобы приостановить себя, пока myApi.fetchMenu ожидает ответа. После этого мы отправляем другое действие с put(), чтобы отобразить наше меню для пользователя.

Хорошо, давайте объединим эти концепции и создадим еще одну подсагу - checkoutSaga.

import { put, select, takeLatest } from 'redux-saga/effects'
function* itemAddedToBasketHandler(action) {
  try {
    const { item } = action.payload
    const onSaleItems = yield select(onSaleItemsSelector)
    const totalPrice = yield select(totalPriceSelector)
    if (onSaleItems.includes(item)) {
      yield put({ type: 'SALE_REACHED' })
    }
    if ((totalPrice + item.price) >= minimumOrderValue) {
      yield put({ type: 'MINIMUM_ORDER_VALUE_REACHED' })
    }
  } catch (error) {
    yield put(logError(error))
  }
}
function* checkoutSaga() {
  yield takeLatest('ITEM_ADDED_TO_BASKET', itemAddedToBasketHandler)
}

Когда товар добавляется в корзину, вы можете представить, что необходимо выполнить несколько проверок и проверок. Здесь мы проверяем, получил ли пользователь право на какие-либо продажи после добавления товара или достиг ли пользователь минимальной стоимости заказа, необходимой для размещения заказа. Помните, что Redux Saga - это инструмент, позволяющий нам справляться с побочными эффектами. Его не обязательно использовать для размещения логики, которая фактически добавляет товар в корзину. Мы бы использовали для этого редуктор, потому что для этого идеально подходит гораздо более простой шаблон редуктора.

Здесь мы используем новый эффект - select(). Select передается селектор и извлекает этот фрагмент из хранилища Redux прямо из нашей саги! Обратите внимание, что мы можем получить любую часть хранилища из наших саг, что очень полезно, когда вы зависите от нескольких контекстов в одной саге.

Что такое селектор? Селектор - это общий шаблон проектирования, используемый в Redux, где мы создаем функцию, которая передает состояние и просто возвращает небольшую часть этого состояния. Например:

const onSaleItemsSelector = state => state.onSaleItems
const basketSelector = state => state.basket
const totalPriceSelector = state => basketSelector(state).totalPrice

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

Заключение

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

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

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

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

Есть отзывы? Предложения? Просто хочешь поздороваться? Свяжитесь со мной в LinkedIn или Twitter!