af Michael Sargent

et routingbibliotek er en nøglekomponent i enhver kompleks applikation på en side. Hvis du udvikler apps med React, har du sikkert brugt eller i det mindste hørt om React Router. Det er et velkendt routingbibliotek til React, og en fantastisk løsning til mange brugssager.

men React Router er ikke den eneste levedygtige løsning i React-økosystemet. Faktisk er der masser af routingløsninger bygget til React og til Reduks, hver med forskellige API ‘ er, funktioner og mål — og listen vokser kun. Det er overflødigt at sige, at routing på klientsiden ikke forsvinder når som helst snart, og der er stadig meget plads til design i morgendagens routingbiblioteker.

i dag vil jeg gerne gøre opmærksom på emnet for routing i Reduks. Jeg vil præsentere og gøre en sag for Redouch-first routing – et paradigme, der gør Redouch til stjernen i routingmodellen og den fælles tråd blandt mange Redouch routing-løsninger. Jeg vil demonstrere, hvordan man sammensætter kernen, ramme-agnostisk API i Under 100 linjer kode, før man udforsker muligheder for brug i den virkelige verden med React og andre front-end rammer.

en lille historie

placeringen (URL-oplysninger) og sessionshistorikken (en stak steder besøgt af den aktuelle fane) gemmes i det globale window objekt. De er tilgængelige via:

  • window.location (placering API)
  • window.history (historie API).

History API tilbyder følgende historiknavigationsmetoder, der er bemærkelsesværdige for deres evne til at opdatere bro. sererens historie og placering uden at nødvendiggøre en genindlæsning af siden:

  • pushState(href) — skubber en ny placering på historiestakken
  • replaceState(href) — overskriver den aktuelle placering på stakken
  • back() — navigerer til den forrige placering på stakken
  • forward() — navigerer til den næste placering på stakken
  • go(index) — navigerer til et sted på stakken, i begge retninger.

sammen muliggør historie-og placerings — API ‘ erne det moderne routingparadigme på klientsiden kendt som pushState routing-den første hovedperson i vores historie.

nu er det næsten en forbrydelse at nævne historie og placering API ‘ er uden at nævne et moderne indpakningsbibliotek som history.

ReactTraining / historie
Administrer sessionshistorik med JavaScriptgithub.com

history giver en enkel, men kraftfuld API til grænseflade med bro.serhistorikken og placeringen, mens den dækker uoverensstemmelser mellem forskellige bro. ser-implementeringer. Det bruges som en peer eller intern afhængighed i mange moderne routingbiblioteker, og jeg vil henvise til det i hele denne artikel.

omdirigering og pushState Routing

den anden hovedperson i vores historie er Reduks. Det er 2017, så jeg sparer dig for introduktionen og kommer lige til det punkt:

ved at bruge almindelig pushState-routing i et program, deler vi applikationstilstanden på tværs af to domæner: bro.serhistorik og butik.

Sådan ser det ud med React Router, som instantierer og ombrydes history:

history → React Router ↘ view Redux ↗

nu ved vi, at ikke alle data skal opholde sig i butikken. For eksempel er lokal komponenttilstand ofte et passende sted at gemme data, der er specifikke for en enkelt komponent.

men placeringsdata er ikke trivielle. Det er en dynamisk og vigtig del af applikationstilstanden — den slags data, der hører hjemme i butikken. Hvis du holder den i butikken, kan du bruge luksus som tidsrejsefejlfinding og nem adgang fra enhver butikstilsluttet komponent.

så hvordan flytter vi placeringen ind i butikken?

der er ingen vej udenom, at bro.sereren læser og gemmer historik og placeringsoplysninger i window, men hvad vi kan gøre er at opbevare en kopi af placeringsdataene i butikken og holde dem synkroniseret med bro. sereren.

er det ikke hvad react-router-redux gør for React Router?

Ja, men kun for at aktivere mulighederne for tidsrejser i DevTools. Applikationen afhænger stadig af placeringsdata, der opbevares i React Router:

history → React Router ↘ ↕ view Redux ↗

brug af react-router-redux til at læse placeringsdata fra butikken i stedet for React Router frarådes (på grund af potentielt modstridende kilder til sandhed).

kan vi gøre det bedre?

kan vi bygge en alternativ routingmodel — en model, der er bygget fra bunden til at spille godt med reduk, så vi kan læse og opdatere placeringen på Reduks måde — med store.getState() og store.dispatch()?

