av Michael Sargent

ett routingbibliotek är en nyckelkomponent i alla komplexa, enkelsidiga applikationer. Om du utvecklar webbappar med React och Redux har du förmodligen använt, eller åtminstone hört talas om React Router. Det är ett välkänt routingbibliotek för React, och en bra lösning för många användningsfall.

men React Router är inte den enda hållbara lösningen i React/Redux-ekosystemet. Faktum är att det finns massor av routinglösningar byggda för React och Redux, alla med olika API: er, funktioner och mål — och listan växer bara. Det är självklart att routing på klientsidan inte går bort när som helst snart, och det finns fortfarande mycket utrymme för design i morgondagens routingbibliotek.

idag vill jag uppmärksamma ämnet routing i Redux. Jag ska presentera och göra ett fall för Redux-first routing – ett paradigm som gör Redux stjärnan i routingmodellen och den röda tråden bland många Redux routinglösningar. Jag ska visa hur man sätter ihop kärnan, framework-agnostic API i under 100 rader kod, innan du utforskar alternativ för verklig användning med React och andra front-end-ramar.

en liten historia

i webbläsaren lagras platsen (URL-information) och sessionshistoriken (en stapel platser som besöks av den aktuella webbläsarfliken) i det globala window – objektet. De är tillgängliga via:

  • window.location (plats API)
  • window.history (historia API).

historia API erbjuder följande Historia navigeringsmetoder, känd för sin förmåga att uppdatera webbläsarens historia och plats utan att kräva en sida reload:

  • pushState(href) — skjuter en ny plats på historikstacken
  • replaceState(href) — skriver över den aktuella platsen på stacken
  • back() — navigerar till föregående plats på stacken
  • forward() — navigerar till nästa plats på stacken
  • go(index) — navigerar till en plats på stacken, i endera riktningen.

tillsammans möjliggör historia och plats API: er det moderna klientsidan routing paradigm som kallas pushState routing-den första huvudpersonen i vår historia.

nu är det nästan ett brott att nämna historia och plats API: er utan att nämna ett modernt omslagsbibliotek som history.

ReactTraining/history
hantera sessionshistorik med JavaScriptgithub.com

history ger en enkel men ändå kraftfull API för gränssnitt med webbläsarhistorik och plats, samtidigt som den täcker inkonsekvenser mellan olika webbläsare implementeringar. Det används som peer eller internt beroende i många moderna routingbibliotek, och jag kommer att göra flera referenser till det i hela den här artikeln.

Redux Och pushState Routing

den andra huvudpersonen i vår berättelse är Redux. Det är 2017, så jag sparar dig introduktionen och får rätt till punkten:

genom att använda vanlig pushState-routing i en Redux-applikation delar vi applikationstillståndet över två domäner: webbläsarhistorik och Redux-butiken.

här är vad som ser ut med React Router, som instantiates och wraps history:

history → React Router ↘ view Redux ↗

nu vet vi att inte alla data måste finnas i butiken. Till exempel är lokal komponentstatus ofta en lämplig plats för att lagra data som är specifika för en enda komponent.

men platsdata är inte triviala. Det är en dynamisk och viktig del av applikationstillståndet — den typ av data som hör hemma i butiken. Att hålla den i butiken möjliggör Redux lyx som tidsresor felsökning och enkel åtkomst från alla butiksanslutna komponenter.

så hur flyttar vi platsen till butiken?

Det går inte att komma runt det faktum att webbläsaren läser och lagrar historik och platsinformation i window, men vad vi kan göra är att hålla en kopia av platsdata i butiken och hålla den synkroniserad med webbläsaren.

är det inte vad react-router-redux gör för React Router?

Ja, men bara för att aktivera Redux DevTools tidsresefunktioner. Applikationen beror fortfarande på platsdata som finns i React Router:

history → React Router ↘ ↕ view Redux ↗

att använda react-router-redux för att läsa platsdata från butiken istället för React Router avskräcks (på grund av potentiellt motstridiga sanningskällor).

kan vi göra bättre?

kan vi bygga en alternativ routingmodell — en som är byggd från grunden för att spela bra med Redux, så att vi kan läsa och uppdatera platsen Redux way — med store.getState() och store.dispatch()?

vi kan absolut, och det kallas Redux-first routing.

Redux – första Routing

Redux-first routing är en variant på pushState routing som gör Redux stjärnan i routingmodellen.

