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.
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.status
loading
.
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:
- W reduktora. Po wywołaniu akcji
SET_USER
reduktorrecord
musi również ustawić status rekordów należących do obserwowanych tablic wyników naloading
. - 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?
Skąd ta asynchroniczna kompozycja powinna normalnie rezydować? Chodzi mi o to, gdzieś poza reduxem czy lepiej pasuje do middleware? –
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. –
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