vi kan absolut, og det kaldes reduce-first routing.

første Routing

ruteflyvning er en variation af pushState routing, der gør ruteflyvning til stjernen i rutemodellen.

en Routingløsning opfylder følgende kriterier:

  • placeringen afholdes i butikken.
  • placeringen ændres ved at sende redigeringshandlinger.
  • applikationen læser placeringsdata udelukkende fra butikken.
  • butikken og bro.serhistorikken holdes synkroniseret bag kulisserne.

her er en grundlæggende ide om, hvordan det ser ud:

history ↕ Redux → router → view

Vent, er der ikke stadig to kilder til placeringsdata?

Ja, men hvis vi kan stole på, at bro.serhistorikken og butikken er synkroniseret, kan vi bygge vores applikationer til kun nogensinde at læse placeringsdata fra butikken. Derefter er der fra applikationens synspunkt kun en kilde til sandhed — butikken.

Hvordan opnår vi den første routing?

vi kan starte med at oprette en konceptuel model ved at flette de grundlæggende elementer i klientsiden routing og reduce data lifecycle modeller.

revision af klientsiden Routing Model

routing på klientsiden er en flertrinsproces, der starter med navigation og slutter med gengivelse — routing i sig selv er kun et trin i denne proces! Lad os gennemgå detaljerne:

  • Navigation-Alt starter med en ændring i placering. Der er 2 typer navigation: intern og ekstern. Intern navigation opnås fra inden app (f. eks. via historie API), mens ekstern navigation opstår, når brugeren interagerer med navigationslinjen eller går ind i applikationen fra et eksternt sted.
  • svar på navigation — når placeringen ændres, reagerer applikationen ved at overføre den nye placering til routeren. Ældre routing teknikker påberåbt polling window.location for at opnå dette, men i dag har vi den handy history.listen nytte.
  • Routing — dernæst matches den nye placering med det tilsvarende sideindhold. Koden, der håndterer dette trin, kaldes en router, og det tager generelt en inputparameter for matchende ruter og sider kaldet en rutekonfiguration.
  • Rendering — endelig gengives indholdet på klienten. Dette trin kan selvfølgelig håndteres af en front-end ramme/bibliotek som React.

Bemærk, at routingbiblioteker ikke behøver at håndtere alle dele af routingmodellen.

nogle biblioteker, som React Router og Vue Router, gør – mens andre, som Universal Router, kun beskæftiger sig med et enkelt aspekt( som routing), hvilket giver fleksibilitet i de andre aspekter:

Routing biblioteker kan have forskellige ansvarsområder. (Klik for større billede)

gennemgang af Livscyklusmodellen for data

en envejs datastrøm / livscyklusmodel, der sandsynligvis ikke behøver nogen introduktion — men her er en kort oversigt for godt mål:

  • handling — enhver ændring i tilstand starter ved at sende en ny handling (et almindeligt objekt, der indeholder en type og valgfri nyttelast).
  • mellemvare — handlingen passerer gennem butikens kæde af mellemvare, hvor handlinger kan opfanges og yderligere adfærd kan udføres. Mellemvarer bruges ofte til at håndtere bivirkninger i Reduks-applikationer.
  • Reducer — handlingen når derefter rodreduktionen, som beregner butikens næste tilstand som en ren funktion af den forrige tilstand og den modtagne handling. Rodreduktionen kan være sammensat af individuelle reduktionsmidler, der hver håndterer en skive af butikens tilstand.
  • ny tilstand — butikken gemmer den nye tilstand, der returneres af reduktionsapparatet, og underretter sine abonnenter om ændringen (i React, via connect).
  • Rendering — endelig kan den store-tilsluttede visning gengives i overensstemmelse med den nye tilstand.

opbygning af en første rutemodel

den ensrettede karakter af klientsiden routing og reduce data livscyklus modeller egner sig godt til en fusioneret model, der opfylder de kriterier, vi lagt ud for Reduce-første routing.

