door Michael Sargent

een routing library is een belangrijk onderdeel van een complexe toepassing met één pagina. Als u web apps te ontwikkelen met React en Redux, je hebt waarschijnlijk gebruikt, of op zijn minst gehoord van React Router. Het is een bekende routing bibliotheek voor React, en een geweldige oplossing voor veel use cases.

maar React Router is niet de enige levensvatbare oplossing in het React/Redux ecosysteem. In feite zijn er tonnen routeringsoplossingen gebouwd voor React en voor Redux, elk met verschillende API ‘ s, functies en doelen — en de lijst groeit alleen maar. Onnodig te zeggen, client-side routing is niet weg op elk moment snel, en er is nog veel ruimte voor ontwerp in de routing bibliotheken van morgen.

vandaag wil ik uw aandacht vestigen op het onderwerp routing in Redux. Ik zal een pleidooi houden voor Redux-first routing — een paradigma dat Redux de ster maakt van het routing model, en de rode draad tussen vele Redux routing oplossingen. Ik zal laten zien hoe je de core, framework-agnostische API samen te stellen in minder dan 100 regels code, voordat het verkennen van opties voor real-world gebruik met React en andere front-end frameworks.

een beetje geschiedenis

in de browser worden de locatie (URL-informatie) en de sessiegeschiedenis (een stapel locaties die door het huidige tabblad browser worden bezocht) opgeslagen in het globale window – object. Ze zijn toegankelijk via:

  • window.location (locatie API)
  • window.history (geschiedenis API).

De Geschiedenis API biedt de volgende geschiedenis navigatiesystemen, bekend om hun vermogen om de update van de browser geschiedenis en locatie zonder dat er een pagina opnieuw laden:

  • pushState(href) — duwt een nieuwe locatie op de geschiedenis van stapel
  • replaceState(href) — hiermee overschrijft u de huidige locatie op de stapel
  • back() — hiermee navigeert u naar de vorige plaats op de stack
  • forward() — navigeert naar de volgende locatie op de stack
  • go(index) — hiermee navigeert u naar een locatie op de stapel, in beide richtingen.

samen maken de geschiedenis-en Locatieapi ‘ s het moderne routeringsparadigma voor de client mogelijk, bekend als pushState routing-de eerste protagonist van ons verhaal.

nu is het bijna een misdaad om de geschiedenis en locatie API ‘ s te vermelden zonder een moderne wrapper bibliotheek zoals historyte noemen.

ReactTraining / geschiedenis
sessiegeschiedenis beheren met JavaScriptgithub.com

history biedt een eenvoudige maar krachtige API voor interfacing met de browsergeschiedenis en-locatie, terwijl inconsistenties tussen verschillende browserimplementaties worden behandeld. Het wordt gebruikt als een peer of interne afhankelijkheid in veel moderne routing bibliotheken,en Ik zal meerdere verwijzingen naar het in dit artikel.

Redux En pushState Routing

de tweede protagonist van ons verhaal is Redux. Het is 2017, dus Ik zal je de introductie besparen en meteen ter zake komen:

door gebruik te maken van gewone pushstate routing in een Redux applicatie, splitsen we de toepassingsstatus over twee domeinen: browsergeschiedenis en De Redux store.

zo ziet dat eruit met React Router, die historyinstanteert en wraps:

history → React Router ↘ view Redux ↗

nu weten we dat niet alle gegevens in de winkel hoeven te staan. Bijvoorbeeld, lokale component staat is vaak een geschikte plek om gegevens die specifiek zijn voor een enkele component op te slaan.

maar locatiegegevens zijn niet triviaal. Het is een dynamisch en belangrijk onderdeel van de applicatie staat — het soort gegevens dat hoort in de winkel. Het in de winkel houden maakt Redux luxe, zoals tijdreizen debugging, en gemakkelijke toegang vanaf elke winkel aangesloten component.

dus hoe verplaatsen we de locatie naar de winkel?