en Redux-first routing-lösning uppfyller följande kriterier:

  • platsen hålls i Redux-butiken.
  • platsen ändras genom att skicka Redux-åtgärder.
  • applikationen läser platsdata enbart från butiken.
  • butiken och webbläsarhistoriken hålls synkroniserade bakom kulisserna.

här är en grundläggande uppfattning om hur det ser ut:

history ↕ Redux → router → view

vänta, finns det inte fortfarande två källor till platsdata?

Ja, men om vi kan lita på att webbläsarhistoriken och Redux-butiken är synkroniserade kan vi bygga våra applikationer för att bara läsa platsdata från butiken. Sedan, från applikationens synvinkel, finns det bara en källa till sanning — butiken.

hur uppnår vi Redux-first routing?

vi kan börja med att skapa en konceptuell modell, genom att slå samman de grundläggande elementen i klientsidan routing och Redux data lifecycle modeller.

återbesök klientsidan Routing Modell

klientsidan routing är en flerstegsprocess som börjar med navigering och slutar med rendering — routing själv är bara ett steg i den processen! Låt oss granska detaljerna:

  • navigering-allt börjar med en platsändring. Det finns 2 typer av navigering: internt och externt. Intern navigering sker inifrån appen (t.ex. via History API), medan extern navigering uppstår när användaren interagerar med webbläsarens navigeringsfält eller går in i applikationen från en extern webbplats.
  • svara på navigering – när platsen ändras svarar applikationen genom att skicka den nya platsen till routern. Äldre routingtekniker förlitade sig på polling window.location för att uppnå detta, men nuförtiden har vi det praktiska verktyget history.listen.
  • Routing-därefter matchas den nya platsen med motsvarande sidinnehåll. Koden som hanterar detta steg kallas en router, och det tar i allmänhet en ingångsparameter för matchande rutter och sidor som kallas en ruttkonfiguration.
  • Rendering-slutligen återges innehållet på klienten. Detta steg kan naturligtvis hanteras av ett front-end-ramverk/bibliotek som React.

Observera att routingbibliotek inte behöver hantera alla delar av routingmodellen.

vissa bibliotek, som React Router och Vue Router, gör – medan andra, som Universal Router, endast handlar om en enda aspekt( som routing), vilket ger flexibilitet i andra aspekter:

Routingbibliotek kan ha olika ansvarsområden. (Klicka för att förstora)

revidera Redux Data Lifecycle-modellen

Redux har en enkelriktad dataflöde / livscykelmodell som sannolikt inte behöver introduceras — men här är en kort översikt för bra mått:

  • åtgärd — varje tillståndsändring börjar med att skicka en Redux-åtgärd (ett vanligt objekt som innehåller en type och valfri nyttolast).
  • Middleware-åtgärden passerar genom butikens kedja av middlewares, där åtgärder kan avlyssnas och ytterligare beteende kan utföras. Middlewares används ofta för att hantera biverkningar i Redux applikationer.
  • Reducer-åtgärden når sedan rotreduceraren, som beräknar butikens nästa tillstånd som en ren funktion av föregående tillstånd och den mottagna åtgärden. Rotreduceraren kan bestå av enskilda reducerare som varje hanterar en bit av butikens tillstånd.
  • nytt tillstånd-butiken sparar det nya tillståndet som returneras av reduceraren och meddelar sina abonnenter om ändringen (i React, via connect).
  • Rendering-slutligen kan den butiksanslutna vyn återges i enlighet med det nya tillståndet.

bygga en Redux-första routingmodell

den enkelriktade karaktären hos klientsidan routing och Redux data lifecycle modeller lämpar sig väl för en sammanslagen modell som uppfyller de kriterier som vi lagt ut för Redux-first routing.

