ОБНОВЛЕНИЕ Август / 2017 г .:

ОБНОВЛЕНИЕ апрель 2017 г .:

  • Спасибо Эдуардо за перевод этой статьи на португальский, найди здесь.
  • Также этот неизвестный парень, который перевел эту статью на китайский, найди здесь.

Несколько дней назад мой коллега говорил об управлении асинхронными операциями. Он использовал несколько инструментов для расширения возможностей redux. Слушая его, мы действительно осознали реальность усталости от JavaScript.

Посмотрим правде в глаза: если вы привыкли выполнять свою работу и использовать технологии, основанные на ваших потребностях, а не ради самих технологий, создание экосистемы React может оказаться трудным и трудоемким.

Последние два года я работал над проектами Angular и наслаждался новейшими моделями Model-View-Controller. И я должен сказать, что - даже если кривая обучения была проблемой, исходящей от Backbone.js, изучение Angular действительно окупилось. У меня появилась работа получше, и у меня была возможность сотрудничать в интересных проектах. Я многому научился у сообщества поддержки Angular.

Это были действительно крутые дни, но, ну, The Fatigue Must Go On (заявка на товарный знак находится на рассмотрении), и я продолжаю заниматься модой: React, Redux и Sagas.

Несколько лет назад я наткнулся на статью Томаса Бурлесона Сглаживание цепочек обещаний. Я многому научился, читая это. Даже два года спустя я все еще вспоминаю многие из этих идей.

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

Отказ от ответственности: я буду работать с тем же сценарием и расширять его, я надеюсь (если мне повезет) организовать обсуждение обоих подходов. Я предполагаю, что читатель имеет некоторое базовое представление о Promises, React, Redux и (боже!) ... JavaScript.

Перво-наперво.

По словам создателя редукс-саги Яссин Элуафи:

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

По сути, это вспомогательная библиотека, которая позволяет нам организовывать все асинхронные и распределенные операции на основе Sagas и генераторов функций ES6. Если вы хотите узнать больше о самом шаблоне Saga, Caitie McCaffrey отлично поработала в этом видео и многом другом о генераторах функций. Посмотрите это бесплатное видео Egghead (по крайней мере, оно было бесплатным, когда я разместил эту статью).

Кейс с приборной панелью

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

Сценарий выглядит так:

Как мы видим, последовательность из трех API вызывает: getDeparture - ›getFlight -› getForecast, поэтому наш служебный класс API выглядит так:

class TravelServiceApi {
 static getUser() {
   return new Promise((resolve) => {
     setTimeout(() => {
       resolve({
            email : "[email protected]",
            repository: "https://github.com/username"
       });
     }, 3000);
   });
 }
 static getDeparture(user) {
  return new Promise((resolve) => {
   setTimeout(() => {
    resolve({
      userID : user.email,
      flightID : “AR1973”,
      date : “10/27/2016 16:00PM”
     });
    }, 2500);
   });
 }
 static getForecast(date) {
  return new Promise((resolve) => {
      setTimeout(() => {
        resolve({
            date: date,
            forecast: "rain"
        });
      }, 2000);
   });
  }
}

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

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

const dashboard = (state = {}, action) => {
 switch(action.type) {
  case ‘FETCH_DASHBOARD_SUCCESS’:
  return Object.assign({}, state, action.payload);
  default :
  return state;
 }
};

Мы используем свой редуктор для каждой панели с тремя разными сценариями, которые предоставляют компоненту доступ к части пользователя с помощью функции redux StateToProps:

const mapStateToProps =(state) => ({
 user : state.user,
 dashboard : state.dashboard
});

После того, как все настроено (да, я знаю, что не объяснял много вещей, но я хочу сосредоточиться только на сагах…), мы готовы к игре!

Покажи мне саги

Уильям Деминг однажды сказал:

Если вы не можете описать то, что делаете, как процесс, значит, вы не знаете, что делаете.

Хорошо, давайте создадим пошаговый процесс работы с Redux Saga.

1. Зарегистрируйте саги.

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

Для начала нам нужно создать наш генератор саг и зарегистрировать их:

function* rootSaga() {
  yield[
    fork(loadUser),
    takeLatest('LOAD_DASHBOARD', loadDashboardSequenced)
  ];
}

Сага Redux предоставляет несколько методов, называемых Эффекты, мы собираемся определить несколько из них:

  • Вилка выполняет неблокирующую операцию с переданной функцией.
  • Сделайте паузу, пока не будет получено действие.
  • Гонка запускает эффекты одновременно, а затем отменяет их все, когда один из них завершается.
  • Вызов выполняет функцию. Если он возвращает обещание, приостанавливает сагу до тех пор, пока обещание не будет выполнено.
  • Put отправляет действие.
  • Выбрать. Запускает функцию выбора для получения данных о состоянии.
  • takeLatest означает, что мы собираемся выполнить операции, а затем вернуть только результаты последнего вызова. Если мы инициируем несколько обращений, он проигнорирует все, кроме последнего.
  • takeEvery вернет результаты для всех инициированных вызовов.

