Плюси / мінуси використання redux-saga з генераторами ES6 проти redux-thunk з ES2017 async / очікувати


488

Зараз багато говорять про останню дитину в місті редукс, редукс-сагу / редукс-сагу . Він використовує функції генератора для прослуховування / диспетчеризації дій.

Перш ніж я обернути голову навколо цього, я хотів би дізнатися плюси та мінуси використання redux-sagaзамість підходу нижче, де я використовую redux-thunkasync / очікувати.

Компонент може виглядати так, диспетчерські дії, як зазвичай.

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 (<div>
        <input type="text" ref="user" />
        <input type="password" ref="pass" />
        <button onClick={::this.onClick}>Sign In</button>
    </div>);
  } 
}

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...

6
Дивіться також мою відповідь, де порівнюють redux-thunk з redux-saga тут: stackoverflow.com/a/34623840/82609
Sebastien Lorber,

22
Що ::перед тим, як this.onClickзробити?
Downhillski

37
@ZhenyangHua - це короткочасний зв'язок функції з об'єктом ( this), ака this.onClick = this.onClick.bind(this). Більш довгу форму зазвичай рекомендується робити в конструкторі, оскільки коротка рука повторно прив'язується до кожного візуалізації.
hampusohlsson

7
Розумію. Дякую! Я бачу людей, які використовують bind()багато для переходу thisдо функції, але я почав використовувати () => method()зараз.
Downhillski

2
@Hosar Я деякий час використовував у виробництві redux & redux-saga, але фактично перейшов на MobX через пару місяців, тому що менше накладних витрат
hampusohlsson

Відповіді:


461

У редукс-сазі еквівалент наведеного вище прикладу був би

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). 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метод ітератора. Знущання з даних набагато простіше, ніж функції знущання.

Друге, що слід помітити - це заклик до yield take(ACTION). Thunks викликає творець дії щодо кожної нової дії (наприклад LOGIN_REQUEST). тобто дії постійно підштовхуються до гронів, і грона не мають контролю над тим, як припинити обробляти ці дії.

У Redux-сазі, генератори тягнути наступна дія. тобто вони мають контроль, коли слухати якусь дію, а коли ні. У наведеному вище прикладі вказівки потоку розміщуються всередині while(true)циклу, тож вони прослуховуватимуть кожну вхідну дію, яка дещо імітує поведінку натискання грона.

Підхід натягу дозволяє реалізувати складні потоки управління. Припустимо, наприклад, ми хочемо додати наступні вимоги

  • Обробляйте дії користувача LOGOUT

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

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

Як би ти це реалізував із грозами; при цьому також забезпечується повне покриття тесту для всього потоку? Ось як це може виглядати з Sagas:

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})дзвінка було заблоковано, воно також буде скасовано. Скасування поширюється вниз автоматично.

Ви можете знайти демонстраційну версію вищезазначеного потоку


@yassine звідки ця delayфункція? Ах, знайшли це: github.com/yelouafi/redux-saga/blob/…
philk

122
redux-thunkКод цілком читаємо і самостійно пояснити. Але redux-sagasодин дійсно нечитабельним, в основному з - за цих дієслів-подібних функцій: call, fork, take, put...
SYG

11
@syg, я погоджуюсь, що дзвінок, розсилка, перевезення та розміщення можуть бути більш семантично привітними. Однак саме ті дієсловоподібні функції дозволяють перевірити всі побічні ефекти.
Downhillski

3
@syg як і раніше функція з тими дивними функціями дієслів є більш читабельною, ніж функція з ланцюгом глибоких обіцянок
Ясер Сінджаб

3
ці "дивні" дієслова також допомагають вам зрозуміти відношення саги до повідомлень, що виходять із надмірних. ви можете виводити типи повідомлень із скорочення - часто, щоб викликати наступну ітерацію, і ви можете повернути нові повідомлення для передачі результату вашого побічного ефекту.
worc

104