i den här modellen prenumererar routern på butiken, navigering sker via Redux-åtgärder och uppdateringar av webbläsarhistoriken hanteras av en anpassad mellanprogramvara. Låt oss undersöka detaljerna i denna modell:

  • Intern navigering via Redux — åtgärder-istället för att använda History API direkt uppnås intern navigering genom att skicka en av 5 navigationsåtgärder som speglar historiknavigeringsmetoderna.
  • uppdatera webbläsarhistoriken via middleware – en middleware används för att avlyssna navigationsåtgärderna och hantera biverkningen av uppdatering av webbläsarhistoriken. Eftersom den nya platsen inte nödvändigtvis eller lätt är känd utan att först konsultera webbläsarhistoriken (t.ex. i fallet med en go – åtgärd) förhindras navigeringsåtgärderna från att nå reduceraren.
  • svara på navigering-exekveringsflödet fortsätter med en history – lyssnare som svarar på navigering (från både middleware och extern navigering) genom att skicka en andra åtgärd som innehåller den nya platsen.
  • Platsreducerare – åtgärden som skickas av lyssnaren når sedan platsreduceraren, vilket lägger till platsen i butiken. Platsreduceraren bestämmer också formen på platstillståndet.
  • ansluten routing-den butiksanslutna routern kan sedan reaktivt bestämma det nya sidinnehållet när det meddelas om en platsändring i butiken.
  • Rendering-slutligen kan sidan återges med det nya innehållet.

Observera att detta inte är det enda sättet att uppnå Redux-first routing — vissa variationer har användningen av en butiksförstärkare och/eller ytterligare logik i middleware — men det är en enkel modell som täcker alla baser.

en grundläggande implementering

efter den modell vi just tittat på, låt oss implementera core API — åtgärderna, middleware, listener och reducer.

vi använder paketet history som ett internt beroende och bygger lösningen stegvis. Om du hellre vill följa med det slutliga resultatet kan du se det här.

åtgärder

vi börjar med att definiera de 5 navigeringsåtgärderna som speglar historiknavigeringsmetoderna:

// 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,});

Middleware

låt oss sedan definiera middleware. Det ska avlyssna navigationsåtgärderna, ringa motsvarande history navigationsmetoder och stoppa sedan åtgärden från att nå reduceraren – men lämna alla andra åtgärder ostörda:

// 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); }};

om du inte har haft chansen att skriva eller undersöka internalerna i en Redux middleware tidigare, kolla in den här introduktionen.

History Listener

därefter behöver vi en history listener som svarar på navigering genom att skicka en ny åtgärd som innehåller den nya platsinformationen.

Låt oss först lägga till den nya åtgärdstypen och skaparen. De intressanta delarna av platsen är pathname, search och hash – så det är vad vi ska inkludera i nyttolasten:

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

låt oss sedan skriva lyssnarfunktionen:

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

vi gör ett litet tillägg – en initial locationChange – sändning, för att redogöra för den första inmatningen i applikationen (som inte hämtas av historiklyssnaren):

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

låt oss sedan definiera platsreduceraren. Vi använder en enkel statsform och gör minimalt arbete i reduceraren:

// 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; }};

Ansökningskod

slutligen, låt oss ansluta vårt API till 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'))

och det är allt som finns till det! Med hjälp av vår lilla (under 100 rader kod) API, har vi uppfyllt alla kriterier för Redux-first routing:

  • platsen hålls i Redux-butiken.
  • platsen ändras genom att skicka Redux-åtgärder.
  • ansökan läser platsdata enbart från butiken.
  • butiken och webbläsarhistoriken hålls synkroniserade bakom kulisserna. ✔

Visa alla filer tillsammans här — importera dem gärna till ditt projekt, eller använd det som utgångspunkt för att utveckla din egen implementering.

redux-first-routing paketet

jag har också lagt API tillsammans i redux-first-routing paketet, som du kan npm install och använda på samma sätt.

mksarge/redux-first-routing
redux-first-routing — en minimal, RAM-agnostisk bas för att åstadkomma Redux-first routing.github.com

den innehåller en implementering som liknar den vi byggde här, men med det anmärkningsvärda tillägget av frågeparsning via paketet query-string.

vänta — Hur är det med den faktiska routingkomponenten?

du kanske har märkt att redux-first-routing endast handlar om navigationsaspekten i routingmodellen:

genom att koppla bort navigationsaspekten från de andra aspekterna av vår routingmodell har vi fått viss flexibilitet — redux-first-routing är både router-agnostiker och ram-agnostiker.

du kan därför para ihop det med ett bibliotek som Universal Router för att skapa en komplett Redux-first routing-lösning för alla front-end-ramar:

Klicka här för att komma igång med redux-first-routing + universal-router.

eller, du kan bygga opinionated bindningar för din RAM val — och det är vad vi ska göra för reagera i nästa och sista avsnittet i denna artikel.

användning med React

Låt oss avsluta vår utforskning genom att titta på hur vi kan bygga butiksanslutna komponenter för deklarativ navigering och routing i React.

