par Michael Sargent

Une bibliothèque de routage est un composant clé de toute application complexe d’une seule page. Si vous développez des applications Web avec React et Redux, vous avez probablement utilisé, ou du moins entendu parler de React Router. C’est une bibliothèque de routage bien connue pour React, et une excellente solution pour de nombreux cas d’utilisation.

Mais React Router n’est pas la seule solution viable dans l’écosystème React/ Redux. En fait, il existe des tonnes de solutions de routage conçues pour React et pour Redux, chacune avec des API, des fonctionnalités et des objectifs différents — et la liste ne fait que s’allonger. Inutile de dire que le routage côté client ne disparaîtra pas de sitôt, et il reste encore beaucoup d’espace pour la conception dans les bibliothèques de routage de demain.

Aujourd’hui, je veux attirer votre attention sur le sujet du routage dans Redux. Je présenterai et plaiderai en faveur du routage Redux-first – un paradigme qui fait de Redux la star du modèle de routage et le fil conducteur parmi de nombreuses solutions de routage Redux. Je vais montrer comment assembler l’API de base, indépendante du framework, en moins de 100 lignes de code, avant d’explorer des options pour une utilisation réelle avec React et d’autres frameworks frontaux.

Un Peu d’Histoire

Dans le navigateur, l’emplacement (informations d’URL) et l’historique de session (une pile d’emplacements visités par l’onglet du navigateur actuel) sont stockés dans l’objet global window. Ils sont accessibles via:

  • window.location ( API de LOCALISATION)
  • window.history ( Historique API).

L’API Historique propose les méthodes de navigation historiques suivantes, remarquables pour leur capacité à mettre à jour l’historique et l’emplacement du navigateur sans nécessiter de rechargement de page:

  • pushState(href) — pousse un nouvel emplacement sur la pile d’historique
  • replaceState(href) — écrase l’emplacement actuel sur la pile
  • back() — navigue vers l’emplacement précédent sur la pile
  • forward() — navigue vers l’emplacement suivant sur la pile
  • go(index) — navigue vers un emplacement sur la pile, dans les deux sens.

Ensemble, les API d’historique et de localisation permettent le paradigme moderne de routage côté client connu sous le nom de routage pushState – le premier protagoniste de notre histoire.

Maintenant, c’est presque un crime de mentionner les API d’historique et de localisation sans mentionner une bibliothèque wrapper moderne comme history.

ReactTraining/history
Gérer l’historique des sessions avec JavaScriptgithub.com

history fournit une API simple mais puissante pour l’interfaçage avec l’historique et l’emplacement du navigateur, tout en couvrant les incohérences entre les différentes implémentations du navigateur. Il est utilisé comme une dépendance pair ou interne dans de nombreuses bibliothèques de routage modernes, et j’y ferai plusieurs références tout au long de cet article.

Routage Redux et pushState

Le deuxième protagoniste de notre histoire est Redux. Nous sommes en 2017, je vais donc vous épargner l’introduction et aller droit au but:

En utilisant un routage pushState simple dans une application Redux, nous divisons l’état de l’application sur deux domaines: l’historique du navigateur et le magasin Redux.

Voici à quoi cela ressemble avec React Router, qui instancie et enveloppe history:

history → React Router ↘ view Redux ↗

Maintenant, nous savons que toutes les données ne doivent pas résider dans le magasin. Par exemple, l’état du composant local est souvent un endroit approprié pour stocker des données spécifiques à un seul composant.

Mais les données de localisation ne sont pas triviales. C’est une partie dynamique et importante de l’état de l’application — le type de données qui appartient au magasin. Le maintien dans le magasin permet des luxes Redux comme le débogage de voyage dans le temps et un accès facile depuis n’importe quel composant connecté au magasin.

Alors, comment déplacer l’emplacement dans le magasin?

Il n’est pas possible de contourner le fait que le navigateur lit et stocke l’historique et les informations de localisation dans le window, mais ce que nous pouvons faire est de conserver une copie des données de localisation dans le magasin et de les synchroniser avec le navigateur.

N’est-ce pas ce que react-router-redux fait pour le routeur React?

Oui, mais uniquement pour activer les capacités de voyage dans le temps des DevTools Redux. L’application dépend toujours des données de localisation détenues dans le routeur React:

history → React Router ↘ ↕ view Redux ↗

L’utilisation de react-router-redux pour lire les données de localisation du magasin au lieu du routeur React est déconseillée (en raison de sources de vérité potentiellement contradictoires).

Pouvons-nous faire mieux?

Pouvons—nous construire un modèle de routage alternatif — un modèle construit à partir du sol pour bien jouer avec Redux, nous permettant de lire et de mettre à jour l’emplacement de la manière Redux – avec store.getState() et store.dispatch()?