Я додам свій досвід використання саги у виробничій системі на додаток до досить ґрунтовної відповіді автора бібліотеки.

Про (за допомогою саги):

  • Заповітність. Тестувати саги дуже просто, оскільки call () повертає чистий об'єкт. Для тестування громозахисів зазвичай потрібно включити mockStore до свого тесту.

  • Редукс-сага поставляється з великою кількістю корисних допоміжних функцій щодо завдань. Мені здається, що концепція саги полягає у створенні якогось фонового працівника / потоку для вашої програми, які виступають як відсутній фрагмент в реагуваній архітектурі редукцій (діїCreators і редуктори повинні бути чистими функціями.), Що призводить до наступного моменту.

  • Саги пропонують незалежне місце для боротьби з усіма побічними ефектами. Зазвичай, мій досвід змінювати та керувати, ніж обтяжливі дії.

Con:

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

  • Багато понять, які слід вивчити.

  • Стабільність API. Здається, редукс-сага все ще додає функції (наприклад, канали?), А спільнота не така велика. Виникає занепокоєння, якщо бібліотека зробить оновлення, яке не відповідає сумісності, одного дня.


9
Просто хочу зробити якийсь коментар, творець дії не повинен бути чистою функцією, про що багато разів заявляв сам Дан.
Марсон Мао

14
На сьогоднішній день редукс-саги дуже рекомендуються в міру використання та розширення співтовариства. Також API став більш зрілим. Розгляньте видалення Con for API stabilityяк оновлення для відображення поточної ситуації.
Деніалос

1
У саги більше запусків, ніж у грудей, і її остання фіксація також після
грому

2
Так, FWIW у редук-сазі зараз 12 тис. Зірок, редукс-тунк має 8 к
Брайан Бернс

3
Я хочу додати ще один виклик саг - це те, що саги за замовчуванням повністю відокремлюються від творців і дій. Хоча Thunks безпосередньо пов'язують творців дії зі своїми побічними ефектами, саги залишають творців дій повністю відокремленими від саг, які їх слухають. Це має технічні переваги, але може зробити код набагато складнішим у дотриманні та може розмити деякі односпрямовані поняття.
Theaceofthespade

33

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

Саги чудово перевіряють:

  • Вам не потрібно знущатися над функціями, оберненими ефектами
  • Тому тести є чистими, читабельними та легкими для запису
  • Використовуючи саги, творці дій здебільшого повертають простолітеральні об'єкти. Це також простіше перевірити і стверджувати, на відміну від обіцянок грона.

Саги більш потужні. Все, що ти можеш зробити за допомогою одного творець дії, ви також можете зробити в одній сазі, але не навпаки (або, принаймні, не просто). Наприклад:

  • дочекайтеся розсилки дії / дії ( take)
  • скасувати існуючу процедуру ( cancel, takeLatest, race)
  • кілька підпрограм можуть слухати ту саму дію ( take, takeEvery, ...)

Sagas також пропонує інші корисні функціональні можливості, які узагальнюють деякі загальні моделі застосування:

  • channels слухати на зовнішніх джерелах подій (наприклад, веб-розетки)
  • модель виделки ( fork, spawn)
  • дросель
  • ...

Саги - чудовий і потужний інструмент. Однак з владою приходить відповідальність. Коли ваша заявка зростає, ви можете легко втратити, з’ясувавши, хто чекає на розсилання дії або що все відбувається, коли якась дія надсилається. З іншого боку, грона простіша і простіша. Вибір того чи іншого залежить від багатьох аспектів, таких як тип і розмір проекту, які типи побічних ефектів повинен обробляти ваш проект або переваги команди розробників. У будь-якому випадку просто тримайте додаток простим і передбачуваним.


8