i denne model abonnerer routeren på butikken, navigationen udføres via handlinger, og opdateringer til bro.serhistorikken håndteres af en brugerdefineret mellemvare. Lad os undersøge detaljerne i denne model:

  • intern navigation via redigeringshandlinger — i stedet for at bruge History API direkte opnås intern navigation ved at sende en af 5 navigationshandlinger, der afspejler historiknavigationsmetoderne.
  • opdatering af bro.serhistorikken via mellemvare — en mellemvare bruges til at opfange navigationshandlingerne og håndtere bivirkningen ved opdatering af bro. serhistorikken. Da den nye placering ikke nødvendigvis eller let er kendt uden først at konsultere bro.serhistorikken (f. eks. i tilfælde af en go handling) forhindres navigationshandlingerne i at nå reduceren.
  • svar på navigation-strømmen af udførelse fortsætter med en history lytter, der reagerer på navigation (fra både mellemvaren og ekstern navigation) ved at sende en anden handling, der indeholder den nye placering.
  • Placeringsreduktion — handlingen, der sendes af lytteren, når derefter placeringsreduktionen, som tilføjer placeringen til butikken. Placeringsreduktionen bestemmer også formen på placeringstilstanden.
  • tilsluttet routing – den store-tilsluttede router kan derefter reaktivt bestemme det nye sideindhold, når den får besked om en ændring i placering i butikken.
  • Rendering — endelig kan siden gengives med det nye indhold.

Bemærk, at dette ikke er den eneste måde at udføre routing på-nogle variationer har brugen af en butiksforstærker og/eller yderligere logik i mellemvaren — men det er en simpel model, der dækker alle baserne.

en grundlæggende implementering

efter den model, vi lige har kigget på, lad os implementere core API — handlingerne, mellemvaren, lytteren og reduceren.

vi bruger pakken history som en intern afhængighed og bygger løsningen trinvist. Hvis du hellere vil følge med i det endelige resultat, kan du se det her.

handlinger

vi starter med at definere de 5 navigationshandlinger, der afspejler historiknavigationsmetoderne:

// constants.jsexport const PUSH = 'ROUTER/PUSH';export const REPLACE = 'ROUTER/REPLACE';export const GO = 'ROUTER/GO';export const GO_BACK = 'ROUTER/GO_BACK';export const GO_FORWARD = 'ROUTER/GO_FORWARD';
// actions.jsexport const push = (href) => ({ type: PUSH, payload: href,});

export const replace = (href) => ({ type: REPLACE, payload: href,});
export const go = (index) => ({ type: GO, payload: index,});
export const goBack = () => ({ type: GO_BACK,});
export const goForward = () => ({ type: GO_FORWARD,});

mellemvare

lad os derefter definere mellemvaren. Det skal opfange navigationshandlingerne, kalde de tilsvarende history navigationsmetoder og derefter stoppe handlingen fra at nå reduceren — men lad alle andre handlinger være uforstyrrede:

// middleware.jsexport const routerMiddleware = (history) => () => (next) => (action) => { switch (action.type) { case PUSH: history.push(action.payload); break; case REPLACE: history.replace(action.payload); break; case GO: history.go(action.payload); break; case GO_BACK: history.goBack(); break; case GO_FORWARD: history.goForward(); break; default: return next(action); }};

hvis du ikke har haft chancen for at skrive eller undersøge det indre af en Redou-mellemvare før, skal du tjekke denne introduktion.

Historielytter

Dernæst har vi brug for en history lytter, der reagerer på navigation ved at sende en ny handling, der indeholder de nye placeringsoplysninger.

lad os først tilføje den nye handlingstype og skaber. De interessante dele af lokationen er pathname, search og hash — så det er hvad vi vil medtage i nyttelasten:

// constants.jsexport const LOCATION_CHANGE = 'ROUTER/LOCATION_CHANGE';
// actions.jsexport const locationChange = ({ pathname, search, hash }) => ({ type: LOCATION_CHANGE, payload: { pathname, search, hash, },});

så lad os skrive lytterfunktionen:

// listener.jsexport function startListener(history, store) { history.listen((location) => { store.dispatch(locationChange({ pathname: location.pathname, search: location.search, hash: location.hash, })); });}

vi laver en lille tilføjelse-en indledende locationChange forsendelse for at redegøre for den første indtastning i applikationen (som ikke bliver hentet af historielytteren):

// listener.jsexport function startListener(history, store) { store.dispatch(locationChange({ pathname: history.location.pathname, search: history.location.search, hash: history.location.hash, }));
 history.listen((location) => { store.dispatch(locationChange({ pathname: location.pathname, search: location.search, hash: location.hash, })); });}

Reducer

