Flux de date simplu în aplicațiile React folosind Flux și Backbone: Un tutorial cu exemple

Publicat: 2022-03-11

React.js este o bibliotecă fantastică. Uneori pare cel mai bun lucru de la felii de Python. Cu toate acestea, React este doar o parte a unei stive de aplicații front-end. Nu are prea multe de oferit când vine vorba de gestionarea datelor și a stării.

Facebook, creatorii lui React, au oferit câteva îndrumări acolo sub forma Flux. Flux este o „Arhitectură de aplicație” (nu un cadru) construită în jurul fluxului de date unidirecțional, folosind React Views, un Action Dispatcher și Stores. Modelul Flux rezolvă unele probleme majore prin încorporarea unor principii importante de control al evenimentelor, care fac aplicațiile React mult mai ușor de gândit, dezvoltat și întreținut.

Aici, voi prezenta exemple de bază Flux de flux de control, voi discuta despre ceea ce lipsește pentru magazine și despre cum să utilizați modelele și colecțiile Backbone pentru a umple golul într-un mod „compatibil cu Flux”.

(Notă: folosesc CoffeeScript în exemplele mele pentru comoditate și concizie. Dezvoltatorii non-CoffeeScript ar trebui să poată urmări și pot trata exemplele ca pseudocod.)

Introducere în fluxul Facebook

Backbone este o mică bibliotecă excelentă și bine verificată, care include vizualizări, modele, colecții și rute. Este o bibliotecă standard de facto pentru aplicații front-end structurate și a fost asociată cu aplicațiile React de când aceasta din urmă a fost introdusă în 2013. Cele mai multe exemple de React în afara Facebook.com au inclus până acum mențiuni despre utilizarea Backbone în tandem.

Din păcate, sprijinirea doar pe Backbone pentru a gestiona întregul flux de aplicații în afara React’s Views prezintă complicații nefericite. Când am început să lucrez la codul aplicației React-Backbone, „lanțurile complexe de evenimente” despre care citisem nu au durat mult să-și ridice capetele asemănătoare hidrei. Trimiterea evenimentelor din interfața de utilizare la modele, apoi de la un model la altul și apoi înapoi, face dificilă urmărirea cine a schimbat pe cine, în ce ordine și de ce.

Acest tutorial Flux va demonstra modul în care modelul Flux gestionează aceste probleme cu ușurință și simplitate impresionantă.

O imagine de ansamblu

Sloganul lui Flux este „flux de date unidirecțional”. Iată o diagramă la îndemână din documentele Flux care arată cum arată acel flux:

Facebook Flux folosește un model de „flux de date unidirecționale” care variază puțin atunci când este asociat cu React și Backbone.

Partea importantă este că lucrurile curg din React --> Dispatcher --> Stores --> React .

Să ne uităm la ce sunt fiecare dintre componentele principale și cum se conectează:

Documentele oferă, de asemenea, această avertizare importantă:

Flux este mai mult un model decât un cadru și nu are dependențe dure. Cu toate acestea, folosim adesea EventEmitter ca bază pentru magazine și React pentru vizualizările noastre. Singura parte a Flux care nu este ușor disponibilă în altă parte este Dispatcher. Acest modul este disponibil aici pentru a vă completa setul de instrumente Flux.

Deci, Flux are trei componente:

  1. Vizualizări ( React = require('react') )
  2. Dispatcher ( Dispatcher = require('flux').Dispatcher )
  3. Magazine ( EventEmitter = require('events').EventEmitter )
    • (sau, după cum vom vedea în curând, Backbone = require('backbone') )

Vederile

Nu voi descrie React aici, deoarece s-au scris atât de multe despre el, în afară de a spune că îl prefer mult decât Angular. Aproape niciodată nu mă simt confuz când scriu codul React, spre deosebire de Angular, dar desigur, opiniile vor varia.

Dispeceratul

Flux Dispatcher este un singur loc unde sunt gestionate toate evenimentele care vă modifică Magazinele. Pentru a-l folosi, fiecare magazin trebuie să register un singur apel invers pentru a gestiona toate evenimentele. Apoi, ori de câte ori doriți să modificați un Magazin, dispatch un eveniment.