Просто певний особистий досвід:

  1. Для стилю кодування та читабельності однією з найбільш значущих переваг використання редук-саги в минулому є уникання зворотного зворотного виклику в режимі редук-тунк - ніколи більше не потрібно використовувати багато вкладень / ловити. Але тепер, маючи популярність async / await thunk, можна також писати код асинхронізації в стилі синхронізації при використанні функції redux-thunk, що може розцінюватися як поліпшення в redux-think.

  2. Можливо, вам доведеться написати набагато більше кодового коду при використанні редукс-саги, особливо в Typescript. Наприклад, якщо ви хочете реалізувати функцію асинхронізації вибору, дані та обробку помилок можуть бути безпосередньо виконані в одній грохотній одиниці в action.js з однією єдиною дією FETCH. Але в redux-saga, можливо, потрібно буде визначити дії FETCH_START, FETCH_SUCCESS і FETCH_FAILURE та всі їх пов’язані з цим перевірки типу, тому що одна з особливостей у redux-saga полягає у використанні такого роду багатого механізму “token” для створення ефектів та інструктажу магазин для скорочення для легкого тестування. Звичайно, можна було б написати сагу, не використовуючи цих дій, але це зробить її схожою на громовідвід.

  3. Що стосується структури файлів, скорочення-сага здається більш явним у багатьох випадках. Можна легко знайти код, пов’язаний з асинхронією, у кожному sagas.ts, але в режимі redux-thunk потрібно було б бачити його в діях.

  4. Легке тестування може бути ще однією зваженою особливістю в редукс-сазі. Це справді зручно. Але одне, що потрібно уточнити, це те, що тест "виклик" redux-saga не здійснював би фактичний виклик API під час тестування, тому потрібно було б вказати зразок результату для етапів, які можуть використовувати його після виклику API. Тому перед тим, як писати редукс-сагу, краще було б детально спланувати сагу та її відповідні sagas.spec.ts.

  5. Redux-saga також пропонує безліч вдосконалених функцій, таких як виконання завдань паралельно, помічники в одночасності, такі як takeLatest / takeEvery, fork / spawn, які набагато потужніші, ніж громи.

На закінчення особисто я хотів би сказати: у багатьох звичайних випадках та невеликих та середніх розмірах додатків виконайте стилі async / очікуйте стилю redux-thunk. Це дозволить заощадити багато кодів / дій / typedefs, і вам не потрібно буде перемикатися на багато різних sagas.ts та підтримувати певне дерево саг. Але якщо ви розробляєте великий додаток із значно складною логікою асинхронізації та потребуєте таких функцій, як паралельність / паралельний малюнок, або маєте високий попит на тестування та технічне обслуговування (особливо в тестовій розробці), redux-sagas, можливо, врятує ваше життя .

У будь-якому випадку, редукс-сага не є складнішою і складнішою, ніж сама редукція, і вона не має так званої крутої кривої навчання, оскільки має чітко обмежені основні поняття та API. Витратити невелику кількість часу на навчання редукс-саги може принести користь собі один день у майбутньому.


5

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

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

Я бачив пару проектів, в яких громовідвід ставляться так, ніби вони є контролерами з патенту MVC, і це швидко стає непорушним безладом.

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


2

Громадять проти Сагаса

Redux-Thunk і Redux-Saga відрізняються кількома важливими способами, обидві - це бібліотеки середнього програмного забезпечення для Redux (Redux middleware - це код, який перехоплює дії, що надходять у магазин за допомогою методу dispatch ()).

Дія може бути буквально будь-якою, але якщо ви дотримуєтесь кращих практик, дія - це звичайний об’єкт javascript з полем типу та необов'язкові поля корисного навантаження, мета та помилки. напр

const loginRequest = {
    type: 'LOGIN_REQUEST',
    payload: {
        name: 'admin',
        password: '123',
    }, };

Redux-Thunk

Крім диспетчеризації стандартних дій, Redux-Thunkсереднє програмне забезпечення дозволяє відправляти спеціальні функції, званіthunks .

Thunks (в Redux), як правило, має таку структуру:

export const thunkName =
   parameters =>
        (dispatch, getState) => {
            // Your application logic goes here
        };

