di Michael Sargent

Una libreria di routing è un componente chiave di qualsiasi applicazione complessa a pagina singola. Se si sviluppano applicazioni web con React e Redux, probabilmente avete usato, o almeno sentito parlare di React Router. È una libreria di routing ben nota per React e un’ottima soluzione per molti casi d’uso.

Ma React Router non è l’unica soluzione praticabile nell’ecosistema React/Redux. In effetti, ci sono tonnellate di soluzioni di routing create per React e per Redux, ognuna con API, funzionalità e obiettivi diversi — e l’elenco è in crescita. Inutile dire che il routing lato client non scomparirà presto, e c’è ancora molto spazio per la progettazione nelle librerie di routing di domani.

Oggi, voglio portare la vostra attenzione sul tema del routing in Redux. Presenterò e creerò un caso per Redux-first routing – un paradigma che rende Redux la stella del modello di routing e il filo conduttore tra molte soluzioni di routing Redux. Illustrerò come mettere insieme l’API core, indipendente dal framework in meno di 100 righe di codice, prima di esplorare le opzioni per l’utilizzo nel mondo reale con React e altri framework front-end.

Un po ‘ di storia

Nel browser, la posizione (informazioni URL) e la cronologia delle sessioni (una pila di posizioni visitate dalla scheda browser corrente) vengono memorizzate nell’oggetto globale window. Sono accessibili tramite:

  • window.location (API posizione)
  • window.history (Storia API).

La Storia API propone la seguente storia dei metodi di navigazione, notevoli per la loro capacità di aggiornare la cronologia del browser e la posizione, senza necessità di un ricaricamento della pagina:

  • pushState(href) — spinge un nuovo percorso sulla storia dello stack
  • replaceState(href) — sovrascrive l’attuale posizione in pila
  • back() — consente di visualizzare la prima posizione nello stack
  • forward() — passa alla posizione successiva in pila
  • go(index) — naviga in una posizione in pila, in entrambe le direzioni.

Insieme, le API History e Location abilitano il moderno paradigma di routing lato client noto come pushState routing, il primo protagonista della nostra storia.

Ora, è quasi un crimine menzionare le API di cronologia e posizione senza menzionare una moderna libreria di wrapper come history.

ReactTraining/history
Gestisci la cronologia delle sessioni con JavaScriptgithub.com

history fornisce un’API semplice ma potente per interfacciarsi con la cronologia e la posizione del browser, coprendo le incongruenze tra le diverse implementazioni del browser. È usato come dipendenza peer o interna in molte librerie di routing moderne e farò più riferimenti ad esso in questo articolo.

Routing Redux e pushState

Il secondo protagonista della nostra storia è Redux. È il 2017, quindi ti risparmierò l’introduzione e arriverò al punto:

Utilizzando il routing pushState semplice in un’applicazione Redux, abbiamo diviso lo stato dell’applicazione su due domini: cronologia del browser e negozio Redux.

Ecco come appare con React Router, che istanzia e avvolge history:

history → React Router ↘ view Redux ↗

Ora sappiamo che non tutti i dati devono risiedere nello store. Ad esempio, lo stato del componente locale è spesso un luogo adatto per archiviare dati specifici per un singolo componente.

Ma i dati sulla posizione non sono banali. È una parte dinamica e importante dello stato dell’applicazione: il tipo di dati che appartiene all’archivio. Tenendolo nel negozio consente lussi Redux come il debug di viaggio nel tempo e un facile accesso da qualsiasi componente collegato al negozio.

Quindi, come spostiamo la posizione nel negozio?

Non c’è modo di aggirare il fatto che il browser legge e memorizza la cronologia e le informazioni sulla posizione nel window, ma quello che possiamo fare è mantenere una copia dei dati sulla posizione nel negozio, e tenerlo in sincronia con il browser.

Non è quello che fa react-router-redux per React Router?

Sì, ma solo per abilitare le funzionalità di viaggio nel tempo di Redux DevTools. L’applicazione dipende ancora dalla posizione dei dati detenuti in React Router:

history → React Router ↘ ↕ view Redux ↗

L’utilizzo di react-router-redux per leggere i dati sulla posizione dall’archivio anziché dal Router React è sconsigliato (a causa di fonti di verità potenzialmente contrastanti).

Possiamo fare meglio?

Possiamo costruire un modello di routing alternativo-uno che è costruito da zero per giocare bene con Redux, permettendoci di leggere e aggiornare la posizione in modo Redux — con store.getState() e store.dispatch()?

Possiamo assolutamente, e si chiama Redux-first routing.

Redux-Primo routing

Redux-first routing è una variante del routing pushState che rende Redux la stella del modello di routing.