La fel ca React, Dispatcher-ul mi se pare o idee bună, bine implementată. De exemplu, o aplicație care permite utilizatorului să adauge elemente la o listă de activități ar putea include următoarele:

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

Acest lucru face foarte ușor să răspunzi la două întrebări:

  1. Î: Care sunt toate evenimentele care modifică MyStore ?
    • R: Verificați doar cazurile din declarația switch din MyStore.dispatchCallback .
  2. Î: Care sunt toate sursele posibile ale acestui eveniment?
    • R: Pur și simplu căutați acel actionType .

Acest lucru este mult mai ușor decât, de exemplu, să căutați MyModel.set și MyModel.save și MyCollection.add etc, unde găsirea răspunsurilor la aceste întrebări de bază devine foarte dificilă, foarte rapid.

Dispatcher-ul vă permite, de asemenea, să rulați apelurile înapoi într-un mod simplu, sincron, folosind waitFor . De exemplu:

 # 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

În practică, am fost șocat să văd cât de mai curat a fost codul meu când am folosit Dispatcher pentru a-mi modifica Magazinele, chiar și fără a folosi waitFor .

Magazinele

Deci datele circulă în magazine prin Dispatcher. Am înțeles. Dar cum circulă datele de la Magazine la Vizualizări (adică React)? După cum se precizează în documentele Flux:

[Vizualizarea] ascultă evenimentele care sunt difuzate de magazinele de care depinde.

Bine, grozav. La fel cum am înregistrat apeluri inverse în magazinele noastre, înregistrăm apeluri inverse cu vizualizările noastre (care sunt React Components). Îi spunem lui React să render ori de câte ori are loc o schimbare în Magazin care a fost transmisă prin elementele de props .

De exemplu:

 # 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

Minunat!

Deci, cum emitem acel eveniment de "change" ? Ei bine, Flux recomandă utilizarea EventEmitter . Dintr-un exemplu oficial:

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

Brut! Toate acestea trebuie să scriu eu, de fiecare dată când vreau un magazin simplu? Pe care ar trebui să-l folosesc de fiecare dată când am o informație pe care vreau să o afișez? Trebuie să existe o cale mai bună!

Piesa lipsă

Modelele și colecțiile Backbone au deja tot ce par să facă magazinele Flux bazate pe EventEmitter.

Spunându-vă să utilizați EventEmitter brut, Flux vă recomandă să recreați poate 50-75% din Modelele și Colecțiile Backbone de fiecare dată când creați un Magazin. Folosirea EventEmitter pentru magazinele dvs. este ca și cum ați folosi Node.js pentru serverul dvs. atunci când microframework-uri bine construite, cum ar fi Express.js sau echivalent, există deja pentru a avea grijă de toate elementele de bază și standarde.

La fel cum Express.js este construit pe Node.js, modelele și colecțiile Backbone sunt construite pe EventEmitter. Și are toate lucrurile de care aveți nevoie aproape întotdeauna: Backbone emite evenimente de change și are metode de interogare, getters și setters și tot. În plus, Jeremy Ashkenas de la Backbone și armata sa de 230 de colaboratori au făcut o treabă mult mai bună în toate aceste lucruri decât probabil că voi putea face eu.

Ca exemplu pentru acest tutorial Backbone, am convertit exemplul MessageStore de mai sus într-o versiune Backbone.

Este în mod obiectiv mai puțin cod (nu este nevoie să duplicați munca) și este subiectiv mai clar și mai concis (de exemplu, this.add(message) în loc de _messages[message.id] = message ).

Deci, să folosim Backbone pentru magazine!

Modelul FluxBone: Flux Stores by Backbone

