Плюсы / минусы использования редукс-саги с генераторами ES6 по сравнению с редукцией с ES2017 async / wait

В последнее время в городе редукс много говорят о последнем ребенке, о своде-саге / редукс-саге . Он использует функции генератора для прослушивания / диспетчеризации.

Прежде чем я оберну свою голову вокруг него, я хотел бы знать плюсы и минусы использования redux-saga вместо подхода ниже, где я использую redux-thunk с async / await.

Компонент может выглядеть так, как обычно.

 import { login } from 'redux/auth'; class LoginForm extends Component { onClick(e) { e.preventDefault(); const { user, pass } = this.refs; this.props.dispatch(login(user.value, pass.value)); } render() { return (
); } } export default connect((state) => ({}))(LoginForm);

Затем мои действия выглядят примерно так:

 // auth.js import request from 'axios'; import { loadUserData } from './user'; // define constants // define initial state // export default reducer export const login = (user, pass) => async (dispatch) => { try { dispatch({ type: LOGIN_REQUEST }); let { data } = await request.post('/login', { user, pass }); await dispatch(loadUserData(data.uid)); dispatch({ type: LOGIN_SUCCESS, data }); } catch(error) { dispatch({ type: LOGIN_ERROR, error }); } } // more actions... 

 // user.js import request from 'axios'; // define constants // define initial state // export default reducer export const loadUserData = (uid) => async (dispatch) => { try { dispatch({ type: USERDATA_REQUEST }); let { data } = await request.get(`/users/${uid}`); dispatch({ type: USERDATA_SUCCESS, data }); } catch(error) { dispatch({ type: USERDATA_ERROR, error }); } } // more actions... 

    В редукционной саге эквивалент приведенного выше примера будет

     export function* loginSaga() { while(true) { const { user, pass } = yield take(LOGIN_REQUEST) try { let { data } = yield call(request.post, '/login', { user, pass }); yield fork(loadUserData, data.uid); yield put({ type: LOGIN_SUCCESS, data }); } catch(error) { yield put({ type: LOGIN_ERROR, error }); } } } export function* loadUserData(uid) { try { yield put({ type: USERDATA_REQUEST }); let { data } = yield call(request.get, `/users/${uid}`); yield put({ type: USERDATA_SUCCESS, data }); } catch(error) { yield put({ type: USERDATA_ERROR, error }); } } 

    Первое, что нужно заметить, это то, что мы вызываем функции api, используя yield call(func, ...args) формы yield call(func, ...args) . call не выполняет эффект, он просто создает простой объект типа {type: 'CALL', func, args} . Исполнение делегируется промежуточному программному обеспечению redux-saga, которое заботится о выполнении функции и возобновлении генератора с его результатом.

    Основное преимущество заключается в том, что вы можете протестировать генератор вне Redux с помощью простых проверок равенства

     const iterator = loginSaga() assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST)) // resume the generator with some dummy action const mockAction = {user: '...', pass: '...'} assert.deepEqual( iterator.next(mockAction).value, call(request.post, '/login', mockAction) ) // simulate an error result const mockError = 'invalid user/password' assert.deepEqual( iterator.throw(mockError).value, put({ type: LOGIN_ERROR, error: mockError }) ) 

    Обратите внимание, что мы издеваемся над результатом вызова api, просто вводя насмешливые данные в next метод iteratorа. Издевательские данные проще, чем насмехающиеся функции.

    Второе, что нужно заметить, – это призыв к yield take(ACTION) . Thunks вызывается создателем действия при каждом новом действии (например, LOGIN_REQUEST ). т.е. действия постоянно подталкиваются к thunks, и thunks не имеют контроля над тем, когда перестать обрабатывать эти действия.

    В редукс-саге генераторы вытягивают следующее действие. т.е. они имеют контроль, когда слушать какое-то действие, а когда нет. В приведенном выше примере инструкции streamа помещаются внутри цикла while(true) , поэтому он будет прослушивать каждое входящее действие, которое несколько имитирует поведение нажатия thunk.

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

    • Действия пользователя LOGOUT LOGOUT

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

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

    Как бы вы реализовали это с помощью thunks; а также обеспечение полного охвата тестированием всего streamа? Вот как это может выглядеть с Сагами:

     function* authorize(credentials) { const token = yield call(api.authorize, credentials) yield put( login.success(token) ) return token } function* authAndRefreshTokenOnExpiry(name, password) { let token = yield call(authorize, {name, password}) while(true) { yield call(delay, token.expires_in) token = yield call(authorize, {token}) } } function* watchAuth() { while(true) { try { const {name, password} = yield take(LOGIN_REQUEST) yield race([ take(LOGOUT), call(authAndRefreshTokenOnExpiry, name, password) ]) // user logged out, next while iteration will wait for the // next LOGIN_REQUEST action } catch(error) { yield put( login.error(error) ) } } } 

    В приведенном выше примере мы выражаем наше требование параллелизма с использованием race . Если take(LOGOUT) выигрывает гонку (т.е. пользователь нажал кнопку выхода). Гонка автоматически отменяет фоновые задания authAndRefreshTokenOnExpiry . И если authAndRefreshTokenOnExpiry был заблокирован в середине call(authorize, {token}) вызов также будет отменен. Отмена распространяется автоматически.

    Вы можете найти runnable demo вышеупомянутого streamа

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

    Pro (используя сагу):

    • Тестируемость. Это очень легко проверить саги, так как call () возвращает чистый объект. Тестирование thunks обычно требует, чтобы вы включили mockStore в свой тест.

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

    • Sagas предлагает независимое место для обработки всех побочных эффектов. В моем опыте, как правило, легче модифицировать и управлять, чем действия thunk.

    Против:

    • Синтаксис генератора.

    • Много концепций, чтобы учиться.

    • Стабильность API. Кажется, редукс-сага все еще добавляет функции (например, каналы?), И сообщество не так велико. Существует проблема, если библиотека однажды сделает обновление, не поддерживающее обратную совместимость.

    Я просто хотел бы добавить некоторые комментарии из моего личного опыта (используя как саги, так и thunk):

    Саги отлично справляются:

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

    Саги более мощные. Все, что вы можете сделать в создателе действия одного thunk, вы также можете делать в одной саге, но не наоборот (или, по крайней мере, не легко). Например:

    • дождитесь действия / действия, которые нужно отправить ( take )
    • отменить существующую процедуру ( cancel , takeLatest , race )
    • несколько процедур могут прослушивать одно и то же действие ( take , takeEvery , …)

    Sagas также предлагает другие полезные функции, которые обобщают некоторые общие шаблоны приложений:

    • channels для прослушивания внешних источников событий (например, websockets)
    • вилка модель ( fork , spawn )
    • дроссель

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

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

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

    Я видел несколько проектов, в которых торны обрабатывались так, как если бы они были controllerами из MVC patten, и это быстро становится незаметным беспорядком.

    Мой совет – использовать Sagas, где вам нужны три типа B типа, относящиеся к одному событию. Для всего, что можно было бы разрезать по нескольким действиям, я считаю, что проще написать клиентское ПО и использовать мета-свойство действия FSA для его запуска.

    Вот проект, в котором сочетаются лучшие части (как), так и redux-saga и redux-thunk : вы можете справиться со всеми побочными эффектами на сагах, получая promise, dispatching соответствующее действие: https://github.com/diegohaz/ перевождь-сага-преобразователь

     class MyComponent extends React.Component { componentWillMount() { // `doSomething` dispatches an action which is handled by some saga this.props.doSomething().then((detail) => { console.log('Yaay!', detail) }).catch((error) => { console.log('Oops!', error) }) } } 

    Более простой способ – использовать redux-auto .

    из документарного материала

    redux-auto исправила эту асинхронную проблему, просто создав функцию «действие», которая возвращает promise. Для сопровождения вашей логики действий по умолчанию.

    1. Нет необходимости в другом промежуточном программном обеспечении Redux async. например, удар, promise-промежуточное ПО, сага
    2. Легко позволяет вам сдать promise на сокращение, и вам это удалось.
    3. Позволяет совместно размещать внешние вызовы службы, где они будут преобразованы
    4. Именование файла «init.js» вызовет его один раз при запуске приложения. Это полезно для загрузки данных с сервера при запуске

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

    Он также автоматически присоединяет вспомогательный объект (называемый «async») к прототипу вашего состояния, позволяя отслеживать в пользовательском интерфейсе, запрошенные переходы.

    Одно быстрое замечание. Генераторы отменяются, асинхронно / ждут – нет. Итак, для примера из вопроса, на самом деле не имеет смысла, что выбрать. Но для более сложных streamов иногда нет лучшего решения, чем использование генераторов.

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

    И, конечно же, генераторы легче тестировать.

    Давайте будем гением компьютера.