Flusso di dati semplice nelle app React utilizzando Flux e Backbone: un tutorial con esempi

Pubblicato: 2022-03-11

React.js è una libreria fantastica. A volte sembra la cosa migliore da quando ha tagliato Python. Tuttavia, React è solo una parte di uno stack di applicazioni front-end. Non ha molto da offrire quando si tratta di gestire i dati e lo stato.

Facebook, i creatori di React, hanno offerto alcune indicazioni sotto forma di Flux. Flux è una "Architettura dell'applicazione" (non un framework) costruita attorno al flusso di dati unidirezionale che utilizza React Views, Action Dispatcher e Store. Il modello Flux risolve alcuni problemi importanti incorporando importanti principi di controllo degli eventi, che rendono le applicazioni React molto più facili da ragionare, sviluppare e mantenere.

Qui, introdurrò esempi Flux di base del flusso di controllo, discuterò cosa manca per i negozi e come utilizzare i modelli e le raccolte backbone per colmare il divario in modo "conforme al flusso".

(Nota: uso CoffeeScript nei miei esempi per comodità e brevità. Gli sviluppatori non-CoffeeScript dovrebbero essere in grado di seguire e trattare gli esempi come pseudocodice.)

Introduzione al flusso di Facebook

Backbone è una piccola libreria eccellente e ben controllata che include viste, modelli, raccolte e percorsi. È una libreria standard de facto per applicazioni front-end strutturate ed è stata accoppiata con le app React da quando quest'ultima è stata introdotta nel 2013. La maggior parte degli esempi di React al di fuori di Facebook.com finora hanno incluso menzioni di Backbone utilizzato in tandem.

Sfortunatamente, affidarsi solo a Backbone per gestire l'intero flusso di applicazioni al di fuori di React's Views presenta spiacevoli complicazioni. Quando ho iniziato a lavorare sul codice dell'applicazione React-Backbone, le "catene di eventi complesse" di cui avevo letto non hanno impiegato molto a sollevare le loro teste simili a idra. L'invio di eventi dall'interfaccia utente ai modelli, quindi da un modello all'altro e poi di nuovo indietro, rende difficile tenere traccia di chi stava cambiando chi, in quale ordine e perché.

Questo tutorial Flux dimostrerà come il modello Flux gestisce questi problemi con facilità e semplicità impressionanti.

Una panoramica

Lo slogan di Flux è “flusso di dati unidirezionale”. Ecco un diagramma pratico dai documenti Flux che mostra come appare quel flusso:

Facebook Flux utilizza un modello di "flusso di dati unidirezionale" che varia leggermente se associato a React e Backbone.

La cosa importante è che le cose fluiscono da React --> Dispatcher --> Stores --> React .

Diamo un'occhiata a quali sono ciascuno dei componenti principali e come si collegano:

I documenti offrono anche questo importante avvertimento:

Flux è più un modello che un framework e non ha dipendenze rigide. Tuttavia, utilizziamo spesso EventEmitter come base per i negozi e reagiamo alle nostre visualizzazioni. L'unico pezzo di Flux non facilmente disponibile altrove è il Dispatcher. Questo modulo è disponibile qui per completare la tua cassetta degli attrezzi Flux.

Quindi Flux ha tre componenti:

  1. Visualizzazioni ( React = require('react') )
  2. Dispatcher ( Dispatcher = require('flux').Dispatcher )
  3. Negozi ( EventEmitter = require('events').EventEmitter )
    • (o, come vedremo presto, Backbone = require('backbone') )

Le visualizzazioni

Non descriverò React qui, dal momento che è stato scritto così tanto al riguardo, oltre a dire che lo preferisco di gran lunga ad Angular. Non mi sento quasi mai confuso quando scrivo il codice React, a differenza di Angular, ma ovviamente le opinioni variano.

Lo spedizioniere