Тобто, a thunk- це функція, яка (необов'язково) приймає деякі параметри і повертає іншу функцію. Внутрішня функція приймає a dispatch functionі getStateфункцію - обидві вони будуть постачатися Redux-Thunkпроміжним програмним забезпеченням.

Редукс-Сага

Redux-SagaПрограмне забезпечення дозволяє виражати складну логіку програми як чисті функції, що називаються сагами. Чисті функції бажані з точки зору тестування, оскільки вони передбачувані та повторювані, що робить їх порівняно легкими для тестування.

Саги реалізуються за допомогою спеціальних функцій, званих генераторними функціями. Це нова особливість ES6 JavaScript. В основному, виконання скаче в генератор і виходить скрізь, де ви бачите заяву про вихід. Подумайте про yieldте, що генератор призупиняє та повертає отримане значення. Пізніше абонент може відновити генератор за заявою, що слідує за yield.

Функція генератора така, як це визначено. Помітьте зірочку після ключового слова функції.

function* mySaga() {
    // ...
}

Після реєстрації саги про вхід Redux-Saga. Але тоді перехід на yieldперший рядок призупинить сагу, поки дія з типом не 'LOGIN_REQUEST'буде відправлено в магазин. Як тільки це станеться, виконання буде продовжено.

Детальніше дивіться у цій статті .


1

Одна швидка примітка. Генератори можна скасувати, асинхронізувати / очікувати - ні. Отже, для прикладу з питання, насправді не має сенсу, що вибрати. Але для більш складних потоків іноді немає кращого рішення, ніж використання генераторів.

Так, ще одна ідея може бути - використовувати генератори з редукційними, але для мене, схоже, намагаються винайти велосипед з квадратними колесами.

І звичайно, генератори простіше перевірити.


0

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

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)
    })
  }
}

1
використання then()всередині компонента React проти парадигми. Вам слід керуватися зміненим станом, componentDidUpdateа не чекати, коли обіцянка буде вирішена.

3
@ Maxincredible52 Це неправда для серверного візуалізації.
Дієго Хаз

На мій досвід, точка Макса все ще справедлива для візуалізації на стороні сервера. З цим, мабуть, слід звертатися десь у шарі маршрутизації.
ThinkingInBits

3
@ Maxincredible52 чому це проти парадигми, де ти це читав? Я, як правило, подібний до @Diego Haz, але роблю це в компоненті компонентаDidMount (згідно з документами React, тим більше слід робити мережеві дзвінки), тому у нас єcomponentDidlMount() { this.props.doSomething().then((detail) => { this.setState({isReady: true})} }
user3711421

0

Найпростіший спосіб - використовувати функцію redux-auto .

від документальноїазії

redux-auto виправляв цю асинхронну проблему, просто дозволяючи створити функцію "дії", яка повертає обіцянку. Щоб супроводжувати вашу логіку дій "за замовчуванням" функції.

  1. Немає потреби в іншому програмному забезпеченні Redux async. наприклад, заграти, обіцяти, прошивку, сагу
  2. Легко дозволяє передати обіцянку в редукс і домогтися цього
  3. Дозволяє знаходити зовнішні виклики служб із тим, куди вони будуть перетворені
  4. Іменуючи файл "init.js", його буде викликано один раз при запуску програми. Це добре для завантаження даних із сервера на початку

Ідея полягає в тому, щоб кожна дія була у певному файлі . спільне розміщення виклику сервера у файлі з функціями скорочення для "очікування", "виконано" та "відхилено". Це робить обробку обіцянок дуже простою.

Він також автоматично приєднує допоміжний об’єкт (званий "асинхронізація") до прототипу вашого штату, що дозволяє відстежувати в інтерфейсі користувача запитувані переходи.


2
Я зробив +1, навіть якщо це відповідь не має значення, тому що слід розглянути і різні рішення
вчора,

12
Я думаю, що там є, тому що він не повідомив, що він є автором проекту
jreptak
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.