Prosty przepływ danych w aplikacjach React przy użyciu Flux i Backbone: samouczek z przykładami

Opublikowany: 2022-03-11

React.js to fantastyczna biblioteka. Czasami wydaje się to najlepszą rzeczą od czasu pokrojonego Pythona. Jednak React jest tylko jedną częścią stosu aplikacji front-endowych. Nie ma wiele do zaoferowania, jeśli chodzi o zarządzanie danymi i stanem.

Facebook, twórcy Reacta, zaoferowali tam pewne wskazówki w postaci Fluxa. Flux to „Architektura aplikacji” (nie framework) zbudowana wokół jednokierunkowego przepływu danych przy użyciu widoków React, Dyspozytora akcji i Sklepów. Wzorzec Flux rozwiązuje niektóre główne problemy, urzeczywistniając ważne zasady kontroli zdarzeń, co znacznie ułatwia rozumowanie, rozwijanie i konserwację aplikacji React.

Tutaj przedstawię podstawowe przykłady przepływu sterowania Flux, omówię, czego brakuje w Sklepach oraz jak używać modeli i kolekcji szkieletowych, aby wypełnić lukę w sposób „zgodny z Flux”.

(Uwaga: używam CoffeeScript w moich przykładach dla wygody i zwięzłości. Deweloperzy spoza CoffeeScript powinni być w stanie śledzić i traktować przykłady jako pseudokod.)

Wprowadzenie do Facebooka Flux

Backbone to doskonała i dobrze sprawdzona mała biblioteka zawierająca widoki, modele, kolekcje i trasy. Jest to de facto standardowa biblioteka dla ustrukturyzowanych aplikacji front-endowych i jest łączona z aplikacjami React od czasu wprowadzenia tej ostatniej w 2013 roku. Większość przykładów Reacta poza Facebook.com do tej pory zawierała wzmianki o używaniu Backbone w tandemie.

Niestety, opieranie się na samym Backbone do obsługi całego przepływu aplikacji poza widokami Reacta powoduje niefortunne komplikacje. Kiedy po raz pierwszy zacząłem pracować nad kodem aplikacji React-Backbone, „złożone łańcuchy zdarzeń”, o których czytałem, nie zajęły dużo czasu, aby podnieść ich głowy przypominające hydrę. Wysyłanie zdarzeń z interfejsu użytkownika do modeli, a następnie z jednego modelu do drugiego iz powrotem utrudnia śledzenie, kto zmieniał kogo, w jakiej kolejności i dlaczego.

Ten samouczek Flux pokaże, jak wzorzec Flux radzi sobie z tymi problemami z imponującą łatwością i prostotą.

Przegląd

Hasłem Fluxa jest „jednokierunkowy przepływ danych”. Oto przydatny diagram z dokumentacji Flux pokazujący, jak wygląda ten przepływ:

Facebook Flux wykorzystuje model „jednokierunkowego przepływu danych”, który różni się nieco w połączeniu z React i Backbone.

Ważną rzeczą jest to, że rzeczy płyną z React --> Dispatcher --> Stores --> React .

Przyjrzyjmy się, czym jest każdy z głównych elementów i jak się łączą:

Dokumenty zawierają również to ważne zastrzeżenie:

Flux jest bardziej wzorcem niż frameworkiem i nie ma żadnych twardych zależności. Jednak często używamy EventEmitter jako podstawy dla Sklepów i Reaguj na nasze widoki. Jedynym elementem Fluxa, który nie jest łatwo dostępny gdzie indziej, jest Dyspozytor. Ten moduł jest dostępny tutaj, aby uzupełnić zestaw narzędzi Flux.

Więc Flux ma trzy składniki:

  1. Wyświetlenia ( React = require('react') ) )
  2. Dyspozytor ( Dispatcher = require('flux').Dispatcher )
  3. Sklepy ( EventEmitter = require('events').EventEmitter )
    • (lub, jak wkrótce zobaczymy, Backbone = require('backbone') ) )

Widoki

Nie będę tutaj opisywał Reacta, bo tyle o nim napisano, poza tym, że zdecydowanie wolę go od Angulara. Prawie nigdy nie czuję się zdezorientowany podczas pisania kodu React, w przeciwieństwie do Angulara, ale oczywiście opinie będą się różnić.

Dyspozytor

Dyspozytor Flux to jedno miejsce, w którym obsługiwane są wszystkie zdarzenia modyfikujące Twoje Sklepy. Aby z niego skorzystać, każdy Sklep register pojedyncze wywołanie zwrotne do obsługi wszystkich zdarzeń. Następnie, za każdym razem, gdy chcesz zmodyfikować Sklep, dispatch zdarzenie.