Il Flux Dispatcher è un unico luogo in cui vengono gestiti tutti gli eventi che modificano i tuoi negozi. Per usarlo, devi fare in modo che ogni Store register una singola richiamata per gestire tutti gli eventi. Quindi, ogni volta che desideri modificare uno Store, dispatch un evento.

Come React, il Dispatcher mi sembra una buona idea, implementato bene. Ad esempio, un'app che consente all'utente di aggiungere elementi a un elenco di cose da fare potrebbe includere quanto segue:

 # 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!"

Questo rende davvero facile rispondere a due domande:

  1. D: Quali sono tutti gli eventi che modificano MyStore ?
    • R: Basta controllare i casi nell'istruzione switch in MyStore.dispatchCallback .
  2. D: Quali sono tutte le possibili fonti di quell'evento?
    • A: Cerca semplicemente quel actionType .

Questo è molto più semplice rispetto, ad esempio, alla ricerca di MyModel.set e MyModel.save e MyCollection.add ecc., dove rintracciare le risposte a queste domande di base diventa davvero difficile molto velocemente.

Il Dispatcher consente inoltre di eseguire i callback in sequenza in modo semplice e sincrono, utilizzando waitFor . Per esempio:

 # 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

In pratica, sono rimasto scioccato nel vedere quanto fosse più pulito il mio codice quando usavo Dispatcher per modificare i miei negozi, anche senza usare waitFor .

I negozi

Quindi i dati fluiscono negli Store attraverso il Dispatcher. Fatto. Ma in che modo i dati fluiscono dagli Store alle View (ad esempio, React)? Come affermato nei documenti Flux:

[La] vista ascolta gli eventi trasmessi dai negozi da cui dipende.

Va bene, fantastico. Proprio come abbiamo registrato i callback con i nostri Store, registriamo i callback con le nostre Views (che sono React Components). Diciamo a React di eseguire nuovamente il render ogni volta che si verifica un cambiamento nel negozio che è stato passato attraverso i suoi props di scena.

Per esempio:

 # 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

Stupendo!

Quindi, come emettiamo quell'evento di "change" ? Bene, Flux consiglia di utilizzare EventEmitter . Da un esempio ufficiale:

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

Schifoso! Devo scrivere tutto da solo, ogni volta che voglio un semplice Store? Quale dovrei usare ogni volta che ho un'informazione che voglio visualizzare? Ci deve essere un modo migliore!

Il pezzo mancante

I modelli e le collezioni di Backbone hanno già tutto ciò che i negozi basati su EventEmitter di Flux sembrano fare.

Dicendoti di utilizzare EventEmitter grezzo, Flux consiglia di ricreare forse il 50-75% dei modelli e delle collezioni di Backbone ogni volta che crei uno Store. Usare EventEmitter per i tuoi negozi è come usare Node.js nudo per il tuo server quando esistono già microframework ben costruiti come Express.js o equivalenti per prendersi cura di tutte le basi e standard.

Proprio come Express.js è basato su Node.js, i modelli e le raccolte di Backbone sono basati su EventEmitter. E ha tutte le cose di cui hai praticamente sempre bisogno: Backbone emette eventi di change e ha metodi di query, getter e setter e tutto il resto. Inoltre, Jeremy Ashkenas di Backbone e il suo esercito di 230 contributori hanno fatto un lavoro molto migliore su tutte queste cose di quanto io sia probabilmente in grado di fare.

Come esempio per questo tutorial Backbone, ho convertito l'esempio di MessageStore dall'alto in una versione Backbone.

È oggettivamente meno codice (non è necessario duplicare il lavoro) ed è soggettivamente più chiaro e conciso (ad esempio, this.add(message) invece di _messages[message.id] = message ).

Quindi usiamo Backbone per i negozi!

Il modello FluxBone: Flux Store di Backbone

