2015-10-03 23 views
38

Od dawna walczę o znalezienie rozwiązania tego problemu ...Jak radzić sobie ze złożonymi działaniami niepożądanymi w Redux?

Zajmuję się tworzeniem gier z tablicą wyników online. Gracz może się zalogować i wylogować w dowolnym momencie. Po zakończeniu gry gracz zobaczy tablicę wyników i zobaczy swoją własną rangę, a wynik zostanie przesłany automatycznie.

Tablica wyników pokazuje ranking gracza i ranking.

Screenshot

Wynik jest wykorzystywany zarówno gdy użytkownik zakończy odtwarzanie (aby przesłać wynik), a gdy użytkownik chce tylko sprawdzić ich ranking.

To gdzie logika staje się bardzo skomplikowane:

  • Jeśli użytkownik jest zalogowany, wówczas wynik zostanie przedstawiony pierwszy. Po zapisaniu nowego rekordu tablica wyników zostanie załadowana.

  • W przeciwnym razie tablica wyników zostanie natychmiast załadowana. Gracz otrzyma opcję zalogowania się lub rejestracji. Następnie wynik zostanie przesłany, a tablica wyników zostanie ponownie odświeżona.

  • Jednak, jeśli nie ma punktów do przesłania (wystarczy przejrzeć tabelę najlepszych wyników). W takim przypadku istniejący rekord gracza jest po prostu pobrany. Ale ponieważ akcja ta nie ma wpływu na tablicę wyników, zarówno tablica wyników, jak i rekord gracza powinny zostać pobrane jednocześnie.

  • Istnieje nieograniczona liczba poziomów. Każdy poziom ma inną tablicę wyników. Gdy użytkownik wyświetli tablicę wyników, wówczas użytkownik "obserwuje" tę tablicę wyników. Po zamknięciu użytkownik przestaje go obserwować.

  • Użytkownik może się zalogować i wylogować w dowolnym momencie. Jeśli użytkownik się wyloguje, ranking użytkownika powinien zniknąć, a jeśli użytkownik zaloguje się jako inne konto, informacje o rankingu dla tego konta powinny zostać pobrane i wyświetlone.

    ... ale to pobranie tej informacji powinno mieć miejsce tylko dla tablicy wyników, której użytkownik aktualnie obserwuje.

  • W przypadku operacji przeglądania wyniki powinny być buforowane w pamięci, aby użytkownik ponownie zasubskrybował tę samą tablicę wyników, nie będzie pobierania. Jeśli jednak zostanie przesłany wynik, pamięć podręczna nie powinna być używana.

  • Każda z tych operacji sieciowych może się nie udać, a gracz musi mieć możliwość ponowienia próby.

  • Te operacje powinny być atomowe. Wszystkie stany powinny być aktualizowane za jednym razem (bez stanów pośrednich).

Obecnie jestem w stanie rozwiązać to za pomocą Bacon.js (funkcjonalna reaktywna biblioteka programowania), ponieważ jest wyposażona w obsługę aktualizacji atomowej. Kod jest dość zwięzły, ale teraz jest to niechlujny, nieprzewidywalny kod spaghetti.

Zacząłem patrzeć na Redux.Więc starałem się uporządkować magazyn, i wpadł na coś takiego (w składni YAMLish):

user: (user information) 
record: 
    level1: 
    status: (loading/completed/error) 
    data: (record data) 
    error: (error/null) 
scoreboard: 
    level1: 
    status: (loading/completed/error) 
    data: 
     - (record data) 
     - (record data) 
     - (record data) 
    error: (error/null) 

Problemem staje się: gdzie mogę umieścić efekty uboczne.

W przypadku akcji bez skutków ubocznych staje się to bardzo łatwe. Na przykład w działaniu LOGOUT reduktor record może po prostu wybić wszystkie rekordy.

Jednak niektóre działania mają efekt uboczny. Na przykład, jeśli nie jestem zalogowany przed przesłaniem wyniku, to loguję się pomyślnie, akcja SET_USER zapisuje użytkownika w sklepie.

Ale ponieważ mam wynik przedstawienia, to SET_USER działanie musi również spowodować żądanie AJAX do opalania się, a jednocześnie, aby ustawić record.levelN.statusloading.