Acest tutorial este baza unei abordări pe care am numit-o cu mândrie FluxBone , arhitectura Flux care utilizează Backbone pentru magazine. Iată modelul de bază al unei arhitecturi FluxBone:

  1. Magazinele sunt instanțiate de modele sau colecții Backbone, care au înregistrat un apel invers la Dispatcher. De obicei, aceasta înseamnă că sunt singletons.
  2. Componentele de vizualizare nu modifică niciodată în mod direct Magazinele (de exemplu, fără .set() ). În schimb, componentele trimit Acțiuni către Dispatcher.
  3. Vizualizați interogarea componentelor Magazine și legați-vă la evenimentele acestora pentru a declanșa actualizări.

Acest tutorial Backbone este conceput pentru a analiza modul în care Backbone și Flux lucrează împreună în aplicațiile React.

Să folosim exemple de Backbone și Flux pentru a ne uita la fiecare parte din aceasta pe rând:

1. Magazinele sunt instanțiate de modele sau colecții Backbone, care au înregistrat un apel invers la 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. Componentele nu modifică niciodată direct magazinele (de exemplu, fără .set() ). În schimb, componentele trimit Acțiuni către 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. Componentele interogează Stocurile și se leagă de evenimentele lor pentru a declanșa actualizări.

 # 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

Am aplicat această abordare Flux și Backbone pentru propriile mele proiecte și, odată ce mi-am re-arhitectat aplicația React pentru a utiliza acest model, aproape toate părțile urâte au dispărut. A fost puțin miraculos: una câte una, bucățile de cod care m-au făcut să scrâșnesc din dinți să caut o cale mai bună au fost înlocuite cu un flux sensibil. Și netezimea cu care Backbone pare să se integreze în acest model este remarcabilă: nu simt că mă lupt cu Backbone, Flux sau React pentru a le potrivi într-o singură aplicație.

Exemplu Mixin

Scrierea this.on(...) și this.off(...) de fiecare dată când adăugați un FluxBone Store la o componentă poate deveni puțin mai vechi.

Iată un exemplu de React Mixin care, deși extrem de naiv, ar face cu siguranță repetarea rapidă și mai ușoară:

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

Sincronizarea cu un API Web

În diagrama Flux originală, interacționați cu API-ul web numai prin ActionCreators, care necesită un răspuns de la server înainte de a trimite acțiuni către Dispatcher. Asta nu a stat niciodată bine cu mine; nu ar trebui Magazinul să fie primul care știe despre modificări, înaintea serverului?

Aleg să răsturn acea parte a diagramei: magazinele interacționează direct cu un API RESTful CRUD prin sync() . Acest lucru este minunat de convenabil, cel puțin dacă lucrați cu un API CRUD RESTful real.

Integritatea datelor este menținută fără probleme. Când .set() o nouă proprietate, evenimentul de change declanșează o redare React, afișând optimist noile date. Când încercați să .save() pe server, evenimentul de request vă permite să afișați o pictogramă de încărcare. Când lucrurile trec bine, evenimentul de sync vă informează că trebuie să eliminați pictograma de încărcare sau evenimentul de error vă informează că trebuie să transformați lucrurile în roșu. Puteți vedea inspirația aici.

Există, de asemenea, validarea (și un eveniment invalid corespunzător) pentru un prim strat de apărare și o metodă .fetch() pentru a extrage informații noi de pe server.

Pentru sarcini mai puțin standard, interacțiunea prin ActionCreators poate avea mai mult sens. Bănuiesc că Facebook nu face prea mult „simplu CRUD”, caz în care nu este surprinzător că nu pun magazinele pe primul loc.

Concluzie

Echipele de inginerie de la Facebook au făcut o muncă remarcabilă pentru a promova web-ul front-end cu React, iar introducerea Flux oferă o privire într-o arhitectură mai amplă care se scalează cu adevărat: nu doar în termeni de tehnologie, ci și de inginerie. Utilizarea inteligentă și atentă a Backbone (după exemplul acestui tutorial) poate umple golurile din Flux, făcând uimitor de ușor pentru oricine, de la magazine independente cu o singură persoană, până la companii mari, să creeze și să mențină aplicații impresionante.

Înrudit: Cum componentele React fac testarea UI ușoară