React.js er et fantastisk bibliotek. Noen ganger virker det som det beste siden skiver Python. React er imidlertid bare en del av en front-end applikasjonsstabel. Det har ikke mye å tilby når det gjelder administrasjon av data og tilstand.
Facebook, produsentene av React, har tilbød litt veiledning der i form av Flux . Flux er en 'Application Architecture' (ikke et rammeverk) bygget rundt enveis dataflyt ved hjelp av React Views, en Action Dispatcher og Stores. Flux-mønsteret løser noen store problemer ved å legemliggjøre viktige prinsipper for hendelseskontroll, noe som gjør React-applikasjoner mye lettere å resonnere om, utvikle og vedlikeholde.
Her vil jeg introdusere grunnleggende Flux-eksempler på kontrollflyt, diskutere hva som mangler for butikker, og hvordan du bruker Backbone Models and Collections for å fylle gapet på en 'Flux-kompatibel' måte.
(Merk: Jeg bruker CoffeeScript i eksemplene mine for bekvemmelighet og kortfattethet. Ikke-CoffeeScript-utviklere bør kunne følge med, og kan behandle eksemplene som pseudokode.)
Ryggrad er et utmerket og velprøvd lite bibliotek som inkluderer utsikt, modeller, samlinger og ruter. Det er en de facto standardbiblioteket for strukturerte front-end-applikasjoner, og det har blitt parret med React-apper siden sistnevnte ble introdusert i 2013. De fleste eksempler på React utenfor Facebook.com har så langt tatt med omtaler av Backbone som ble brukt i tandem.
Dessverre gir det seg uheldige komplikasjoner å lene seg på Backbone alene for å håndtere hele applikasjonsflyten utenfor React’s Views. Da jeg først begynte å jobbe med React-Backbone applikasjonskode, de 'komplekse hendelseskjedene' jeg hadde lese om tok ikke lang tid å bakre opp de hydralignende hodene. Å sende hendelser fra brukergrensesnittet til modellene, og deretter fra en modell til en annen og deretter tilbake igjen, gjør det vanskelig å holde rede på hvem som endret hvem, i hvilken rekkefølge og hvorfor.
Denne Flux-opplæringen vil demonstrere hvordan Flux-mønsteret takler disse problemene med imponerende letthet og enkelhet.
Flux sitt slagord er 'ensrettet dataflyt'. Her er et praktisk diagram fra Flux dokumenterer viser hvordan strømmen ser ut:
Den viktige biten er at ting flyter fra React --> Dispatcher --> Stores --> React
.
La oss se på hva hver av hovedkomponentene er og hvordan de kobles til:
Dokumentene gir også denne viktige advarselen:
Flux er mer et mønster enn et rammeverk, og har ingen harde avhengigheter. Imidlertid bruker vi ofte EventEmitter som grunnlag for Stores og Reager for våre synspunkter. Det ene stykket Flux som ikke er lett tilgjengelig andre steder, er Dispatcher. Denne modulen er tilgjengelig her for å fullføre din Flux verktøykasse.
Så Flux har tre komponenter:
React = require('react')
)Dispatcher = require('flux').Dispatcher
)EventEmitter = require('events').EventEmitter
)Backbone = require('backbone')
)Jeg vil ikke beskrive React her, siden det er skrevet så mye om det, annet enn å si at jeg sterkt foretrekker det fremfor Angular. Jeg føler nesten aldri forvirret når du skriver React code, i motsetning til Angular, men selvfølgelig vil meningene variere.
Flux Dispatcher er et enkelt sted der alle hendelser som endrer butikkene dine blir håndtert. For å bruke den, har du hver butikk register
en enkelt tilbakeringing for å håndtere alle hendelser. Så, når du vil endre en butikk, vil du dispatch
en hendelse.
I likhet med React, ser Dispatcher meg som en god idé, implementert godt. Som et eksempel kan en app som lar brukeren legge til elementer i en oppgaveliste inneholde følgende:
# 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!'
Dette gjør det veldig enkelt å svare på to spørsmål:
MyStore
?switch
uttalelse i MyStore.dispatchCallback
.actionType
.Dette er mye lettere enn for eksempel å se etter MyModel.set
og MyModel.save
og MyCollection.add
osv. hvor sporing av svarene på disse grunnleggende spørsmålene blir veldig vanskelig veldig fort.
Dispatcher lar deg også få tilbakeringinger kjørt sekvensielt på en enkel, synkron måte ved hjelp av waitFor
For eksempel:
# 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
I praksis var jeg sjokkert over å se hvor mye renere koden min var når jeg brukte Dispatcher til å endre butikkene mine, selv uten å bruke waitFor
.
Så datastrømmer inn i Butikker gjennom Dispatcher. Har det. Men hvordan flyter data fra butikkene til utsikten (dvs. reagerer)? Som nevnt i Flux dokumenterer :
[Visningen] lytter til hendelser som sendes av butikkene som den avhenger av.
Ok, fint. Akkurat som vi registrerte tilbakeringinger i butikkene våre, registrerer vi tilbakeringinger med våre Views (som er React Components). Vi ber React om å re- render
hver gang det skjer en endring i butikken som ble sendt inn gjennom props
.
For eksempel:
# 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
Rått!
Så hvordan sender vi ut det 'change'
begivenhet? Flux anbefaler å bruke EventEmitter
. Fra et offisielt eksempel:
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...
Ekkelt! Jeg må skrive alt det selv, hver gang jeg vil ha en enkel butikk? Hvilket jeg skal bruke hver gang jeg har en informasjon jeg vil vise? Det må være en bedre måte!
Backbones modeller og samlinger har allerede alt Fluxs EventEmitter-baserte butikker ser ut til å gjøre.
Ved å fortelle deg å bruke rå EventEmitter, anbefaler Flux at du gjenskaper kanskje 50-75% av Backbones modeller og samlinger hver gang du oppretter en butikk. Å bruke EventEmitter for butikkene dine er som å bruke bare Node.js til serveren din når det allerede er velbygde mikrorammer som Express.js eller tilsvarende for å ta vare på alt det grunnleggende og kjele.
Akkurat som Express.js er bygget på Node.js, er Backbones modeller og samlinger bygget på EventEmitter. Og den har alle ting du stort sett alltid trenger: Ryggraden avgir change
hendelser og har spørringsmetoder, getters og setters og alt. Pluss ryggrad Jeremy Ashkenas og hans hær av 230 bidragsytere gjorde en mye bedre jobb med alle disse tingene enn det jeg sannsynligvis vil klare.
Som et eksempel på denne Backbone-opplæringen konverterte jeg MessageStore-eksemplet ovenfra til en ryggradsversjon .
Det er objektivt mindre kode (ikke nødvendig å duplisere arbeid) og er subjektivt mer tydelig og kortfattet (for eksempel this.add(message)
i stedet for _messages[message.id] = message
).
Så la oss bruke Backbone for Stores!
Denne opplæringen er grunnlaget for en tilnærming jeg stolt har kalt FluxBone , Flux-arkitekturen som bruker Backbone for Stores. Her er det grunnleggende mønsteret i en FluxBone-arkitektur:
.set()
). I stedet sender komponenter handlinger til utsenderen.
La oss bruke eksempler på ryggrad og flux for å se på hvert stykke av det igjen:
1. Butikker er instantierte ryggradsmodeller eller samlinger som har registrert en tilbakeringing hos 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. Komponenter aldri modifiser Stores direkte (for eksempel nei .set()
). I stedet sender komponenter handlinger til utsenderen.
# 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. Komponenter spørre Lagrer og binder til hendelsene deres for å utløse oppdateringer.
# 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
Jeg har brukt denne Flux and Backbone-tilnærmingen til mine egne prosjekter, og når jeg arkitekterte React-applikasjonen for å bruke dette mønsteret, forsvant nesten alle de stygge bitene. Det var litt mirakuløst: kodebitene som fikk meg til å gnage tennene på jakt etter en bedre måte, ble erstattet av en fornuftig flyt. Og glattheten som Backbone ser ut til å integreres i dette mønsteret er bemerkelsesverdig: Jeg føler ikke at jeg kjemper mot Backbone, Flux eller React for å få dem sammen i en enkelt applikasjon.
Skriver this.on(...)
og this.off(...)
kode hver gang du legger til en FluxBone Store i en komponent kan bli litt gammel.
Her er et eksempel på React Mixin som, selv om det er ekstremt naivt, absolutt ville gjøre iterering raskt enda enklere:
# 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') )
I det opprinnelige Flux-diagrammet samhandler du bare med Web API gjennom ActionCreators, som krever svar fra serveren før du sender handlinger til Dispatcher. Som aldri satt riktig med meg; burde ikke butikken være den første som fikk vite om endringer før serveren?
Jeg velger å snu den delen av diagrammet: Butikkene samhandler direkte med en RESTful CRUD API gjennom Backbone's sync()
Dette er fantastisk praktisk, i det minste hvis du jobber med en faktisk RESTful CRUD API.
Dataintegriteten opprettholdes uten problemer. Når du .set()
en ny eiendom, change
hendelse utløser en gjengivelse av React, og viser de nye dataene optimistisk. Når du prøver å .save()
den til serveren, request
hendelse forteller deg om å vise et lasteikon. Når ting går gjennom, blir sync
hendelsen forteller deg om å fjerne lasteikonet, eller error
hendelse forteller deg om å gjøre ting rødt. Du kan se inspirasjon her .
Det er også validering (og en tilsvarende invalid
hendelse) for et første forsvarslag, og en .fetch()
metode for å hente ny informasjon fra serveren.
For mindre standardoppgaver kan samhandling via ActionCreators være mer fornuftig. Jeg mistenker at Facebook ikke gjør mye 'bare CRUD', i så fall er det ikke overraskende at de ikke setter butikker først.
Ingeniørlagene på Facebook har gjort bemerkelsesverdig arbeid for skyv frontendebanen fremover med Reagere , og introduksjonen av Flux gir et innblikk i en bredere arkitektur som virkelig skalerer: ikke bare når det gjelder teknologi, men også engineering. Smart og forsiktig bruk av Backbone (ifølge eksemplet på denne veiledningen) kan fylle hullene i Flux, noe som gjør det utrolig enkelt for alle fra en-person-indiebutikker til store selskaper å lage og vedlikeholde imponerende applikasjoner.
I slekt: Hvordan reagerer komponenter gjør det enkelt å teste UI