deklarativ navigering

för navigering kan vi använda en butiksansluten <Link/> komponent som liknar den i React Router och andra React routing-lösningar.

det åsidosätter helt enkelt standardbeteendet för ankarelement <a / > och Dispatchhes en push-åtgärd när du klickar:

// 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 hitta en mer fullständig implementering här.

deklarativ Routing

även om det inte finns mycket för en navigeringskomponent, finns det otaliga sätt att designa en routingkomponent — vilket gör den till den mest intressanta delen av någon routinglösning.

Vad är en router, hur som helst?

du kan i allmänhet se en router som en funktion eller svart låda med två ingångar och en utgång:

route configuration ↘ matched content current location ↗

även om routing och efterföljande rendering kan ske i separata steg, gör React det enkelt och intuitivt att bunta ihop dem till ett deklarativt routing API. Låt oss titta på två strategier för att uppnå detta.

strategi 1: en monolitisk<Router/ > komponent

vi kan använda en monolitisk, butiksansluten<Router/ > komponent som:

  • accepterar ett ruttkonfigurationsobjekt via rekvisita
  • läser platsdata från Redux-butiken
  • beräknar det nya innehållet när platsen ändras
  • återger/återger innehållet efter behov.

ruttkonfigurationen kan vara ett vanligt JavaScript-objekt som innehåller alla matchande sökvägar och sidor (en centraliserad ruttkonfiguration).

så här kan det se ut:

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

ganska enkelt, eller hur? Inget behov av kapslade JSX-rutter-bara ett enda ruttkonfigurationsobjekt och en enda routerkomponent.

om den här strategin är tilltalande för dig, kolla in min mer fullständiga implementering i biblioteket redux-json-router. Det wraps redux-first-routing och ger reagera bindningar för deklarativ navigering och routing med hjälp av de strategier som vi har granskat hittills.

mksarge / redux-json-router
redux-json-router-deklarativ, Redux – första routing för React / Redux browser applications.github.com

strategi 2: Composable <Route/> komponenter

medan en monolitisk komponent kan vara ett enkelt sätt att uppnå deklarativ routing i React, är det definitivt inte det enda sättet.

reacts komposterbara natur möjliggör en annan intressant möjlighet: att använda JSX för att definiera rutter på ett decentraliserat sätt. Naturligtvis är det främsta exemplet React routers<Rout e / > API:

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

andra routingbibliotek utforskar också den här tanken. Medan jag inte har haft chansen att göra det, ser jag ingen anledning till att ett liknande API inte kunde implementeras ovanpå redux-first-routing – paketet.

istället för att förlita sig på platsdata som tillhandahålls av <BrowserRoute r / >, the &l t; rutt / > komponent could si mply Anslut till butiken:

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

om det är något som du är intresserad av att bygga eller använda, Låt mig veta i kommentarerna! För att lära dig mer om olika ruttkonfigurationsstrategier, kolla in den här introduktionen på React routers webbplats.

slutsats

jag hoppas att denna utforskning har bidragit till att fördjupa dina kunskaper om klientsidan routing och har visat dig hur enkelt det är att åstadkomma det Redux sätt.

om du letar efter en komplett Redux-routinglösning kan du använda paketet redux-first-routing med en kompatibel router som anges i readme. Och om du tycker att du behöver utveckla en skräddarsydd lösning, förhoppningsvis har det här inlägget gett dig en bra utgångspunkt för att göra det.

om du vill lära dig mer om routing på klientsidan i React och Redux, kolla in följande artiklar-de hjälpte mig att bättre förstå de ämnen jag täckte här:

  • låt webbadressen prata av Tyler Thompson
  • du kanske inte behöver reagera Router av Konstantin Tarkus
  • behöver jag ens ett Routingbibliotek? av James K. Nelson
  • och otaliga informativa diskussioner i react-router-redux – frågorna.

klientsidan routing är ett utrymme med oändliga designmöjligheter, och jag är säker på att några av er har spelat med ideer som liknar de jag har delat här. Om du vill fortsätta konversationen kommer jag gärna att kontakta dig i kommentarerna eller via Twitter. Tack för att du läste!

redigera 22/06/17: Kolla också in den här artikeln på redux-first-router, ett separat projekt som använder intelligenta åtgärdstyper för att uppnå kraftfulla routingfunktioner.

Lämna ett svar

Din e-postadress kommer inte publiceras.