Pytanie brzmi: jak mogę oznaczać, że skutki uboczne (składanie wynik) powinno nastąpić po zalogowaniu się w sposób atomowy?

W Elm architektura, Updater może również emitować skutków ubocznych przy stosowaniu formę Action -> Model -> (Model, Effects Action), ale w Redux, to tylko (State, Action) -> State.

Z dokumentów Async Actions wynika, że ​​zaleca się umieszczenie ich w kreatorze akcji. Czy oznacza to, że logika przesyłania wyników będzie musiała zostać umieszczona w kreatorze akcji również w przypadku udanej akcji logowania?

function login (options) { 
    return (dispatch) => { 
    service.login(options).then(user => dispatch(setUser(user))) 
    } 
} 

function setUser (user) { 
    return (dispatch, getState) => { 
    dispatch({ type: 'SET_USER', user }) 
    let scoreboards = getObservedScoreboards(getState()) 
    for (let scoreboard of scoreboards) { 
     service.loadUserRanking(scoreboard.level) 
    } 
    } 
} 

Uważam to trochę dziwne, ponieważ kod odpowiedzialny za tę reakcję łańcuchową istnieje teraz w 2 miejscach:

  1. W reduktora. Po wywołaniu akcji SET_USER reduktor record musi również ustawić status rekordów należących do obserwowanych tablic wyników na loading.
  2. W kreatorze działań, który wykonuje rzeczywisty efekt uboczny pobierania/przesyłania wyniku.

Wydaje się również, że muszę ręcznie śledzić wszystkich aktywnych obserwatorów. Natomiast w wersji Bacon.js, zrobiłem coś takiego:

Bacon.once() // When first observing the scoreboard 
.merge(resubmit口) // When resubmitting because of network error 
.merge(user川.changes().filter(user => !!user).first()) // When user logs in (but only once) 
.flatMapLatest(submitOrGetRanking(data)) 

Rzeczywisty kod boczek jest o wiele dłużej, bo ze wszystkich skomplikowanych zasad powyżej, sprawiło, że wersja Bacon ledwo czytelny.

Ale Bacon automatycznie śledził wszystkie aktywne subskrypcje. To doprowadziło mnie do zakwestionowania, że ​​może nie być warta zmiany, ponieważ przepisanie tego na Redux wymagałoby dużej ręcznej obsługi. Czy ktoś może zaproponować jakiś wskaźnik?

Odpowiedz

50

Jeśli potrzebujesz złożonych zależności asynchronicznych, użyj po prostu Bacona, Rx, kanałów, sag lub innej asynchronicznej abstrakcji. Możesz ich używać z lub bez Redux. Przykład z Redux:

observeSomething() 
    .flatMap(someTransformation) 
    .filter(someFilter) 
    .map(createActionSomehow) 
    .subscribe(store.dispatch); 

Można skomponować asynchroniczne działania dowolny sposób, jedynym ważnym elementem jest to, że w końcu zamieniają się w store.dispatch(action) połączeń.

Redux Thunk wystarcza do prostych aplikacji, ale w miarę potrzeb asynchroniczny uzyskać bardziej wyrafinowane, trzeba użyć prawdziwego asynchronicznego skład abstrakcji i Redux nie obchodzi, który z nich korzystać.


Aktualizacja: Minęło trochę czasu i pojawiło się kilka nowych rozwiązań. Proponuję wypróbować Redux Saga, która stała się dość popularnym rozwiązaniem asynchronicznego sterowania w Redux.

+3

Skąd ta asynchroniczna kompozycja powinna normalnie rezydować? Chodzi mi o to, gdzieś poza reduxem czy lepiej pasuje do middleware? –

+4

Oprogramowanie pośrednie służy do prostej transformacji działań - nie do złożonej kompozycji. Po prostu umieść go poza Redux jak zwykłe funkcje zwracające obserwowalne, tak czy inaczej. Możesz także użyć czegoś takiego jak https://github.com/acdlite/redux-rx. –

+1