Nous le pouvons absolument, et cela s’appelle le routage Redux-first.

Redux – Premier routage

Redux-first routing est une variante du routage pushState qui fait de Redux l’étoile du modèle de routage.

Une solution de routage Redux-first satisfait aux critères suivants:

  • L’emplacement a lieu dans le magasin Redux.
  • L’emplacement est modifié en envoyant des actions Redux.
  • L’application lit les données de localisation uniquement à partir du magasin.
  • L’historique du magasin et du navigateur est synchronisé en coulisses.

Voici une idée de base de ce à quoi cela ressemble:

history ↕ Redux → router → view

Attendez, n’y a-t-il pas encore deux sources de données de localisation?

Oui, mais si nous pouvons croire que l’historique du navigateur et le magasin Redux sont synchronisés, nous pouvons créer nos applications pour ne lire que les données de localisation du magasin. Ensuite, du point de vue de l’application, il n’y a qu’une seule source de vérité: le magasin.

Comment accomplissons-nous le routage Redux-first ?

Nous pouvons commencer par créer un modèle conceptuel, en fusionnant les éléments fondamentaux des modèles de routage côté client et de cycle de vie des données Redux.

Revisiter le Modèle de routage Côté Client

Le routage côté client est un processus en plusieurs étapes qui commence par la navigation et se termine par le rendu — le routage lui-même n’est qu’une étape de ce processus! Passons en revue les détails:

  • Navigation – Tout commence par un changement d’emplacement. Il existe 2 types de navigation: interne et externe. La navigation interne est effectuée à partir de l’application (par exemple. via l’API d’historique), tandis que la navigation externe se produit lorsque l’utilisateur interagit avec la barre de navigation du navigateur ou entre dans l’application à partir d’un site externe.
  • Réponse à la navigation – Lorsque l’emplacement change, l’application répond en transmettant le nouvel emplacement au routeur. Les anciennes techniques de routage reposaient sur l’interrogation window.location pour y parvenir, mais de nos jours, nous avons l’utilitaire history.listen pratique.
  • Routage – Ensuite, le nouvel emplacement est mis en correspondance avec le contenu de la page correspondante. Le code qui gère cette étape s’appelle un routeur, et il prend généralement un paramètre d’entrée de routes et de pages correspondantes appelé configuration de route.
  • Rendu – Enfin, le contenu est rendu sur le client. Cette étape peut, bien sûr, être gérée par un framework / bibliothèque frontal comme React.

Notez que les bibliothèques de routage n’ont pas à gérer toutes les parties du modèle de routage.

Certaines bibliothèques, comme React Router et Vue Router, le font – tandis que d’autres, comme Universal Router, ne concernent qu’un seul aspect (comme le routage), offrant ainsi une flexibilité dans les autres aspects:

Les bibliothèques de routage peuvent avoir des champs de responsabilité différents. (Cliquez pour agrandir)

Revisiter le Modèle de cycle de vie des données Redux

Redux dispose d’un modèle de flux / cycle de vie de données à sens unique qui n’a probablement pas besoin d’être présenté — mais voici un bref aperçu pour faire bonne mesure:Action

  • – Tout changement d’état commence par l’envoi d’une action Redux (un objet simple contenant une charge utile type et facultative).
  • Middleware — L’action passe par la chaîne de middlewares du magasin, où les actions peuvent être interceptées et un comportement supplémentaire peut être exécuté. Les middlewares sont couramment utilisés pour gérer les effets secondaires dans les applications Redux.
  • Réducteur — L’action atteint alors le réducteur racine, qui calcule l’état suivant du magasin comme une fonction pure de l’état précédent et de l’action reçue. Le réducteur de racine peut être composé de réducteurs individuels qui gèrent chacun une tranche de l’état du magasin.
  • Nouvel état — Le magasin enregistre le nouvel état renvoyé par le réducteur et informe ses abonnés du changement (dans React, via connect).Rendu
  • – Enfin, la vue connectée au magasin peut se restituer conformément au nouvel état.

Construction d’un modèle de routage Redux-First

La nature unidirectionnelle du routage côté client et des modèles de cycle de vie des données Redux se prêtent bien à un modèle fusionné qui répond aux critères que nous avons définis pour le routage Redux-first.