er is geen ontkomen aan het feit dat de browser geschiedenis en locatiegegevens leest en opslaat in de window, maar wat we wel kunnen doen is een kopie van de locatiegegevens in de store bewaren en synchroon houden met de browser.

is dat niet wat react-router-redux doet voor React Router?

Ja, maar alleen om de tijdreizen mogelijkheden van De Redux DevTools in te schakelen. De applicatie is nog steeds afhankelijk van locatiegegevens in React Router:

history → React Router ↘ ↕ view Redux ↗

het gebruik van react-router-redux om locatiegegevens uit de winkel te lezen in plaats van React Router wordt afgeraden (vanwege mogelijk tegenstrijdige bronnen van waarheid).

kunnen we het beter doen?

kunnen we een alternatief routeringsmodel bouwen — een model dat vanaf de grond opgebouwd is om goed te spelen met Redux, zodat we de locatie op de Redux manier kunnen lezen en bijwerken — met store.getState() en store.dispatch()?

dat kunnen we absoluut, en het heet Redux-first routing.

Redux-eerste Routing

Redux-first routing is een variant op pushState routing die Redux de ster maakt van het routeringsmodel.

een Redux-first routing oplossing voldoet aan de volgende criteria:

  • de locatie wordt gehouden in De Redux store.
  • de locatie wordt gewijzigd door Redux-acties te verzenden.
  • de toepassing leest locatiegegevens uitsluitend uit de winkel.
  • de geschiedenis van de winkel en de browser worden achter de schermen gesynchroniseerd bewaard.

hier is een basisidee van hoe dat eruit ziet:

history ↕ Redux → router → view

wacht, zijn er niet nog twee bronnen van locatiegegevens?

Ja, maar als we erop kunnen vertrouwen dat de browsergeschiedenis en Redux store synchroon zijn, kunnen we onze applicaties bouwen om alleen locatiegegevens uit de store te lezen. Dan, vanuit het oogpunt van de toepassing, Er is slechts één bron van de waarheid — de winkel.

hoe bereiken we Redux-first routing?

we kunnen beginnen met het creëren van een conceptueel model, door het samenvoegen van de fundamentele elementen van de client-side routing en Redux data lifecycle modellen.

het Routeringsmodel aan de clientzijde opnieuw bekijken

Client-side routing is een multi-stap proces dat begint met navigatie en eindigt met rendering-routing zelf is slechts een stap in dat proces! Laten we de details bekijken.:

  • navigatie – alles begint met een verandering in locatie. Er zijn 2 soorten navigatie: intern en extern. Interne navigatie wordt uitgevoerd vanuit de app (bijv. via de geschiedenis API), terwijl externe navigatie plaatsvindt wanneer de gebruiker communiceert met de navigatiebalk van de browser of de toepassing invoert vanaf een externe site.
  • reageren op navigatie – wanneer de locatie verandert, reageert de toepassing door de nieuwe locatie door te geven aan de router. Oudere routeringstechnieken vertrouwden op polling window.location om dit te bereiken, maar tegenwoordig hebben we het handige history.listen hulpprogramma.
  • Routing-vervolgens wordt de nieuwe locatie afgestemd op de bijbehorende pagina-inhoud. De code die deze stap behandelt wordt een router genoemd, en het neemt over het algemeen een invoerparameter van overeenkomende routes en pagina ‘ s die een routeconfiguratie worden genoemd.
  • Rendering-ten slotte wordt de inhoud op de client gerenderd. Deze stap kan natuurlijk worden afgehandeld door een front-end framework/bibliotheek zoals React.

merk op dat routeringsbibliotheken niet elk deel van het routeringsmodel hoeven te verwerken.

sommige bibliotheken, zoals React Router en Vue Router, doen dat, terwijl andere, zoals Universal Router, zich uitsluitend bezighouden met één aspect (zoals routing), waardoor flexibiliteit wordt geboden in de andere aspecten.:

Routeringsbibliotheken kunnen verschillende verantwoordelijkheden hebben. (Klik om te vergroten))

het Redux Data Lifecycle model opnieuw bekijken

Redux heeft een one-way data flow / lifecycle model dat waarschijnlijk geen introductie nodig heeft-maar hier is een kort overzicht voor een goede maatregel:

  • actie – elke statuswijziging begint met het verzenden van een Redux-actie (een gewoon object met een type en optionele lading).
  • Middleware-de actie loopt door de keten van middlewares van de winkel, waar acties kunnen worden onderschept en aanvullend gedrag kan worden uitgevoerd. Middlewares worden vaak gebruikt om bijwerkingen te behandelen in Redux toepassingen.
  • Reducer-de actie bereikt dan de rootreducer, die de volgende status van het archief berekent als een zuivere functie van de vorige status en de ontvangen actie. De root reducer kan worden samengesteld uit individuele reductoren die elk behandelen een deel van de staat van de winkel.
  • nieuwe status-de winkel slaat de nieuwe status op die door het reductiemiddel wordt geretourneerd en stelt zijn abonnees op de hoogte van de wijziging (in React, via connect).
  • Rendering-ten slotte kan het met de opslag verbonden beeld opnieuw renderen in overeenstemming met de nieuwe status.

bouwen van een Redux-First Routing Model

de unidirectionele aard van de client-side routing en Redux data lifecycle modellen leent zich goed voor een samengevoegd model dat voldoet aan de criteria die we hebben opgesteld voor Redux-first routing.

in dit model is de router geabonneerd op de store, wordt navigatie uitgevoerd via Redux-acties en worden updates van de browsergeschiedenis afgehandeld door een aangepaste middleware. Laten we eens kijken naar de details van dit model:

  • interne navigatie via Redux-acties-in plaats van de History API direct te gebruiken, wordt interne navigatie bereikt door een van de 5 navigatieacties te verzenden die de navigatiemethoden uit de geschiedenis weerspiegelen.
  • de browsergeschiedenis bijwerken via middleware-een middleware wordt gebruikt om de navigatieacties te onderscheppen en het neveneffect van het bijwerken van de browsergeschiedenis af te handelen. Aangezien de nieuwe locatie niet noodzakelijk of gemakkelijk bekend is zonder eerst de browsergeschiedenis te raadplegen (bijv. in het geval van een actie go) kunnen de navigatieacties het verloopstuk niet bereiken.
  • reageren op Navigatie-De stroom van uitvoering gaat door met een history luisteraar die reageert op Navigatie (van zowel middleware als externe navigatie) door een tweede actie te sturen die de nieuwe locatie bevat.
  • Location Reducer-de actie die door de luisteraar wordt verzonden, bereikt vervolgens het location reducer, waardoor de locatie wordt toegevoegd aan het archief. De locatie reducer bepaalt ook de vorm van de locatie staat.
  • Connected routing-de met het archief verbonden router kan dan reactief de nieuwe pagina-inhoud bepalen wanneer er een melding wordt gemaakt van een verandering in de locatie in het archief.
  • Rendering-ten slotte kan de pagina opnieuw worden gerenderd met de nieuwe inhoud.

merk op dat dit niet de enige manier is om Redux-first routing te bereiken — sommige variaties bevatten het gebruik van een opslagversterker en/of extra logica in de middleware — maar het is een eenvoudig model dat alle bases dekt.

een basisimplementatie

naar aanleiding van het model dat we net bekeken, laten we de uitvoering van de kern API — de acties, middleware, luisteraar, en reducer.

we gebruiken het history pakket als een interne afhankelijkheid,en bouwen de oplossing stapsgewijs. Als u liever volgen samen met het eindresultaat, kunt u het hier bekijken.

acties

we beginnen met het definiëren van de 5 navigatieacties die de navigatiemethoden uit de geschiedenis weerspiegelen:

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