Dziękuję. W końcu zdecydowałem, że po prostu użyję Bacona do złożonej asynchronicznej logiki, ale dla części, która po prostu przechowuje dane, używam wzorca Redux. Teraz moja implementacja sklepu Redux wygląda następująco: 'const state 川 = action 川 => action 川 .scan (INITIAL_STATE, reducer)', a teraz mogę przetestować te reduktory samodzielnie, a także można przetestować te strumienie, które tworzą te działania osobno. Teraz myślę, że sprowadza się to do prawidłowego organizowania kodu Bacona/Rx, co samo w sobie jest wyzwaniem. Dzięki jeszcze raz! – Thai

16

Edit: tam teraz redux-saga projekt zainspirowany tych pomysłów

Oto kilka ciekawych zasobów


Flux/Redux jest inspirowana z przetwarzaniem strumienia zdarzeń backend (niezależnie od nazwy jest: eventsourcing, CQRS, CEP, lambda architektury ...).

Możemy porównać ActionCreators/Actions of Flux do Commands/Events (terminologia zwykle używana w systemach zaplecza).

W tych architekturach backendu używamy wzorca, który jest często nazywany Saga lub Process Manager. Zasadniczo jest to element w systemie, który odbiera zdarzenia, może zarządzać własnym stanem, a następnie może wydawać nowe polecenia. Aby było to proste, jest to trochę jak implementacja IFTTT (If-This-Then-That).

Można to zaimplementować z FRP i BaconJS, ale można to również zastosować na reduktorach Redux.

function submitScoreAfterLoginSaga(action, state = {}) { 
    switch (action.type) { 

    case SCORE_RECORDED: 
     // We record the score for later use if USER_LOGGED_IN is fired 
     state = Object.assign({}, state, {score: action.score} 

    case USER_LOGGED_IN: 
     if (state.score) { 
     // Trigger ActionCreator here to submit that score 
     dispatch(sendScore(state.score)) 
     } 
    break; 

    } 
    return state; 
} 

Żeby było jasne: stan obliczony od reduktorów do kierowania React tynk powinien pozostać absolutnie czysty! Jednak nie cały stan Twojej aplikacji ma na celu wywoływanie renderowania, i tak jest w przypadku, gdy potrzebna jest synchronizacja różnych oddzielonych od siebie części aplikacji za pomocą złożonych reguł. Stan tej "sagi" nie powinien wywoływać renderowania React!

Nie sądzę, że Redux dostarczy cokolwiek do wsparcia tego schematu, ale prawdopodobnie można go łatwo wdrożyć samodzielnie.

Zrobiłem to w naszym startup framework i ten wzór działa dobrze. To pozwala nam obsługiwać rzeczy IFTTT jak:

  • Kiedy onboarding użytkownik jest aktywny, a użytkownik blisko Popup1, a następnie otworzyć Popup2 i wyświetlić jakąś podpowiedź podpowiedź.

  • Gdy użytkownik korzysta z mobilnej strony i otwiera Menu2, a następnie zamknij Menu1

WAŻNE: jeśli używasz cofnąć funkcji/przerobić/powtarzanie pewnych ram, takich jak Redux, ważne jest, aby podczas powtórka z dziennika zdarzeń, wszystkie te sagi są nieskanowane, ponieważ nie chcesz, aby nowe zdarzenia były uruchamiane podczas powtórki!

+0

Dziękuję za odpowiedź! Mamy więc inny reduktor "saga", który zarządza właśnie tą interakcją/koordynacją, a także może wywoływać efekty uboczne. Aby to zrobić [prawdopodobnie dołączę do ciemnej strony Flux i pozwolę sklepom wywołać efekty uboczne] (http://jamesknelson.com/join-the-dark-side-of-the-flux-responding-to-actions -z-aktorami /). Zamiast tego zdecydowałem się na Bacona (używając reduktorów w częściach nie asynchronicznych), ale twoja odpowiedź jest również bardzo przydatna! Dzięki. – Thai

+0

@Thai tak, mój pomysł jest bardzo podobny do twojego ciemnego linku. Nie nazwałbym tego ciemną stroną, ponieważ reduktory, które są używane do renderowania, są nadal wolne od jakichkolwiek skutków ubocznych. Ten nowy rodzaj reduktora pełni jedynie funkcję koordynacji dla "długich transakcji", a ich stan nie powinien absolutnie być używany do generowania renderingów. –

+0

Powiązane o wzór saga z redux: http://stackoverflow.com/a/33829400/82609 –

Powiązane problemy