Podobnie jak React, Dispatcher wydaje mi się dobrym pomysłem, dobrze zaimplementowanym. Na przykład aplikacja, która pozwala użytkownikowi dodawać elementy do listy rzeczy do zrobienia, może zawierać następujące elementy:

 # in TodoDispatcher.coffee Dispatcher = require("flux").Dispatcher TodoDispatcher = new Dispatcher() # That's all it takes!. module.exports = TodoDispatcher
 # in TodoStore.coffee TodoDispatcher = require("./TodoDispatcher") TodoStore = {items: []} TodoStore.dispatchCallback = (payload) -> switch payload.actionType when "add-item" TodoStore.items.push payload.item when "delete-last-item" TodoStore.items.pop() TodoStore.dispatchToken = TodoDispatcher.registerCallback(TodoStore.dispatchCallback) module.exports = TodoStore
 # in ItemAddComponent.coffee TodoDispatcher = require("./TodoDispatcher") ItemAddComponent = React.createClass handleAddItem: -> # note: you're NOT just pushing directly to the store! # (the restriction of moving through the dispatcher # makes everything much more modular and maintainable) TodoDispatcher.dispatch actionType: "add-item" item: "hello world" render: -> React.DOM.button { onClick: @handleAddItem }, "Add an Item!"

Dzięki temu naprawdę łatwo jest odpowiedzieć na dwa pytania:

  1. P: Jakie są wszystkie zdarzenia, które modyfikują MyStore ?
    • O: Po prostu sprawdź przypadki w instrukcji switch w MyStore.dispatchCallback .
  2. P: Jakie są wszystkie możliwe źródła tego wydarzenia?
    • O: Po prostu wyszukaj ten actionType .

Jest to o wiele łatwiejsze niż na przykład szukanie MyModel.set i MyModel.save i MyCollection.add itp., gdzie znalezienie odpowiedzi na te podstawowe pytania staje się naprawdę trudne i bardzo szybkie.

Dispatcher umożliwia również sekwencyjne uruchamianie wywołań zwrotnych w prosty, synchroniczny sposób przy użyciu waitFor . Na przykład:

 # in MessageStore.coffee MyDispatcher = require("./MyDispatcher") TodoStore = require("./TodoStore") MessageStore = {items: []} MessageStore.dispatchCallback = (payload) -> switch payload.actionType when "add-item" # synchronous event flow! MyDispatcher.waitFor [TodoStore.dispatchToken] MessageStore.items.push "You added an item! It was: " + payload.item module.exports = MessageStore

W praktyce byłem zszokowany, widząc, o ile czystszy był mój kod, gdy używałem Dispatchera do modyfikowania moich Sklepów, nawet bez użycia waitFor .

Sklepy

Tak więc dane przepływają do Sklepów za pośrednictwem Dyspozytora. Rozumiem. Ale w jaki sposób dane przepływają ze Sklepów do Widoków (tj. React)? Jak stwierdzono w dokumentacji Flux:

[Widok] nasłuchuje wydarzeń, które są transmitowane przez sklepy, od których jest zależny.

Okej świetnie. Podobnie jak rejestrowaliśmy wywołania zwrotne w naszych Sklepach, rejestrujemy wywołania zwrotne w naszych Widokach (które są komponentami React). Nakazujemy Reactowi ponowne render za każdym razem, gdy w sklepie pojawi się zmiana, która została przekazana przez jego props .

Na przykład:

 # in TodoListComponent.coffee React = require("react") TodoListComponent = React.createClass componentDidMount: -> @props.TodoStore.addEventListener "change", => @forceUpdate() , @ componentWillUnmount: -> # remove the callback render: -> # show the items in a list. React.DOM.ul {}, @props.TodoStore.items.map (item) -> React.DOM.li {}, item

Świetny!