laten we vervolgens de middleware definiëren. Het moet de navigatieacties onderscheppen, de bijbehorende history navigatiemethoden aanroepen, dan de actie stoppen om het reductiemiddel te bereiken-maar alle andere acties ongestoord laten:

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

als je nog niet eerder de kans hebt gehad om de binnenkant van een Redux middleware te schrijven of te onderzoeken, bekijk dan deze introductie.

geschiedenis luisteraar

vervolgens hebben we een history luisteraar nodig die reageert op navigatie door een nieuwe actie te sturen die de nieuwe locatie-informatie bevat.

laten we eerst het nieuwe actietype en de maker toevoegen. De interessante delen van de locatie zijn de pathname, search, en hash – dus dat is wat we zullen opnemen in de payload:

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

laten we dan de luisteraar functie schrijven:

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

we zullen een kleine toevoeging maken-een initiële locationChange verzending, om rekening te houden met de eerste invoer in de applicatie (die niet wordt opgepikt door de geschiedenis luisteraar):

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

vervolgens definiëren we de locatie reducer. We gebruiken een eenvoudige toestandsvorm, en doen minimaal werk in het verloopstuk:

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

toepassingscode

ten slotte, laten we aansluiten onze API in de code van de toepassing:

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

en dat is alles wat er is! Met behulp van onze kleine (onder 100 regels code) API, we hebben voldaan aan alle criteria voor Redux-eerste routing:

  • de locatie wordt gehouden in De Redux store. ✔
  • de locatie wordt gewijzigd door Redux-acties te verzenden. ✔
  • de applicatie leest locatiegegevens alleen uit de winkel. ✔
  • de winkel-en browsergeschiedenis worden achter de schermen gesynchroniseerd gehouden. ✔

bekijk alle bestanden hier samen – voel je vrij om ze te importeren in uw project, of gebruik het als een startpunt om uw eigen implementatie te ontwikkelen.

het redux-first-routing pakket

ik heb ook de API samengebracht in het redux-first-routing pakket, dat u npm install kunt gebruiken op dezelfde manier.

mksarge / redux-first-routing
redux-first — routing-een minimale, framework-agnostische basis voor het uitvoeren van Redux-first routing.github.com

het bevat een implementatie vergelijkbaar met degene die we hier hebben gebouwd, maar met de opmerkelijke toevoeging van query parsing via het query-string pakket.

wacht-hoe zit het met de eigenlijke routeringscomponent?

u hebt misschien gemerkt dat redux-first-routing alleen betrekking heeft op het navigatieaspect van het routeringsmodel:

door het navigatieaspect los te koppelen van de andere aspecten van ons routeringsmodel, hebben we wat flexibiliteit gekregen — redux-first-routing is zowel router-agnostisch als framework-agnostisch.

u kunt het daarom koppelen met een bibliotheek zoals Universal Router om een complete Redux-first routing oplossing te creëren voor elk front-end framework:

Klik hier om aan de slag te gaan met redux-first-routing + universal-router.

of u kunt eigenzinnige bindingen maken voor uw raamwerk naar keuze — en dat is wat we zullen doen voor React in de volgende en laatste sectie van dit artikel.

gebruik met React

laten we onze verkenning afronden door te kijken hoe we store-connected componenten kunnen bouwen voor declaratieve navigatie en routing in React.

declaratieve navigatie

voor navigatie kunnen we een store-connected<Link/ > component gebruiken die vergelijkbaar is met die in React Router en andere react routing oplossingen.

het overschrijft eenvoudig het standaardgedrag van ankerelement <a/ > en dispatches een pushactie wanneer erop wordt geklikt:

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

misschien vindt u hier een meer volledige implementatie.

declaratieve routering

hoewel er niet veel aan een navigatiecomponent is, zijn er talloze manieren om een routeringscomponent te ontwerpen — waardoor het het interessantste deel van elke routeringsoplossing is.

Wat is een router eigenlijk?

u kunt een router over het algemeen als een functie of black box met twee ingangen en een uitgang bekijken:

route configuration ↘ matched content current location ↗

hoewel de routering en daaropvolgende rendering in afzonderlijke stappen kunnen plaatsvinden, maakt React het eenvoudig en intuïtief om ze samen te bundelen tot een declaratieve routing API. Laten we eens kijken naar twee strategieën om dit te bereiken.

Strategy 1: a monolithic<Router/> component

we kunnen een Monolithic, store-connected <RouteR/> component gebruiken die:

  • accepteert een routeconfiguratieobject via props
  • leest de locatiegegevens uit De Redux-opslag
  • berekent de nieuwe inhoud wanneer de locatie verandert
  • maakt / opnieuw maakt de inhoud indien van toepassing.

de routeconfiguratie kan een eenvoudig JavaScript-object zijn dat alle overeenkomende paden en pagina ‘ s bevat (een gecentraliseerde routeconfiguratie).

zo ziet dit eruit:

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

vrij simpel, toch? Geen behoefte aan geneste JSX routes-slechts een enkele route configuratie object, en een enkele router component.

als deze strategie u aanspreekt, bekijk dan mijn meer volledige implementatie in de redux-json-router bibliotheek. Het wraps redux-first-routing en biedt React bindingen voor declaratieve navigatie en routing met behulp van de strategieën die we tot nu toe hebben onderzocht.

mksarge / redux-json-router
redux-json-router-declaratief, Redux-first routing voor React / Redux browser applications.github.com

Strategie 2: Composable <Route / > components

hoewel een monolithische component een eenvoudige manier kan zijn om declaratieve routing te bereiken in React, is het zeker niet de enige manier.

het composeerbare karakter van React laat nog een interessante mogelijkheid toe: JSX gebruiken om routes decentraal te definiëren. Natuurlijk is het belangrijkste voorbeeld de API van de React Router <Route/ > :

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

andere routing bibliotheken verkennen dit idee ook. Hoewel ik nog niet de kans heb gehad om het te doen, zie ik geen reden waarom een soortgelijke API niet kon worden geïmplementeerd bovenop het redux-first-routing pakket.

in plaats van te vertrouwen op locatiegegevens van <BrowserRouter / >, the &lt; Route / > component could simply verbinding maken met de winkel:

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

als dat iets is dat je geïnteresseerd bent in het bouwen of gebruiken, laat het me weten in de reacties! Voor meer informatie over verschillende routeconfiguratiestrategieën, bekijk deze introductie op de website van React Router.

conclusie

ik hoop dat deze verkenning uw kennis over client-side routing heeft verdiept en u heeft laten zien hoe eenvoudig het is om het op de Redux manier te doen.

als u op zoek bent naar een complete Redux routing oplossing, kunt u het redux-first-routing pakket gebruiken met een compatibele router vermeld in de readme. En als je merkt dat je nodig hebt om een oplossing op maat te ontwikkelen, hopelijk dit bericht heeft u een goed uitgangspunt gegeven om dit te doen.

als u meer wilt weten over routing aan de kant van de client in React en Redux, bekijk dan de volgende artikelen-ze hielpen me om de onderwerpen die ik hier behandelde beter te begrijpen.:

  • laat de URL het woord voeren door Tyler Thompson
  • mogelijk hebt u React Router van Konstantin Tarkus
  • niet nodig heb ik zelfs een Routingbibliotheek nodig? door James K. Nelson
  • en talloze informatieve discussies in de react-router-redux issues.

client-side routing is een ruimte met eindeloze ontwerpmogelijkheden, en ik weet zeker dat sommigen van jullie hebben gespeeld met ideeën vergelijkbaar met degene die ik hier heb gedeeld. Als je het gesprek wilt voortzetten, zal ik graag contact met je opnemen in de commentaren of via Twitter. Bedankt voor het lezen!

Edit 22/06/17: Bekijk ook dit artikel over redux-first-router, een apart project dat intelligente actietypen gebruikt om krachtige routeringsmogelijkheden te bereiken.

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.