Una soluzione di routing Redux-first soddisfa i seguenti criteri:

  • La posizione è tenuta nel negozio Redux.
  • La posizione viene modificata inviando azioni Redux.
  • L’applicazione legge i dati sulla posizione esclusivamente dal negozio.
  • Lo store e la cronologia del browser sono sincronizzati dietro le quinte.

Ecco un’idea di base di come appare:

history ↕ Redux → router → view

Aspetta, non ci sono ancora due fonti di dati sulla posizione?

Sì, ma se possiamo fidarci che la cronologia del browser e Redux store siano sincronizzati, possiamo creare le nostre applicazioni per leggere sempre e solo i dati di posizione dallo store. Quindi, dal punto di vista dell’applicazione, c’è solo una fonte di verità: il negozio.

Come realizziamo il routing Redux-first?

Possiamo iniziare creando un modello concettuale, unendo gli elementi fondamentali dei modelli di routing lato client e del ciclo di vita dei dati Redux.

Rivisitazione del modello di routing lato client

Il routing lato client è un processo a più fasi che inizia con la navigazione e termina con il rendering: il routing stesso è solo un passo in quel processo! Esaminiamo i dettagli:

  • Navigazione-Tutto inizia con un cambiamento di posizione. Esistono 2 tipi di navigazione: interna ed esterna. La navigazione interna viene effettuata dall’interno della app(ad es. la navigazione esterna si verifica quando l’utente interagisce con la barra di navigazione del browser o entra nell’applicazione da un sito esterno.
  • Rispondere alla navigazione – Quando la posizione cambia, l’applicazione risponde passando la nuova posizione al router. Le vecchie tecniche di routing si basavano sul polling window.location per ottenere questo risultato, ma al giorno d’oggi abbiamo la pratica utility history.listen.
  • Routing — Successivamente, la nuova posizione viene abbinata al contenuto della pagina corrispondente. Il codice che gestisce questo passaggio è chiamato router e generalmente richiede un parametro di input di percorsi e pagine corrispondenti chiamato configurazione del percorso.
  • Rendering-Infine, il contenuto viene reso sul client. Questo passaggio può, ovviamente, essere gestito da un framework/libreria front-end come React.

Si noti che le librerie di routing non devono gestire ogni parte del modello di routing.

Alcune librerie, come React Router e Vue Router, lo fanno – mentre altre, come Universal Router, si occupano esclusivamente di un singolo aspetto( come il routing), fornendo così flessibilità negli altri aspetti:

Le librerie di routing possono avere diversi ambiti di responsabilità. (Clicca per ingrandire)

Rivisitazione del modello del ciclo di vita dei dati Redux

Redux vanta un modello di flusso di dati/ciclo di vita unidirezionale che probabilmente non ha bisogno di presentazioni, ma ecco una breve panoramica per una buona misura:

  • Azione-Qualsiasi cambiamento di stato inizia inviando un’azione Redux (un oggetto semplice contenente un type e payload opzionale).
  • Middleware: l’azione passa attraverso la catena di middleware dello store, dove le azioni possono essere intercettate e può essere eseguito un comportamento aggiuntivo. I middlewares sono comunemente usati per gestire gli effetti collaterali nelle applicazioni Redux.
  • Riduttore-L’azione raggiunge quindi il riduttore radice, che calcola lo stato successivo del negozio come una funzione pura dello stato precedente e dell’azione ricevuta. Il riduttore radice può essere composto da singoli riduttori che gestiscono ciascuno una fetta dello stato del negozio.
  • Nuovo stato-Il negozio salva il nuovo stato restituito dal riduttore e notifica ai suoi abbonati la modifica (in React, tramite connect).
  • Rendering-Infine, la vista connessa allo store può eseguire nuovamente il rendering in base al nuovo stato.

Creazione di un modello di routing Redux-First

La natura unidirezionale del routing lato client e dei modelli del ciclo di vita dei dati Redux si prestano bene a un modello unito che soddisfa i criteri che abbiamo definito per il routing Redux-first.

In questo modello, il router è sottoscritto allo store, la navigazione viene eseguita tramite azioni Redux e gli aggiornamenti della cronologia del browser sono gestiti da un middleware personalizzato. Esaminiamo i dettagli di questo modello:

  • Navigazione interna tramite azioni Redux — Invece di utilizzare direttamente l’API Cronologia, la navigazione interna viene ottenuta inviando una delle 5 azioni di navigazione che rispecchiano i metodi di navigazione cronologia.
  • Aggiornamento della cronologia del browser tramite middleware — Un middleware viene utilizzato per intercettare le azioni di navigazione e gestire l’effetto collaterale dell’aggiornamento della cronologia del browser. Dal momento che la nuova posizione non è necessariamente o facilmente conosciuto senza prima consultare la cronologia del browser(ad es. nel caso di un’azione go), le azioni di navigazione non possono raggiungere il riduttore.
  • Risposta alla navigazione — Il flusso di esecuzione continua con un listener history che risponde alla navigazione (sia dal middleware che dalla navigazione esterna) inviando una seconda azione che contiene la nuova posizione.
  • Riduttore di posizione: l’azione inviata dal listener raggiunge quindi il riduttore di posizione, che aggiunge la posizione all’archivio. Il riduttore di posizione determina anche la forma dello stato di posizione.
  • Routing connesso-Il router collegato al negozio può quindi determinare in modo reattivo il nuovo contenuto della pagina quando viene notificata una modifica della posizione nel negozio.
  • Rendering — Infine, la pagina può essere ri-renderizzata con il nuovo contenuto.

Si noti che questo non è l’unico modo per realizzare il routing Redux-first — alcune varianti prevedono l’uso di un potenziatore di negozi e/o logica aggiuntiva nel middleware — ma è un modello semplice che copre tutte le basi.

Un’implementazione di base

Seguendo il modello che abbiamo appena esaminato, implementiamo l’API principale: le azioni, il middleware, il listener e il riduttore.

Useremo il pacchetto history come dipendenza interna e costruiremo la soluzione in modo incrementale. Se preferisci seguire il risultato finale, puoi vederlo qui.

Azioni

Inizieremo definendo le 5 azioni di navigazione che rispecchiano i metodi di navigazione della cronologia:

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

Quindi, definiamo il middleware. Dovrebbe intercettare le azioni di navigazione, chiamare i corrispondenti metodi di navigazione history, quindi interrompere l’azione dal raggiungere il riduttore, ma lasciare indisturbate tutte le altre azioni:

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

Se non hai avuto la possibilità di scrivere o esaminare gli interni di un middleware Redux prima, dai un’occhiata a questa introduzione.

Listener cronologia

Successivamente, avremo bisogno di un listener history che risponda alla navigazione inviando una nuova azione contenente le nuove informazioni sulla posizione.

Per prima cosa, aggiungiamo il nuovo tipo di azione e creatore. Le parti interessanti della posizione sono le pathname, search, e hash — che cosa sarà incluso nel payload:

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

Quindi scriviamo la funzione listener:

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

faremo una piccola aggiunta di un primo locationChange la spedizione, per conto per la prima iscrizione nell’applicazione (che non preleva la storia ascoltatore):

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

Riduttore

Avanti, cerchiamo di definire la posizione del riduttore. Useremo una forma di stato semplice e faremo un lavoro minimo nel riduttore:

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

Codice dell’applicazione

Infine, colleghiamo la nostra API nel codice dell’applicazione:

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

E questo è tutto quello che c’è da fare! Utilizzando la nostra piccola API (meno di 100 righe di codice), abbiamo soddisfatto tutti i criteri per il routing Redux-first:

  • La posizione è tenuta nel negozio Redux. ✔
  • La posizione viene modificata inviando azioni Redux. ✔
  • L’applicazione legge i dati sulla posizione esclusivamente dal negozio. ✔
  • Lo store e la cronologia del browser sono sincronizzati dietro le quinte. ✔

Visualizza tutti i file insieme qui-sentitevi liberi di importarli nel vostro progetto, o usarlo come punto di partenza per sviluppare la propria implementazione.

Il pacchetto redux-first-routing

Ho anche messo insieme l’API nel pacchetto redux-first-routing, che puoi npm install e utilizzare allo stesso modo.

mksarge/redux-first-routing
redux-first-routing — Una base minima, indipendente dal framework per realizzare il routing Redux-first.github.com

Include un’implementazione simile a quella che abbiamo creato qui, ma con la notevole aggiunta dell’analisi delle query tramite il pacchetto query-string.

Aspetta-e il componente di routing effettivo?

Potresti aver notato che redux-first-routing riguarda solo l’aspetto di navigazione del modello di routing:

Disaccoppiando l’aspetto di navigazione dagli altri aspetti del nostro modello di routing, abbiamo acquisito una certa flessibilità: redux-first-routing è sia indipendente dal router che dal framework.

È quindi possibile accoppiarlo con una libreria come Universal Router per creare una soluzione di routing Redux-first completa per qualsiasi framework front-end:

Clicca qui per iniziare con redux-first-routing + universal-router.

Oppure, potresti creare associazioni supponenti per il tuo framework di scelta-ed è quello che faremo per React nella sezione successiva e finale di questo articolo.

Utilizzo con React

Finiamo la nostra esplorazione osservando come potremmo creare componenti collegati al negozio per la navigazione dichiarativa e il routing in React.

Navigazione dichiarativa

Per la navigazione, possiamo utilizzare un componente<Lin k/ > collegato allo store simile a quello di React Router e di altre soluzioni di routing React.

Semplicemente sovrascrive il comportamento predefinito dell’elemento di ancoraggio < a / > e dispatc hes un’azione push quando si fa clic:

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

Puoi trovare un’implementazione più completa qui.

Routing dichiarativo

Mentre non c’è molto da un componente di navigazione, ci sono innumerevoli modi per progettare un componente di routing — che lo rende la parte più interessante di qualsiasi soluzione di routing.

Che cos’è un router, comunque?

È generalmente possibile visualizzare un router come una funzione o scatola nera con due ingressi e una uscita:

route configuration ↘ matched content current location ↗

Sebbene il routing e il rendering successivo possano verificarsi in passaggi separati, React rende facile e intuitivo raggrupparli in un’API di routing dichiarativa. Diamo un’occhiata a due strategie per realizzare questo.

Strategia 1: Un componente monolitico <Router/>

Possiamo utilizzare un componente monolitico collegato al negozio <Route r/> che:

  • accetta un oggetto di configurazione percorso tramite props
  • legge i dati di localizzazione dall’archivio Redux
  • calcola il nuovo contenuto ogni volta che cambia la posizione
  • rende/ri-rende il contenuto a seconda dei casi.

La configurazione del percorso può essere un semplice oggetto JavaScript che contiene tutti i percorsi e le pagine corrispondenti (una configurazione del percorso centralizzata).

Ecco come potrebbe apparire:

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

Piuttosto semplice, giusto? Non c’è bisogno di percorsi JSX nidificati-solo un singolo oggetto di configurazione del percorso e un singolo componente del router.

Se questa strategia ti piace, controlla la mia implementazione più completa nella libreria redux-json-router. Avvolge redux-first-routing e fornisce collegamenti React per la navigazione dichiarativa e il routing utilizzando le strategie che abbiamo esaminato finora.

mksarge / redux-json-router
redux-json-router-Dichiarativo, Redux – primo routing per React / Redux browser applications.github.com

Strategia 2: Componibile<Rout e / >componenti

Mentre un componente monolitico può essere un modo semplice per ottenere il routing dichiarativo in React, non è sicuramente l’unico modo.

La natura componibile di React consente un’altra possibilità interessante: utilizzare JSX per definire i percorsi in modo decentralizzato. Naturalmente, l’esempio principale è l’API <Route/> di React Router:

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

Anche altre librerie di routing esplorano questa idea. Anche se non ho avuto la possibilità di farlo, non vedo alcun motivo per cui un’API simile non possa essere implementata in cima al pacchetto redux-first-routing.

Invece di fare affidamento sui dati di localizzazione forniti da <BrowserRouter/>, the &l t; Route / > component c ould si mply connettersi al negozio:

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

Se questo è qualcosa che ti interessa costruire o utilizzare, fammi sapere nei commenti! Per ulteriori informazioni sulle diverse strategie di configurazione del percorso, dai un’occhiata a questa introduzione sul sito Web di React Router.

Conclusione

Spero che questa esplorazione abbia aiutato ad approfondire le tue conoscenze sul routing lato client e ti abbia mostrato quanto sia semplice realizzarlo in modo Redux.

Se stai cercando una soluzione di routing Redux completa, puoi utilizzare il pacchetto redux-first-routing con un router compatibile elencato nel readme. E se ti trovi a dover sviluppare una soluzione su misura, speriamo che questo post ti abbia dato un buon punto di partenza per farlo.

Se vuoi saperne di più sul routing lato client in React e Redux, dai un’occhiata ai seguenti articoli: sono stati fondamentali per aiutarmi a capire meglio gli argomenti che ho trattato qui:

  • Lascia che l’URL parli di Tyler Thompson
  • Potresti non aver bisogno di React Router di Konstantin Tarkus
  • Ho anche bisogno di una libreria di routing? di James K. Nelson
  • e innumerevoli discussioni informative nei problemi react-router-redux.

Il routing lato client è uno spazio con infinite possibilità di progettazione, e sono sicuro che alcuni di voi hanno giocato con idee simili a quelle che ho condiviso qui. Se si desidera continuare la conversazione, sarò felice di connettersi con voi nei commenti o via Twitter. Grazie per la lettura!

Modifica 22/06/17: Controlla anche questo articolo su redux-first-router, un progetto separato che utilizza tipi di azione intelligenti per ottenere potenti funzionalità di routing.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.