Dans ce modèle, le routeur est abonné au magasin, la navigation s’effectue via des actions Redux et les mises à jour de l’historique du navigateur sont gérées par un middleware personnalisé. Examinons les détails de ce modèle:

  • Navigation interne via des actions Redux — Au lieu d’utiliser directement l’API d’historique, la navigation interne est obtenue en envoyant l’une des 5 actions de navigation qui reflètent les méthodes de navigation de l’historique.
  • Mise à jour de l’historique du navigateur via un middleware — Un middleware est utilisé pour intercepter les actions de navigation et gérer l’effet secondaire de la mise à jour de l’historique du navigateur. Puisque le nouvel emplacement n’est pas nécessairement ou facilement connu sans d’abord consulter l’historique du navigateur (par exemple. dans le cas d’une action go), les actions de navigation sont empêchées d’atteindre le réducteur.
  • Réponse à la navigation — Le flux d’exécution se poursuit avec un écouteur history qui répond à la navigation (à partir du middleware et de la navigation externe) en envoyant une deuxième action contenant le nouvel emplacement.
  • Réducteur d’emplacement — L’action envoyée par l’auditeur atteint ensuite le réducteur d’emplacement, qui ajoute l’emplacement au magasin. Le réducteur d’emplacement détermine également la forme de l’état d’emplacement.
  • Routage connecté — Le routeur connecté au magasin peut alors déterminer de manière réactive le nouveau contenu de la page lorsqu’il est informé d’un changement d’emplacement dans le magasin.
  • Rendu – Enfin, la page peut être restituée avec le nouveau contenu.

Notez que ce n’est pas la seule façon d’accomplir le routage Redux-first — certaines variantes comportent l’utilisation d’un enhancer de magasin et / ou d’une logique supplémentaire dans le middleware — mais c’est un modèle simple qui couvre toutes les bases.

Une Implémentation de base

Suivant le modèle que nous venons d’examiner, implémentons l’API de base — les actions, le middleware, l’écouteur et le réducteur.

Nous utiliserons le package history comme dépendance interne et construirons la solution de manière incrémentielle. Si vous préférez suivre le résultat final, vous pouvez le voir ici.

Actions

Nous allons commencer par définir les 5 actions de navigation qui reflètent les méthodes de navigation de l’historique:

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

Ensuite, définissons le middleware. Il doit intercepter les actions de navigation, appeler les méthodes de navigation history correspondantes, puis empêcher l’action d’atteindre le réducteur — mais laisser toutes les autres actions intactes:

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

Si vous n’avez pas eu la chance d’écrire ou d’examiner les composants internes d’un middleware Redux auparavant, consultez cette introduction.

Écouteur d’historique

Ensuite, nous aurons besoin d’un écouteur history qui répond à la navigation en envoyant une nouvelle action contenant les nouvelles informations de localisation.

Tout d’abord, ajoutons le nouveau type d’action et le nouveau créateur. Les parties intéressantes de l’emplacement sont les pathname, search et hash — c’est donc ce que nous allons inclure dans la charge utile:

// constants.jsexport const LOCATION_CHANGE = 'ROUTER/LOCATION_CHANGE';

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

Ensuite, écrivons la fonction d’écoute:

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

Nous allons faire un petit ajout – une expédition initiale locationChange, pour tenir compte de l’entrée initiale dans l’application (qui n’est pas captée par l’écouteur d’historique):

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

Réducteur

Ensuite, définissons le réducteur d’emplacement. Nous allons utiliser une forme d’état simple et faire un travail minimal dans le réducteur:

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

Code d’application

Enfin, connectons notre API au code d’application:

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

Et c’est tout ce qu’il y a à faire! En utilisant notre API minuscule (moins de 100 lignes de code), nous avons satisfait à tous les critères de routage Redux-first:

  • L’emplacement a lieu dans le magasin Redux. ✔
  • L’emplacement est modifié en envoyant des actions Redux. ✔
  • L’application lit les données de localisation uniquement à partir du magasin. ✔
  • L’historique du magasin et du navigateur est synchronisé en coulisses. ✔

Visualisez tous les fichiers ensemble ici — n’hésitez pas à les importer dans votre projet ou à les utiliser comme point de départ pour développer votre propre implémentation.

Le package redux-first-routing

J’ai également rassemblé l’API dans le package redux-first-routing, que vous pouvez npm install et utiliser de la même manière.

mksarge/redux-first-routing
redux-first-routing – Une base minimale, indépendante du framework, pour réaliser le routage Redux-first.github.com

Il inclut une implémentation similaire à celle que nous avons construite ici, mais avec l’ajout notable de l’analyse des requêtes via le package query-string.

Attendez — qu’en est-il du composant de routage réel?

Vous avez peut-être remarqué que redux-first-routing ne concerne que l’aspect de navigation du modèle de routage:

En découplant l’aspect de navigation des autres aspects de notre modèle de routage, nous avons gagné en flexibilité — redux-first-routing est à la fois agnostique au routeur et agnostique au framework.

Vous pouvez donc l’associer à une bibliothèque comme Universal Router pour créer une solution de routage Redux-first complète pour n’importe quel framework frontal:

Cliquez ici pour commencer avec redux-first-routing + routeur universel.

Ou, vous pouvez créer des liaisons opiniâtres pour votre framework de choix — et c’est ce que nous ferons pour React dans la prochaine et dernière section de cet article.