Questo tutorial è la base di un approccio che ho soprannominato con orgoglio FluxBone , l'architettura Flux che utilizza Backbone for Stores. Ecco lo schema di base di un'architettura FluxBone:

  1. I negozi sono modelli o raccolte backbone istanziati, che hanno registrato una richiamata con il Dispatcher. In genere, questo significa che sono singleton.
  2. I componenti di visualizzazione non modificano mai direttamente gli Store (ad esempio, no .set() ). Al contrario, i componenti inviano le Azioni al Dispatcher.
  3. Visualizza i componenti interrogare gli archivi e collega ai relativi eventi per attivare gli aggiornamenti.

Questo tutorial Backbone è progettato per esaminare il modo in cui Backbone e Flux lavorano insieme nelle applicazioni React.

Usiamo gli esempi di Backbone e Flux per esaminare ogni pezzo a turno:

1. I negozi sono modelli o raccolte Backbone istanziati, che hanno registrato una richiamata con il Dispatcher.

 # 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. I componenti non modificano mai direttamente gli Store (ad esempio, no .set() ). Al contrario, i componenti inviano le Azioni al Dispatcher.

 # 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. Query sui componenti Archivia e si associa ai relativi eventi per attivare gli aggiornamenti.

 # 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

Ho applicato questo approccio Flux e Backbone ai miei progetti e, una volta che ho riprogettato la mia applicazione React per utilizzare questo modello, quasi tutti i brutti bit sono scomparsi. È stato un piccolo miracolo: uno per uno, i pezzi di codice che mi hanno fatto digrignare i denti in cerca di un modo migliore sono stati sostituiti da un flusso ragionevole. E la fluidità con cui Backbone sembra integrarsi in questo schema è notevole: non mi sento come se stessi combattendo Backbone, Flux o React per farli combaciare in un'unica applicazione.

Esempio Mixin

Scrivere il this.on(...) e this.off(...) ogni volta che si aggiunge un FluxBone Store a un componente può diventare un po' vecchio.

Ecco un esempio di React Mixin che, sebbene estremamente ingenuo, renderebbe sicuramente l'iterazione rapida ancora più semplice:

 # 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") )

Sincronizzazione con un'API Web

Nel diagramma Flux originale, interagisci con l'API Web solo tramite ActionCreators, che richiedono una risposta dal server prima di inviare azioni al Dispatcher. Questo non mi è mai piaciuto; lo Store non dovrebbe essere il primo a sapere delle modifiche, prima del server?

Scelgo di capovolgere quella parte del diagramma: gli Store interagiscono direttamente con un'API RESTful CRUD tramite Backbone's sync() . Questo è meravigliosamente conveniente, almeno se stai lavorando con una vera API CRUD RESTful.

L'integrità dei dati viene mantenuta senza problemi. Quando si .set() una nuova proprietà, l'evento change attiva un re-rendering di React, visualizzando ottimisticamente i nuovi dati. Quando provi a .save() sul server, l'evento request ti consente di visualizzare un'icona di caricamento. Quando le cose vanno a buon fine, l'evento di sync ti consente di rimuovere l'icona di caricamento, oppure l'evento di error ti consente di sapere se le cose diventano rosse. Puoi vedere l'ispirazione qui.

C'è anche la convalida (e un corrispondente evento invalid ) per un primo livello di difesa e un metodo .fetch() per estrarre nuove informazioni dal server.

Per attività meno standard, l'interazione tramite ActionCreators può avere più senso. Sospetto che Facebook non faccia molto "semplice CRUD", nel qual caso non sorprende che non mettano gli Store al primo posto.

Conclusione

I team di progettazione di Facebook hanno svolto un lavoro straordinario per portare avanti il ​​Web front-end con React e l'introduzione di Flux offre una sbirciatina in un'architettura più ampia che è davvero scalabile: non solo in termini di tecnologia, ma anche di ingegneria. Un uso intelligente e attento di Backbone (come nell'esempio di questo tutorial) può colmare le lacune in Flux, rendendo incredibilmente facile per chiunque, dai negozi indipendenti di una sola persona alle grandi aziende, creare e mantenere applicazioni impressionanti.

Correlati: in che modo i componenti React semplificano i test dell'interfaccia utente