Jak więc emitujemy zdarzenie "change" ? Cóż, Flux zaleca używanie EventEmitter . Z oficjalnego przykładu:

 var MessageStore = merge(EventEmitter.prototype, { emitChange: function() { this.emit(CHANGE_EVENT); }, /** * @param {function} callback */ addChangeListener: function(callback) { this.on(CHANGE_EVENT, callback); }, get: function(id) { return _messages[id]; }, getAll: function() { return _messages; }, // etc...

Brutto! Muszę to wszystko napisać sam, za każdym razem, gdy chcę mieć prosty Sklep? Którego mam używać za każdym razem, gdy mam informację, którą chcę wyświetlić? Musi być lepszy sposób!

Brakujący kawałek

Modele i kolekcje Backbone mają już wszystko, co robią sklepy Flux oparte na EventEmitter.

Mówiąc ci, żebyś używał surowego EventEmitter, Flux zaleca odtworzenie około 50-75% modeli i kolekcji Backbone za każdym razem, gdy tworzysz sklep. Używanie EventEmitter dla twoich sklepów jest jak używanie samego Node.js dla twojego serwera, gdy istnieją już dobrze zbudowane mikroframeworki, takie jak Express.js lub równoważne, aby zadbać o wszystkie podstawy i szablony.

Podobnie jak Express.js jest zbudowany na Node.js, modele i kolekcje Backbone są zbudowane na EventEmitter. I ma wszystko, czego prawie zawsze potrzebujesz: Backbone emituje zdarzenia change i ma metody zapytań, gettery, settery i wszystko. Poza tym Jeremy Ashkenas z Backbone i jego armia 230 współpracowników wykonali o wiele lepszą robotę we wszystkich tych rzeczach, niż prawdopodobnie jestem w stanie zrobić.

Jako przykład dla tego samouczka Backbone przekonwertowałem przykład MessageStore z powyższego na wersję Backbone.

Jest to obiektywnie mniej kodu (nie ma potrzeby powielania pracy) i jest subiektywnie bardziej przejrzyste i zwięzłe (na przykład this.add(message) zamiast _messages[message.id] = message ).

Użyjmy więc Backbone for Stores!

Wzorzec FluxBone: Sklepy Flux według Backbone

Ten samouczek jest podstawą podejścia, które z dumą nazwałem FluxBone , architekturą Flux używającą Backbone for Stores. Oto podstawowy wzór architektury FluxBone:

  1. Sklepy są instancjami modeli szkieletowych lub kolekcji, które zarejestrowały wywołanie zwrotne u dyspozytora. Zazwyczaj oznacza to, że są singletonami.
  2. Komponenty widoku nigdy nie modyfikują bezpośrednio Sklepów (na przykład nie .set() ). Zamiast tego komponenty wysyłają akcje do dyspozytora.
  3. Wyświetlaj komponenty, wysyłając zapytania do magazynów i wiążąc się z ich zdarzeniami, aby wyzwalać aktualizacje.

Ten samouczek Backbone ma na celu przyjrzenie się, jak Backbone i Flux współpracują ze sobą w aplikacjach React.

Użyjmy przykładów Backbone i Flux, aby przyjrzeć się każdemu z nich po kolei:

1. Sklepy są instancjami modeli szkieletowych lub kolekcji, które zarejestrowały wywołanie zwrotne u Dyspozytora.

 # in TodoDispatcher.coffee Dispatcher = require("flux").Dispatcher TodoDispatcher = new Dispatcher() # That's all it takes! module.exports = TodoDispatcher
 # in stores/TodoStore.coffee Backbone = require("backbone") TodoDispatcher = require("../dispatcher") TodoItem = Backbone.Model.extend({}) TodoCollection = Backbone.Collection.extend model: TodoItem url: "/todo" # we register a callback with the Dispatcher on init. initialize: -> @dispatchToken = TodoDispatcher.register(@dispatchCallback) dispatchCallback: (payload) => switch payload.actionType # remove the Model instance from the Store. when "todo-delete" @remove payload.todo when "todo-add" @add payload.todo when "todo-update" # do stuff... @add payload.todo, merge: true # ... etc # the Store is an instantiated Collection; a singleton. TodoStore = new TodoCollection() module.exports = TodoStore

2. Komponenty nigdy nie modyfikują bezpośrednio Sklepów (na przykład nie .set() ). Zamiast tego komponenty wysyłają akcje do dyspozytora.

 # components/TodoComponent.coffee React = require("react") TodoListComponent = React.createClass handleTodoDelete: -> # instead of removing the todo from the TodoStore directly, # we use the Dispatcher TodoDispatcher.dispatch actionType: "todo-delete" todo: @props.todoItem # ... (see below) ... module.exports = TodoListComponent

3. Komponenty wysyłają zapytania do magazynów i wiążą się z ich zdarzeniami, aby wywołać aktualizacje.

 # components/TodoComponent.coffee React = require("react") TodoListComponent = React.createClass handleTodoDelete: -> # instead of removing the todo from the TodoStore directly, # we use the dispatcher. #flux TodoDispatcher.dispatch actionType: "todo-delete" todo: @props.todoItem # ... componentDidMount: -> # the Component binds to the Store's events @props.TodoStore.on "add remove reset", => @forceUpdate() , @ componentWillUnmount: -> # turn off all events and callbacks that have this context @props.TodoStore.off null, null, this render: -> React.DOM.ul {}, @props.TodoStore.items.map (todoItem) -> # TODO: TodoItemComponent, which would bind to # `this.props.todoItem.on('change')` TodoItemComponent { todoItem: todoItem } module.exports = TodoListComponent

Zastosowałem to podejście Flux i Backbone do moich własnych projektów, a kiedy przebudowałem swoją aplikację React tak, aby używała tego wzorca, prawie wszystkie brzydkie fragmenty zniknęły. To było trochę cudowne: jeden po drugim, fragmenty kodu, przez które zgrzytałem zębami w poszukiwaniu lepszego sposobu, zostały zastąpione rozsądnym przepływem. A płynność, z jaką Backbone wydaje się integrować z tym wzorem, jest niezwykła: nie czuję, że walczę z Backbone, Fluxem lub Reactem, aby dopasować je do siebie w jednej aplikacji.

Przykładowe miksowanie

Pisanie this.on(...) i this.off(...) za każdym razem, gdy dodajesz FluxBone Store do komponentu, może stać się nieco przestarzałe.

Oto przykład React Mixin, który choć niezwykle naiwny, z pewnością ułatwiłby szybkie iterowanie:

 # in FluxBoneMixin.coffee module.exports = (propName) -> componentDidMount: -> @props[propName].on "all", => @forceUpdate() , @ componentWillUnmount: -> @props[propName].off "all", => @forceUpdate() , @
 # in HelloComponent.coffee React = require("react") UserStore = require("./stores/UserStore") TodoStore = require("./stores/TodoStore") FluxBoneMixin = require("./FluxBoneMixin") MyComponent = React.createClass mixins: [ FluxBoneMixin("UserStore"), FluxBoneMixin("TodoStore"), ] render: -> React.DOM.div {}, "Hello, #{ @props.UserStore.get('name') }, you have #{ @props.TodoStore.length } things to do." React.renderComponent( MyComponent { UserStore: UserStore TodoStore: TodoStore } , document.body.querySelector(".main") )

Synchronizacja z internetowym interfejsem API

Na oryginalnym diagramie Flux wchodzisz w interakcję z internetowym interfejsem API tylko za pośrednictwem ActionCreators, które wymagają odpowiedzi z serwera przed wysłaniem akcji do Dispatcher. To nigdy nie pasowało do mnie; Czy sklep nie powinien być pierwszym, który dowie się o zmianach, przed serwerem?

Zdecydowałem się odwrócić tę część diagramu: Sklepy współdziałają bezpośrednio z interfejsem API RESTful CRUD za pośrednictwem funkcji sync() Backbone. Jest to cudownie wygodne, przynajmniej jeśli pracujesz z rzeczywistym interfejsem API CRUD RESTful.

Integralność danych jest zachowana bez problemu. Kiedy .set() nową właściwość, zdarzenie change wyzwala ponowne renderowanie React, optymistycznie wyświetlając nowe dane. Kiedy próbujesz .save() go na serwerze, zdarzenie request informuje o wyświetleniu ikony ładowania. Kiedy coś się dzieje, zdarzenie sync informuje o usunięciu ikony ładowania lub zdarzenie error informuje, że należy zmienić kolor na czerwony. Możesz zobaczyć inspirację tutaj.

Istnieje również walidacja (i odpowiadające mu invalid zdarzenie) dla pierwszej warstwy obrony oraz metoda .fetch() do pobierania nowych informacji z serwera.

W przypadku mniej standardowych zadań interakcja za pośrednictwem ActionCreators może mieć więcej sensu. Podejrzewam, że Facebook nie robi zbyt wiele „zwykłego CRUD”, w związku z czym nic dziwnego, że nie stawiają Sklepów na pierwszym miejscu.

Wniosek

Zespoły inżynierskie na Facebooku wykonały niezwykłą pracę, aby popchnąć front-endową sieć do przodu dzięki React, a wprowadzenie Fluxa daje wgląd w szerszą architekturę, która naprawdę się skaluje: nie tylko pod względem technologii, ale także inżynierii. Sprytne i ostrożne użycie Backbone (na przykładzie tego samouczka) może wypełnić luki w Flux, czyniąc niezwykle łatwym każdemu, od jednoosobowych sklepów niezależnych po duże firmy, tworzenie i utrzymywanie imponujących aplikacji.

Powiązane: Jak komponenty React ułatwiają testowanie interfejsu użytkownika