lad os derefter definere placeringsreduktionen. Vi bruger en simpel tilstandsform og gør minimalt arbejde i reduceren:

// reducer.jsconst initialState = { pathname: '/', search: '', hash: '',};
export const routerReducer = (state = initialState, action) => { switch (action.type) { case LOCATION_CHANGE: return { ...state, ...action.payload, }; default: return state; }};

applikationskode

endelig, lad os tilslutte vores API til applikationskoden:

// index.jsimport { combineReducers, applyMiddleware, createStore } from 'redux'import { createBrowserHistory } from 'history'import { routerReducer } from './reducer'import { routerMiddleware } from './middleware'import { startListener } from './listener'import { push } from './actions'
// Create the history objectconst history = createBrowserHistory()
// Build the root reducerconst rootReducer = combineReducers({ // ...otherReducers, router: routerReducer,}) // Build the middlewareconst middleware = routerMiddleware(history)
// Create the storeconst store = createStore(rootReducer, {}, applyMiddleware(middleware))
// Start the history listenerstartListener(history, store)
// Now you can read location data from the store!let currentLocation = store.getState().router.pathname
// You can also subscribe to changes in the location!let unsubscribe = store.subscribe(() => { let previousLocation = currentLocation currentLocation = store.getState().router.pathname
 if (previousLocation !== currentLocation) { // You can render your application reactively here! }})
// And you can dispatch navigation actions from anywhere!store.dispatch(push('/about'))

og det er alt der er til det! Ved hjælp af vores lille (Under 100 linjer kode) API, har vi opfyldt alle kriterierne for:

  • placeringen afholdes i butikken.
  • placeringen ændres ved at sende redigeringshandlinger.
  • applikationen læser placeringsdata udelukkende fra butikken.
  • butikken og bro.ser historie holdes i sync bag kulisserne. ✔

se alle filerne sammen her — du er velkommen til at importere dem til dit projekt, eller brug det som udgangspunkt for at udvikle din egen implementering.

den første routing pakke

jeg har også sat API sammen i redux-first-routing pakken, som du kan npm install og bruge på samme måde.

mksarge/reduce-first-routing
reduce-first-routing — en minimal, ramme-agnostisk base til udførelse af reduce-first routing.github.com

det inkluderer en implementering, der ligner den, vi byggede her, men med den bemærkelsesværdige tilføjelse af forespørgselsparsing via pakken query-string.

Vent-hvad med den faktiske routing komponent?

du har måske bemærket, at redux-first-routing kun beskæftiger sig med navigationsaspektet af rutemodellen:

ved at afkoble navigationsaspektet fra de andre aspekter af vores routingmodel har vi fået en vis fleksibilitet — redux-first-routing er både router-agnostiker og ramme-agnostiker.

du kan derfor parre det med et bibliotek som Universal Router for at oprette en komplet routingløsning til enhver frontend-ramme:

Klik her for at komme i gang med .

eller du kan opbygge meningsfulde bindinger til din valgte ramme — og det er hvad vi vil gøre for at reagere i det næste og sidste afsnit af denne artikel.

anvendelse med React

lad os afslutte vores udforskning ved at se på, hvordan vi kan opbygge butikstilsluttede komponenter til deklarativ navigation og routing i React.

deklarativ Navigation

til navigation kan vi bruge en butikstilsluttet<Lin k/ > komponent svarende til den i React Router og andre React routing-løsninger.

det tilsidesætter simpelthen standardadfærden for ankerelementet < a / > og dispatc hes en push-handling, når der klikkes på:

// Link.jsimport React from 'react';import { connect } from 'react-redux';import { push as pushAction, replace as replaceAction } from './actions';
const Link = (props) => { const { to, replace, children, dispatch, ...other } = props;
 const handleClick = (event) => { // Ignore any click other than a left click if ((event.button && event.button !== 0) || event.metaKey || event.altKey || event.ctrlKey || event.shiftKey || event.defaultPrevented === true) { return; } // Prevent the default behaviour (page reload, etc.) event.preventDefault();
 // Dispatch the appropriate navigation action if (replace) { dispatch(replaceAction(to)); } else { dispatch(pushAction(to)); } };
 return ( <a href={to} onClick={handleClick} {...other}> {children} </a>);};
export default connect()(Link);

du kan finde en mere komplet implementering her.

deklarativ Routing

selvom der ikke er meget til en navigationskomponent, er der utallige måder at designe en routingkomponent på — hvilket gør det til den mest interessante del af enhver routingløsning.

hvad er en router, alligevel?

du kan generelt se en router som en funktion eller sort boks med to indgange og en udgang:

route configuration ↘ matched content current location ↗

selvom routing og efterfølgende gengivelse kan forekomme i separate trin, gør React det nemt og intuitivt at samle dem sammen i en deklarativ routing API. Lad os se på to strategier for at opnå dette.

strategi 1: en monolitisk <Router / > komponent

vi kan bruge en monolitisk, butiksforbundet <Router/> komponent, der:

  • accepterer et rutekonfigurationsobjekt via rekvisitter
  • læser placeringsdataene fra butikken
  • beregner det nye indhold, når placeringen ændres
  • gengiver/gengiver indholdet efter behov.

rutekonfigurationen kan være et almindeligt JavaScript-objekt, der indeholder alle de matchende stier og sider (en centraliseret rutekonfiguration).

Sådan ser det ud:

const routes = 
React.render( <Provider store={store}> <Router routes={routes}> </Provider>, document.getElementById('app'))

ret simpelt, ikke? Intet behov for indlejrede ruter — kun et enkelt rutekonfigurationsobjekt og en enkelt routerkomponent.

hvis denne strategi appellerer til dig, så tjek min mere komplette implementering i redux-json-router biblioteket. Det ombrydes redux-first-routing og giver React bindinger til deklarativ navigation og routing ved hjælp af de strategier, vi har undersøgt indtil videre.

mksarge/Red-JSON-router
Red-JSON-router – deklarativ, Red-første routing til React / Red-router applications.github.com

strategi 2: Composable <Rout e / > komponenter

mens en monolitisk komponent kan være en enkel måde at opnå deklarativ routing i React, er det bestemt ikke den eneste måde.

React ‘ s sammensatte karakter tillader en anden interessant mulighed: at bruge JSH til at definere ruter på en decentraliseret måde. Selvfølgelig er det primære eksempel React Routers <Route/> API:

React.render( <BrowserRouter> <Route path='/' component={Home}/> <Route path='/about component={About}/> ... </BrowserRouter>

andre routingbiblioteker udforsker også denne ide. Selvom jeg ikke har haft chancen for at gøre det, kan jeg ikke se nogen grund til, at en lignende API ikke kunne implementeres oven på redux-first-routing – pakken.

i stedet for at stole på placeringsdata leveret af <BrowserRouter/>, the &l t;rute/>komponent could si mply forbindelse til butikken:

React.render( <Provider store={store}> <Route path='/' component={Home}/> <Route path='/about component={About}/> ... </Provider>

hvis det er noget, du er interesseret i at bygge eller bruge, så lad mig det vide i kommentarerne! For at lære mere om forskellige rutekonfigurationsstrategier, tjek denne introduktion på React Routers hjemmeside.

konklusion

jeg håber, at denne udforskning har bidraget til at uddybe din viden om klientsiden routing og har vist dig, hvor nemt det er at opnå det på den måde.

hvis du leder efter en komplet routingløsning, kan du bruge pakken redux-first-routing med en kompatibel router, der er angivet i readme. Og hvis du finder dig selv nødt til at udvikle en skræddersyet løsning, forhåbentlig har dette indlæg givet dig et godt udgangspunkt for at gøre det.

hvis du gerne vil vide mere om routing på klientsiden i React, så tjek de følgende artikler-de var medvirkende til at hjælpe mig med bedre at forstå de emner, jeg dækkede her:

  • lad URL ‘ en tale af Tyler Thompson
  • du har muligvis ikke brug for React Router af Konstantin Tarkus
  • har jeg endda brug for et Routingbibliotek? af James K. Nelson
  • og utallige informative diskussioner i de react-router-redux spørgsmål.

routing på klientsiden er et rum med uendelige designmuligheder, og jeg er sikker på, at nogle af jer har spillet med ideer, der ligner dem, jeg har delt her. Hvis du gerne vil fortsætte samtalen, vil jeg være glad for at forbinde med dig i kommentarerne eller via kvidre. Tak fordi du læste!

Rediger 22/06/17: Tjek også denne artikel om redux-first-router, et separat projekt, der bruger intelligente handlingstyper til at opnå kraftfulde routingfunktioner.

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.