Мы только что зарегистрировали две разные саги. Мы собираемся определить их позже. Пока что мы берем один для пользователя, использующего fork, и другой takeLatest, который будет ждать выполнения действия под названием «LOAD_DASHBOARD». выполнен. Подробнее см. В шаге 3.

2. Вставьте промежуточное ПО Saga в магазин Redux.

Когда мы определяем хранилище Redux и инициализируем его, большую часть времени он будет выглядеть так:

const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, [], compose(
      applyMiddleware(sagaMiddleware)  
);
sagaMiddleware.run(rootSaga); /* inject our sagas into the middleware*/

3. Создайте саги.

Сначала мы собираемся определить последовательность саги loadUser:

function* loadUser() {
  try {
   //1st step
    const user = yield call(getUser);
   //2nd step
    yield put({type: 'FETCH_USER_SUCCESS', payload: user});
  } catch(error) {
    yield put({type: 'FETCH_FAILED', error});
  }
}

Мы можем прочитать это так:

  • Сначала вызовите функцию getUser и присвойте результат пользователю const .
  • Позже отправьте действие FETCH_USER_SUCCESS и передайте значение пользователя, которое будет использовано магазином.
  • Если что-то пойдет не так, отправьте действие FETCH_FAILED.

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

Теперь мы собираемся создать последовательную сагу:

function* loadDashboardSequenced() {
 try {
  
  yield take(‘FETCH_USER_SUCCESS’);
  const user = yield select(state => state.user);
  
  const departure = yield call(loadDeparture, user);
  const flight = yield call(loadFlight, departure.flightID);
  const forecast = yield call(loadForecast, departure.date);
  yield put({type: ‘FETCH_DASHBOARD_SUCCESS’, payload: {forecast,  flight, departure} });
  } catch(error) {
    yield put({type: ‘FETCH_FAILED’, error: error.message});
  }
}

Мы можем прочитать сагу следующим образом:

  • Дождитесь отправки действия FETCH_USER_SUCCESS. Это в основном будет приостановлено, пока событие не вызовет его. Для этого мы используем действие.
  • Берём стоимость из магазина. Эффект select получает функцию, которая имеет доступ к магазину. Мы назначаем информацию о пользователе постоянному пользователю.
  • Мы выполняем асинхронную операцию для загрузки информации об отправлении и передаем пользователя в качестве параметра с помощью call Effect.
  • После завершения loadDeparture мы выполняем loadFlight с объектом отправления, полученным в предыдущей операции.
  • То же самое относится и к прогнозу, нам нужно дождаться загрузки рейса, чтобы выполнить следующий call эффект.
  • Наконец, когда все операции завершены, мы используем эффект put для отправки и действия в хранилище и отправляем все аргументы, используя информацию, загруженную в течение всей саги.

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

Довольно аккуратно, а?

Теперь давайте рассмотрим другой случай. Учтите, что getFlight и getForecast могут запускаться одновременно. Им не нужно завершать одну, чтобы начать другую, поэтому мы можем создать другую панель для этого случая.

Неблокирующая сага

Чтобы выполнить две неблокирующие операции, нам нужно немного изменить нашу предыдущую сагу:

function* loadDashboardNonSequenced() {
  try {
    //Wait for the user to be loaded
    yield take('FETCH_USER_SUCCESS');
    //Take the user info from the store
    const user = yield select(getUserFromState);
    //Get Departure information
    const departure = yield call(loadDeparture, user);
    //Here is when the magic happens
    const [flight, forecast] = yield [call(loadFlight, departure.flightID), call(loadForecast, departure.date)];
    //Tell the store we are ready to be displayed
    yield put({type: 'FETCH_DASHBOARD2_SUCCESS', payload: {departure, flight, forecast}});
} catch(error) {
    yield put({type: 'FETCH_FAILED', error: error.message});
  }
}

Мы должны зарегистрировать доходность как массив:

const [flight, forecast] = yield [call(loadFlight, departure.flightID), call(loadForecast, departure.date)];

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

Затем нам нужно зарегистрировать сагу в rootSaga:

function* rootSaga() {
  yield[
    fork(loadUser),
    takeLatest('LOAD_DASHBOARD', loadDashboardSequenced),
    takeLatest('LOAD_DASHBOARD2' loadDashboardNonSequenced)
  ];
}

Что, если нам нужно обновить пользовательский интерфейс, как только операция будет завершена?

Не волнуйся - я тебя поддержу.

Непоследовательные и неблокирующие саги

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

Шаг №1. Мы разделяем прогноз и сагу о полете. Оба они зависят от отъезда.

/* **************Flight Saga************** */
function* isolatedFlight() {
  try {
    /* departure will take the value of the object passed by the put*/
    const departure = yield take('FETCH_DEPARTURE3_SUCCESS');
 
    const flight = yield call(loadFlight, departure.flightID);
 
    yield put({type: 'FETCH_DASHBOARD3_SUCCESS', payload: {flight}});
  } catch (error) {
    yield put({type: 'FETCH_FAILED', error: error.message});
  }
}

