Один из самых хрупких, неудобных и нечитабельных кодов, которые вы напишете, - это управление потоком. - Беовульф

Так в чем же проблема?

Смотреть. Вы знаете, как это бывает.

Просто вызовите API и получите данные. Все просто, правда?

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

И мы также должны обрабатывать ошибки сервера, потому что бэкэнд-команда использует PHP (zing!).

Затем, как только мы получим данные, давайте передадим их редукторам Redux. Но давайте сначала обновим несколько кешированных точек данных. И получите несколько изображений, чтобы пользовательский интерфейс стал немного более быстрым, когда пользователь начнет прокрутку.

Но перед всем этим поставьте экран загрузки. И как только мы закончим, отбросьте это. Да, и обновите наши показатели отслеживания использования.

Какой беспорядок.

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

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

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

Используете ли вы обещания, обратные вызовы, блоки try / catch или старый добрый if утверждения, саги помогут превратить эти истории в инструкции.

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

Что не так с тем, что я уже делаю?

Если вы новичок в сагах (но не в Redux), вероятно, ваши создатели действий выполняют часть вашего управления потоком через redux-thunk. Это может быть сложно проверить, но знаете что? Оно работает.

// an action creator with thunking
function createRequest () {
  return function (dispatch, getState) {
    dispatch({ type: 'REQUEST_STUFF' })
    someApiCall(function(response) {
      // some processing
      dispatch({ type: 'RECEIVE_STUFF' })
    }
  }
}

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

function onHandlePress () {
  this.props.dispatch({ type: 'SHOW_WAITING_MODAL' })
  this.props.dispatch(createRequest())
}

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

Код везде.

Саги могут быть выходом из этого разрастания, поэтому вы запускаете свои потоки одним действием.

function onHandlePress () {
  this.props.dispatch(createRequest())
}

И какие бы шаги ни были задействованы, они централизованы внутри саги.

Все еще немного нечетко, правда?

Как они выглядят?

Саги реализованы как функции генератора. Функции с пользой.

function *hello() {
}

* произносится как суперзвезда. Факт.

Последовательность шагов, составляющих вашу сагу, меняется по ходу дела.

function *hello() {
  yield 'step 1 — cut a hole in a box'
  yield 'step 2 ...'
}

Набор инструментов redux-saga содержит несколько полезных вещей, которые вы можете получить. Некоторые из них носят общий характер (call), некоторые - специфичны для Redux (put и take), а некоторые из них являются дурацкими нитевидными вещами. (вилка, присоединение, отмена, гонка).

Помните тот полностью надуманный пример из прошлого? Ну, его части могут выглядеть примерно так:

function *hello() {
  yield take('BEGIN_REQUEST')
  yield put({ type: 'SHOW_WAITING_MODAL' })
  const response = yield call(myApiFunctionThatWrapsFetch)
  yield put({ type: 'PRELOAD_IMAGES', response.images })
  yield put({ type: 'USAGE_TRACK', activity: 'API_CALL'})
  yield put({ type: 'HIDE_WAITING_MODAL' })
}

Хорошо, я только что устроил тебе засаду. Да, доходность уродливая. Да, * странный. Без этого странного синтаксиса у вас останется 6 однострочников, которые делают следующее:

  1. Подождите, пока через Redux будет выполнено действие BEGIN_REQUEST.
  2. Отправьте действие SHOW_WAITING_MODAL через Redux.
  3. Вызовите API и получите ответ.
  4. Отправьте действие через Redux, чтобы кто-нибудь начал загрузку изображений.
  5. Видите здесь узор?
  6. Да, держу пари.

Неплохо, а?

Мы только что описали красивый поток.

Как им управлять?

Саги - это промежуточное ПО Redux. Когда вы настраиваете хранилище Redux, ваши саги вводятся в хранилище с помощью функции sagaMiddleware ().

Круто, но это не ответ на ваш вопрос, не так ли?

Вернемся на секунду к нашей саге * hello (). Представим, что мы внедрили это в наш магазин с помощью sagaMiddleware ([привет]).

Как только магазин Redux оживает, запускается hello.

Но вы помните строку 1?

yield take('BEGIN_REQUEST')

Теперь казнь прекратится. И ждать. Неблокирующий вид. Терпеливо. Скрываясь. Пока нужное сообщение не будет проходить через Redux.

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

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

function *foreverAlone () {
  while (true) {
    yield take('HI')
    yield put({ type: 'WAVE' })
  }
}

Саги тоже принимают параметр. Функция getState. Это один из способов получить информацию из вашего дерева состояний Redux.

Как их проверить?

Что ж, оказывается, put, call, take и все остальные redux-saga команды инструкции. Это функции, возвращающие объект.

{ TAKE: 'HI' }

Когда ваша сага запускается в Redux sagaMiddleware, они работают так, как вы читали. Но при запуске в модульном тесте они просто возвращают инструкции. Что вы можете проверить!

Вот пример:

test('foreverAlone', (t) => {
  const saga = foreverAlone()
  let actual = null
  let expected = null
  // test step 1
  actual = saga.next().value
  expected = take('HI')
  t.deepEqual(actual, expected, 'waits for hi action')
  // test step 2
  actual = saga.next().value
  expected = put({ type: 'WAVE' })
  t.deepEqual(actual, expected, 'dispatch a wave action')
}

Ваш генератор проходит через один выход за раз с помощью вызова next (). next () возвращает объект с .value, который показывает, что было получено.

next () также может принимать параметры. Таким образом, вы можете вводить фиктивные данные в свои генераторы, чтобы проверить логические пути ветвления (например, операторы if и блоки try / catch).

Эпилог

Для меня использование redux-saga позволило упростить весь уровень действий. Создатели экшенов сейчас в основном однострочники. Чистые функции.

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

Когда я перешел на redux-saga, внезапно мои:

  • компоненты стали проще
  • создатели экшенов сократили почти до нуля
  • редукторы вообще не менялись
  • управление потоком теперь было изолированным и тестируемым

Ознакомьтесь с библиотекой redux-saga на Github. README прекрасен. Он охватывает гораздо больше, чем я здесь.

Спасибо Яссин Элуафи за создание такой замечательной библиотеки. И спасибо Дэну Абрамову за Redux и его твит, который познакомил меня с redux-saga.

Обязательно прочтите статью Джека Сю о сагах. Также на эту тему есть пост в Riad Benguella. И то, и другое - отличное чтение.

Есть вопросы? Комментарии? Я скеллок в Твиттере. Или посетите нас в Infinite Red.