Utilisation avec React

Terminons notre exploration en examinant comment nous pourrions créer des composants connectés au magasin pour la navigation déclarative et le routage dans React.

Navigation déclarative

Pour la navigation, nous pouvons utiliser un composant <Link/ > connecté au magasin similaire à celui du routeur React et d’autres solutions de routage React.

Il remplace simplement le comportement par défaut de l’élément d’ancrage <a/> et dispatc hes une action push lorsque vous cliquez dessus:

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

Vous pouvez trouver une implémentation plus complète ici.

Routage déclaratif

Bien qu’il n’y ait pas grand—chose dans un composant de navigation, il existe d’innombrables façons de concevoir un composant de routage, ce qui en fait la partie la plus intéressante de toute solution de routage.

Qu’est-ce qu’un routeur, de toute façon?

Vous pouvez généralement afficher un routeur comme une fonction ou une boîte noire avec deux entrées et une sortie:

route configuration ↘ matched content current location ↗

Bien que le routage et le rendu ultérieur puissent se dérouler en différentes étapes, React facilite et simplifie leur regroupement dans une API de routage déclarative. Examinons deux stratégies pour y parvenir.

Stratégie 1 : Un composant <Router/>monolithique

Nous pouvons utiliser un composant <Router/>monolithique connecté au magasin qui:

  • accepte un objet de configuration de route via des accessoires
  • lit les données de localisation du magasin Redux
  • calcule le nouveau contenu chaque fois que l’emplacement change
  • rend/restitue le contenu selon les besoins.

La configuration d’itinéraire peut être un objet JavaScript simple qui contient tous les chemins et pages correspondants (une configuration d’itinéraire centralisée).

Voici à quoi cela pourrait ressembler:

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

Assez simple, non? Pas besoin de routes JSX imbriquées — un seul objet de configuration de route et un seul composant de routeur.

Si cette stratégie vous plaît, consultez mon implémentation plus complète dans la bibliothèque redux-json-router. Il enveloppe redux-first-routing et fournit des liaisons React pour la navigation déclarative et le routage en utilisant les stratégies que nous avons examinées jusqu’à présent.

mksarge/redux-json-router
redux-json-router-Déclaratif, Redux-premier routage pour le navigateur React/Redux applications.github.com

Stratégie 2: Composable <Rout e / > composants

Bien qu’un composant monolithique puisse être un moyen simple d’obtenir un routage déclaratif dans React, ce n’est certainement pas le seul moyen.

La nature composable de React permet une autre possibilité intéressante : utiliser JSX pour définir des routes de manière décentralisée. Bien sûr, l’exemple principal est l’API <Route/> de React Router:

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

D’autres bibliothèques de routage explorent également cette idée. Bien que je n’ai pas eu la chance de le faire, je ne vois aucune raison pour laquelle une API similaire ne pourrait pas être implémentée au-dessus du package redux-first-routing.

Au lieu de s’appuyer sur les données de localisation fournies par <BrowserRoute r / > , the &l t; Route /> composant c ould si connectez-vous au magasin:

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

Si c’est quelque chose que vous souhaitez construire ou utiliser, faites-le moi savoir dans les commentaires! Pour en savoir plus sur les différentes stratégies de configuration de routage, consultez cette introduction sur le site Web de React Router.

Conclusion

J’espère que cette exploration a contribué à approfondir vos connaissances sur le routage côté client et vous a montré à quel point il est simple de l’accomplir à la manière de Redux.

Si vous recherchez une solution de routage Redux complète, vous pouvez utiliser le package redux-first-routing avec un routeur compatible répertorié dans le fichier readme. Et si vous avez besoin de développer une solution sur mesure, j’espère que cet article vous a donné un bon point de départ pour le faire.

Si vous souhaitez en savoir plus sur le routage côté client dans React et Redux, consultez les articles suivants – ils m’ont aidé à mieux comprendre les sujets que j’ai traités ici:

  • Laissez L’URL Parler par Tyler Thompson
  • Vous N’Avez Peut-Être Pas Besoin Du Routeur React de Konstantin Tarkus
  • Ai-Je Même Besoin D’une Bibliothèque De Routage? par James K. Nelson
  • et d’innombrables discussions informatives dans les numéros react-router-redux.

Le routage côté client est un espace avec des possibilités de conception infinies, et je suis sûr que certains d’entre vous ont joué avec des idées similaires à celles que j’ai partagées ici. Si vous souhaitez poursuivre la conversation, je serai heureux de vous contacter dans les commentaires ou via Twitter. Merci d’avoir lu!

Modifier le 22/06/17: Consultez également cet article sur redux-first-router, un projet distinct qui utilise des types d’actions intelligentes pour obtenir de puissantes capacités de routage.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.