/* **************Forecast Saga************** */
function* isolatedForecast() {
    try {
      /* departure will take the value of the object passed by the put*/
     const departure = yield take('FETCH_DEPARTURE3_SUCCESS');
     const forecast = yield call(loadForecast, departure.date);
     
     yield put({type: 'FETCH_DASHBOARD3_SUCCESS', payload: { forecast, }});
} catch(error) {
      yield put({type: 'FETCH_FAILED', error: error.message});
    }
}

Заметили здесь что-то очень важное? Вот как мы строим наши саги:

  • Оба они ждут начала одного и того же события действия (FETCH_DEPARTURE3_SUCCESS).
  • Они получат значение при срабатывании этого события. Подробнее об этом на следующем шаге.
  • Они выполнят свою асинхронную операцию, используя Эффект вызова , и оба вызовут одно и то же событие после завершения. Но они оба отправляют в магазин разные данные. Благодаря мощности Redux мы можем сделать это без каких-либо изменений в нашем редукторе.

Шаг № 2. Давайте внесем изменения в последовательность отправления и убедимся, что она отправляет значение отправления вместе с двумя другими сагами:

function* loadDashboardNonSequencedNonBlocking() {
  try {
    //Wait for the action to start
    yield take('FETCH_USER_SUCCESS');
    //Take the user info from the store
    const user = yield select(getUserFromState);
    //Get Departure information
    const departure = yield call(loadDeparture, user);
    //Update the store so the UI get updated
    yield put({type: 'FETCH_DASHBOARD3_SUCCESS', payload: { departure, }});
    //trigger actions for Forecast and Flight to start...
    //We can pass and object into the put statement
    yield put({type: 'FETCH_DEPARTURE3_SUCCESS', departure});
  } catch(error) {
    yield put({type: 'FETCH_FAILED', error: error.message});
  }
}

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

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

В производственном приложении я, вероятно, поступил бы немного иначе. Я просто хотел указать, что вы можете передавать значения при использовании эффекта put.

А как насчет тестирования?

Вы тестируете свой код… верно?

Саги легко тестировать, но они связаны с вашими шагами, установлены в последовательности из-за природы генераторов. Давайте посмотрим на пример. (И не стесняйтесь проверять весь тест в репо в папке sagas):

describe('Sequenced Saga', () => {
  const saga = loadDashboardSequenced();
  let output = null;
it('should take fetch users success', () => {
      output = saga.next().value;
      let expected = take('FETCH_USER_SUCCESS');
      expect(output).toEqual(expected);
  });
it('should select the state from store', () => {
      output = saga.next().value;
      let expected = select(getUserFromState);
      expect(output).toEqual(expected);
  });
it('should call LoadDeparture with the user obj', (done) => {
    output = saga.next(user).value;
    let expected = call(loadDeparture, user);
    done();
    expect(output).toEqual(expected);
  });
it('should Load the flight with the flightId', (done) => {
    let output = saga.next(departure).value;
    let expected = call(loadFlight, departure.flightID);
    done();
    expect(output).toEqual(expected);
  });
it('should load the forecast with the departure date', (done) => {
      output = saga.next(flight).value;
      let expected = call(loadForecast, departure.date);
      done();
      expect(output).toEqual(expected);
    });
it('should put Fetch dashboard success', (done) => {
       output = saga.next(forecast, departure, flight ).value;
       let expected = put({type: 'FETCH_DASHBOARD_SUCCESS', payload: {forecast, flight, departure}});
       const finished = saga.next().done;
       done();
       expect(finished).toEqual(true);
       expect(output).toEqual(expected);
    });
});
  1. Убедитесь, что вы импортировали все помощники эффектов и функций, которые собираетесь тестировать.
  2. Когда вы сохраняете значение в yield, вам необходимо передать фиктивные данные следующей функции. Обратите внимание на третий, четвертый и пятый тест.
  3. За сценой каждый генератор переходит к следующей строке после yield при вызове следующего метода. Вот почему мы используем здесь saga.next (). Value.
  4. Эта последовательность высечена в камне. Если вы измените этапы саги, тест не пройдет.

Заключение.

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

Я обнаружил, что thunks проще внедрять и поддерживать. Но для более сложных операций Redux-Saga действительно отлично справляется.

Еще раз благодарю Томаса за вдохновение для этого поста. Я надеюсь, что кто-то найдет в этом посте столько же вдохновения, сколько я в его :).

Если у вас есть какие-либо вопросы, не стесняйтесь твитнуть мне. Я рада помочь.

Если вас больше интересует эта тема, не стесняйтесь проверить часть 2 этой серии Общие паттерны Redux-saga.



Наконец, не стесняйтесь проверить мои проекты с открытым исходным кодом на данный момент: