Velkommen tilbake til den andre spennende delen av Avdekke ClojureScript ! I dette innlegget skal jeg dekke det neste store trinnet for å bli seriøs med ClojureScript: statsadministrasjon - i dette tilfellet ved hjelp av React.
Med front-end-programvare er statsadministrasjon en stor avtale. Out-of-the-box, det er et par måter å håndtere tilstand i Reagere :
Generelt sett er ingen av disse flotte. Det er ganske enkelt å holde tilstanden på toppnivå, men da er det mye overhead å overføre applikasjonstilstanden til hver komponent som trenger det.
Til sammenligning kan det å ha globale variabler (eller andre naive versjoner av staten) føre til vanskelige å spore samtidighetsproblemer, noe som fører til at komponenter ikke oppdateres når du forventer det, eller omvendt.
Så hvordan kan dette takles? For de av dere som er kjent med React, har du kanskje prøvd Redux, en statscontainer for JavaScript apper. Du har kanskje funnet dette ut av din egen vilje, og dristig søkt etter et håndterbart system for å opprettholde staten. Eller du har nettopp snublet over det mens du leste om JavaScript og annet verktøy på nettet.
Uansett hvordan folk ender opp med å se på Redux, etter min erfaring ender de vanligvis med to tanker:
Generelt gir Redux en abstraksjon som lar statlig ledelse passe inn i reaktiv karakteren av React. Ved å laste ut all statligheten til et system som Redux, bevarer du renhet av React. Dermed vil du ende opp med mye mindre hodepine og generelt noe det er mye lettere å resonnere om.
Selv om dette kanskje ikke hjelper deg å lære ClojureScript helt fra bunnen av, vil jeg her i det minste oppsummere noen grunnleggende tilstandskonsepter i Clojure [Script]. Hopp over disse delene hvis du allerede er en erfaren Clojurian !
Husk ett av de grunnleggende om Clojure som også gjelder ClojureScript: Som standard data er uforanderlig . Dette er bra for å utvikle og ha garantier for at det du oppretter ved tidsteg N fremdeles er det samme ved tidsteg> N. ClojureScript gir oss også en praktisk måte å ha en mutbar tilstand hvis vi trenger det, via atom
konsept.
En atom
i ClojureScript er veldig lik en AtomicReference
i Java: Det gir et nytt objekt som låser innholdet med samtidighetsgarantier. Akkurat som i Java, kan du plassere hva du vil i dette objektet - fra da av vil det atomet være en atomreferanse til hva du vil.
Når du har din atom
, kan du atomisk sette en ny verdi i den ved å bruke reset!
funksjon (legg merke til !
i funksjonen — i Clojure-språket brukes dette ofte for å betegne at en operasjon er stateful eller uren).
Vær også oppmerksom på at - i motsetning til Java - bryr Clojure seg ikke hva du legger i atom
. Det kan være en streng, en liste eller et objekt. Dynamisk skriving, baby!
(def my-mutable-map (atom {})) ; recall that {} means an empty map in Clojure (println @my-mutable-map) ; You 'dereference' an atom using @ ; -> this prints {} (reset! my-mutable-map {:hello 'there'}) ; atomically set the atom (reset! my-mutable-map 'hello, there!') ; don't forget Clojure is dynamic :)
Reagens utvider dette konseptet med et atom med sitt eget atom
. (Hvis du ikke er kjent med Reagent, sjekk ut innlegget før dette .) Dette oppfører seg identisk med ClojureScript atom
, bortsett fra at det også utløser gjengivelseshendelser i Reagent, akkurat som React's innebygde statsbutikk.
Et eksempel:
(ns example (:require [reagent.core :refer [atom]])) ; in this module, atom now refers ; to reagent's atom. (def my-atom (atom 'world!')) (defn component [] [:div [:span 'Hello, ' @my-atom] [:input {:type 'button' :value 'Press Me!' :on-click #(reset! My-atom 'there!')}]])
Dette viser en enkelt som inneholder sier 'Hei, verden!' og en knapp, som du kanskje forventer. Ved å trykke på denne knappen vil atomisk mutere
my-atom
å inneholde 'there!'
. Det vil utløse en tegning på nytt av komponenten, og resultere i at spennet sier 'Hei, der!' i stedet.
Dette virker enkelt nok for lokal mutasjon på komponentnivå, men hva om vi har en mer komplisert applikasjon som har flere abstraksjonsnivåer? Eller hvis vi trenger å dele felles tilstand mellom flere underkomponenter, og deres underkomponenter?
La oss utforske dette med et eksempel. Her skal vi implementere en grov påloggingsside:
(ns unearthing-clojurescript.login (:require [reagent.core :as reagent :refer [atom]])) ;; -- STATE -- (def username (atom nil)) (def password (atom nil)) ;; -- VIEW -- (defn component [on-login] [:div [:b 'Username'] [:input {:type 'text' :value @username :on-change #(reset! username (-> % .-target .-value))}] [:b 'Password'] [:input {:type 'password' :value @password :on-change #(reset! password (-> % .-target .-value))}] [:input {:type 'button' :value 'Login!' :on-click #(on-login @username @password)}]])
Vi vil da være vert for denne påloggingskomponenten i hoved app.cljs
, slik:
(ns unearthing-clojurescript.app (:require [unearthing-clojurescript.login :as login])) ;; -- STATE (def token (atom nil)) ;; -- LOGIC -- (defn- do-login-io [username password] (let [t (complicated-io-login-operation username password)] (reset! token t))) ;; -- VIEW -- (defn component [] [:div [login/component do-login-io]])
Den forventede arbeidsflyten er således:
do-login-io
funksjon i foreldrekomponenten.do-login-io
funksjon gjør noen I / O-operasjoner (for eksempel å logge på en server og hente et token).Hvis denne operasjonen blokkerer, er vi allerede i en haug med problemer, ettersom søknaden vår er frossen - hvis den ikke er det, har vi asynkronisering å bekymre oss for!
I tillegg må vi nå gi dette token til alle underkomponentene våre som vil gjøre spørsmål til serveren vår. Kodereformering ble bare mye vanskeligere!
Endelig er komponenten vår nå ikke lenger rent reaktiv —Det er nå medskyldig i å administrere tilstanden til resten av applikasjonen, utløse I / O og generelt være litt plage.
Redux er tryllestaven som gjør at alle dine statsbaserte drømmer går i oppfyllelse. Riktig implementert, gir den en delingsabstraksjon som er trygg, rask og enkel å bruke.
Det indre arbeidet til Redux (og teorien bak det) ligger noe utenfor omfanget av denne artikkelen. I stedet vil jeg dykke inn i et fungerende eksempel med ClojureScript, som forhåpentligvis skal gå noen vei for å demonstrere hva det er i stand til!
I vår sammenheng er Redux implementert av et av de mange tilgjengelige ClojureScript-bibliotekene; denne kalte omramme . Det gir en Clojure-ified wrapper rundt Redux som (etter min mening) gjør det til en absolutt glede å bruke.
Redux løfter ut søknadstilstanden din, og lar komponentene være lette. En Reduxified-komponent trenger bare å tenke på:
Resten håndteres i kulissene.
For å understreke dette punktet, la oss redusere påloggingssiden vår på nytt.
Første ting først: Vi må bestemme hvordan applikasjonsmodellen vår skal se ut. Vi gjør dette ved å definere form av dataene våre, data som vil være tilgjengelige i hele appen.
En god tommelfingerregel er at hvis dataene må brukes på tvers av flere Redux-komponenter, eller de må ha lang levetid (slik som tokenet vårt vil være), så skal de lagres i databasen. Derimot, hvis dataene er lokale for komponenten (for eksempel brukernavnet og passordfeltene våre), bør de leve som lokal komponenttilstand og ikke lagres i databasen.
La oss lage vår database kjele og spesifisere token vår:
(ns unearthing-clojurescript.state.db (:require [cljs.spec.alpha :as s] [re-frame.core :as re-frame])) (s/def ::token string?) (s/def ::db (s/keys :opt-un [::token])) (def default-db {:token nil})
Det er noen interessante punkter som er verdt å merke seg her:
spec
bibliotek til beskriver hvordan dataene våre skal se ut. Dette er spesielt passende i et dynamisk språk som Clojure [Script].:opt-un
søkeord, som står for 'valgfritt, ukvalifisert.' (I Clojure vil et vanlig nøkkelord være noe sånt som :cat
, mens et kvalifisert nøkkelord kan være noe sånt som :animal/cat
. Kvalifisering foregår normalt på modulnivå - dette hindrer nøkkelord i forskjellige moduler fra å knuse hverandre .)Når som helst, bør vi være sikre på at dataene i databasen vår samsvarer med spesifikasjonene våre her.
Nå som vi har beskrevet datamodellen vår, må vi reflektere hvordan vår utsikt viser dataene. Vi har allerede beskrevet hvordan vårt syn ser ut i Redux-komponenten vår - nå må vi bare koble vårt syn til databasen vår.
Med Redux får vi ikke direkte tilgang til databasen vår - dette kan føre til problemer med livssyklus og samtidighet. I stedet registrerer vi vårt forhold til en fasett av databasen gjennom abonnement .
Et abonnement forteller omramming (og reagens) at vi er avhengige av en del av databasen, og hvis den delen endres, bør Redux-komponenten vår gjengis på nytt.
Abonnementer er veldig enkle å definere:
(ns unearthing-clojurescript.state.subs (:require [re-frame.core :refer [reg-sub]])) (reg-sub :token ; <- the name of the subscription (fn [{:keys [token] :as db} _] ; first argument is the database, second argument is any token)) ; args passed to the subscribe function (not used here)
Her registrerer vi et enkelt abonnement — på selve tokenet. Et abonnement er ganske enkelt navnet på abonnementet, og funksjonen som trekker ut elementet fra databasen. Vi kan gjøre hva vi vil til den verdien, og mutere utsikten så mye vi vil her; i dette tilfellet trekker vi imidlertid ut tokenet fra databasen og returnerer det.
Det er mye, mye mer kan du gjøre med abonnementer - for eksempel å definere visninger på underseksjoner i databasen for å gi et større omfang på gjengivelse - men vi holder det enkelt for nå!
Vi har vår database, og vi har vårt syn på databasen. Nå må vi utløse noen hendelser! I dette eksemplet har vi to typer hendelser:
Vi begynner med den enkle. Re-frame gir til og med en funksjon nøyaktig for denne typen hendelser:
(ns unearthing-clojurescript.state.events (:require [re-frame.core :refer [reg-event-db reg-event-fx reg-fx] :as rf] [unearthing-clojurescript.state.db :refer [default-db]])) ; our start up event that initialises the database. ; we'll trigger this in our core.cljs (reg-event-db :initialise-db (fn [_ _] default-db)) ; a simple event that places a token in the database (reg-event-db :store-login (fn [db [_ token]] (assoc db :token token)))
Igjen, det er ganske greit her - vi har definert to hendelser. Den første er for initialisering av databasen vår. (Se hvordan den ignorerer begge argumentene? Vi initialiserer alltid databasen med default-db
!) Det andre er for lagring av token når vi har fått den.
Legg merke til at ingen av disse hendelsene har bivirkninger - ingen eksterne samtaler, ingen I / O i det hele tatt! Dette er veldig viktig for å bevare helligheten til den hellige Redux-prosessen. Ikke gjør det urent for at du ikke ønsker Reduxs vrede over deg.
Til slutt trenger vi påloggingshendelsen. Vi legger den under de andre:
(reg-event-fx :login (fn [{:keys [db]} [_ credentials]] {:request-token credentials})) (reg-fx :request-token (fn [{:keys [username password]}] (let [token (complicated-io-login-operation username password)] (rf/dispatch [:store-login token]))))
reg-event-fx
funksjonen ligner stort sett på reg-event-db
, selv om det er noen subtile forskjeller.
reg-event-db
.db
, returnerer vi i stedet et kart som representerer alle effektene (“fx”) som skal skje for denne hendelsen. I dette tilfellet kaller vi ganske enkelt :request-token
effekt, som er definert nedenfor. En av de andre gyldige effektene er :dispatch
, som ganske enkelt kaller en annen hendelse.Når effekten vår er sendt, vår :request-token
effekten kalles, som utfører vår langvarige I / O-påloggingsoperasjon. Når dette er ferdig, sender det gjerne resultatet tilbake til hendelsessløyfen, og fullfører dermed syklusen!
Så! Vi har definert vår lagringsabstrahering. Hvordan ser komponenten ut nå?
(ns unearthing-clojurescript.login (:require [reagent.core :as reagent :refer [atom]] [re-frame.core :as rf])) ;; -- STATE -- (def username (atom nil)) (def password (atom nil)) ;; -- VIEW -- (defn component [] [:div [:b 'Username'] [:input {:type 'text' :value @username :on-change #(reset! username (-> % .-target .-value))}] [:b 'Password'] [:input {:type 'password' :value @password :on-change #(reset! password (-> % .-target .-value))}] [:input {:type 'button' :value 'Login!' :on-click #(rf/dispatch [:login {:username @username :password @password]})}]])
Og vår app-komponent:
(ns unearthing-clojurescript.app (:require [unearthing-clojurescript.login :as login])) ;; -- VIEW -- (defn component [] [:div [login/component]])
Og til slutt er det enkelt å få tilgang til tokenet vårt i en ekstern komponent:
(let [token @(rf/subscribe [:token])] ; ... )
Sette alt sammen:
Ingen føtter, ingen må.
Ved å bruke Redux (via re-frame), frakoblet vi visningskomponentene våre fra rotet til statlig håndtering. Å utvide vår statlige abstraksjon er nå et stykke kake!
Redux i ClojureScript egentlig er så enkelt - du har ingen unnskyldning for ikke å prøve.
Hvis du er klar til å ta knekken, anbefaler jeg at du sjekker ut de fantastiske omrammingsdokumentene og vårt enkle bearbeidede eksempel . Jeg ser frem til å lese kommentarene dine på denne ClojureScript-opplæringen nedenfor. Lykke til!
I slekt: Statlig ledelse i kantet bruk av FirebaseRedux-staten refererer til enkeltbutikken som Redux bruker for å administrere applikasjonstilstanden. Denne butikken kontrolleres utelukkende av Redux og er ikke direkte tilgjengelig fra selve applikasjonen.
Nei, Redux er en separat teknologi fra mønsteret kjent som sourcing av hendelser. Redux ble inspirert av en annen teknologi som heter Flux.
En Redux-container (eller ganske enkelt en 'container') er en React-komponent som abonnerer på Redux-tilstanden, og mottar oppdateringer når den delen av staten endres.
Ja, Redux gir et rammeverk rundt statlig forvaltning i en webapplikasjon.
ClojureScript er en kompilator for Clojure som retter seg mot JavaScript. Det brukes ofte til å bygge webapplikasjoner og biblioteker ved hjelp av Clojure-språket.