Lignes directrices pratiques pour le développement d'applications React à grande échelle. Partie 2: gestion des états, routage

Aujourd'hui, nous publions la deuxième partie de la traduction du matériel, qui est consacrée au développement d'applications React à grande échelle. Ici, nous parlerons de la gestion de l'état des applications, du routage et du développement d'interface.



Partie 1: Lignes directrices pratiques pour le développement d'applications React à grande échelle. Planification, actions, sources de données et API

Partie 2: Lignes directrices pratiques pour le développement d'applications React à grande échelle. Partie 2: gestion des états, routage


Gestion de l'état des applications, intégration Redux, organisation du routage


Ici, nous parlerons de la façon dont vous pouvez étendre les fonctionnalités de Redux afin de pouvoir effectuer de manière ordonnée des opérations complexes dans l'application. Si ces mécanismes sont mal mis en œuvre, ils peuvent violer le modèle utilisé dans la conception du référentiel.

Les fonctions du générateur JavaScript peuvent résoudre de nombreux problèmes associés à la programmation asynchrone. Le fait est que ces fonctions peuvent être démarrées et arrêtées à la demande du programmeur. Le middleware Redux-saga utilise ce concept pour gérer les aspects problématiques d'une application. En particulier, nous parlons de résoudre de tels problèmes, qui ne peuvent pas être résolus à l'aide de réducteurs, présentés sous la forme de fonctions pures.

▍Résoudre des tâches qui ne peuvent pas être résolues avec des fonctions pures


Considérez le scénario suivant. On vous a proposé de travailler sur une application conçue pour une entreprise qui travaille sur le marché immobilier. Le client veut obtenir un nouveau site Web meilleur. A votre disposition il y a une API REST, vous avez des mises en page de toutes les pages préparées avec l'aide de Zapier, vous avez esquissé un plan d'application. Mais un gros problème s'est alors posé.

L'entreprise cliente utilise depuis longtemps un certain système de gestion de contenu (CMS). Les employés de l'entreprise connaissent bien ce système, de sorte que le client ne souhaite pas passer à un nouveau CMS simplement pour faciliter la rédaction de nouveaux articles sur un blog d'entreprise. En outre, vous devez également copier les publications existantes du blog vers un nouveau site, ce qui peut également entraîner un problème.

La bonne chose est que le CMS utilisé par le client dispose d'une API pratique à travers laquelle vous pouvez accéder aux publications du blog. Mais, si vous avez créé un agent pour travailler avec cette API, la situation est compliquée par le fait qu'il se trouve sur un certain serveur sur lequel les données ne sont pas présentées du tout comme vous en avez besoin.

Ceci est un exemple de problème, quelque chose qui pourrait polluer le code de l'application, car ici vous devez inclure dans les mécanismes du projet pour travailler avec la nouvelle API utilisée pour télécharger les articles de blog. Vous pouvez gérer cette situation avec redux-saga.
Jetez un œil au schéma suivant. C'est ainsi que notre application et notre API interagissent. Nous téléchargeons les publications en arrière-plan à l'aide de redux-saga.


Diagramme d'application utilisant le stockage Redux et redux-saga

Ici, le composant distribue l'action GET.BLOGS . L'application utilise redux-saga, donc cette demande sera interceptée. Après cela, la fonction de générateur téléchargera les données du magasin de données en arrière-plan et mettra à jour l'état de l'application pris en charge par Redux.

Voici un exemple de la façon dont la fonction de générateur de chargement de publications (ces fonctions sont appelées «sagas») pourrait ressembler dans la situation décrite. Les sagas peuvent être utilisées dans d'autres scénarios. Par exemple, pour organiser le stockage des données utilisateur (par exemple, il peut s'agir de jetons), car il s'agit d'un autre exemple de tâche pour laquelle les fonctions pures ne conviennent pas.

 ... function* fetchPosts(action) { if (action.type === WP_POSTS.LIST.REQUESTED) {   try {     const response = yield call(wpGet, {       model: WP_POSTS.MODEL,       contentType: APPLICATION_JSON,       query: action.payload.query,     });     if (response.error) {       yield put({         type: WP_POSTS.LIST.FAILED,         payload: response.error.response.data.msg,       });       return;         yield put({       type: WP_POSTS.LIST.SUCCESS,       payload: {         posts: response.data,         total: response.headers['x-wp-total'],         query: action.payload.query,       },       view: action.view,     });   } catch (e) {     yield put({ type: WP_POSTS.LIST.FAILED, payload: e.message });  ... 

La saga présentée ici attend des actions comme WP_POSTS.LIST.REQUESTED . En recevant une telle action, il charge les données de l'API. Après avoir reçu les données, elle envoie une autre action - WP_POSTS.LIST.SUCCESS . Son traitement conduit à mettre à jour le référentiel à l'aide du réducteur approprié.

▍ Introduction de réducteurs


Lors du développement de grandes applications, il est impossible de planifier à l'avance le dispositif de tous les modèles nécessaires. De plus, à mesure que la taille de l'application augmente, l'utilisation de la technologie d'introduction de réducteurs permet d'économiser une grande quantité d'heures de travail. Cette technique permet aux développeurs d'ajouter de nouveaux réducteurs au système sans réécrire l'intégralité du référentiel.

Il existe des bibliothèques conçues pour créer des référentiels Redux dynamiques. Cependant, je préfère le mécanisme d'introduction des réducteurs, car il donne au développeur un certain niveau de flexibilité. Par exemple, une application existante peut être équipée de ce mécanisme sans avoir à réorganiser sérieusement l'application.

L'introduction de réducteurs est une forme de séparation de code. La communauté des développeurs React adopte avec enthousiasme cette technologie. J'utiliserai ce morceau de code pour démontrer l'apparence et les caractéristiques du mécanisme d'implémentation des réducteurs.

Tout d'abord, regardons son intégration avec Redux:

 ... const withConnect = connect( mapStateToProps, mapDispatchToProps, ); const withReducer = injectReducer({ key: BLOG_VIEW, reducer: blogReducer, }); class BlogPage extends React.Component {  ... } export default compose( withReducer, withConnect, )(BlogPage); 

Ce code fait partie du fichier BlogPage.js qui contient le composant d'application.

Ici, dans la commande d'exportation, nous n'utilisons pas la fonction connect , mais la fonction compose . C'est l'une des fonctions de la bibliothèque Redux qui vous permet de composer plusieurs fonctions. La liste des fonctions passées à compose doit être lue de droite à gauche ou de bas en haut.

Vous pouvez apprendre de la documentation Redux que la fonction de compose vous permet de créer des transformations de fonctions profondément imbriquées. Dans ce cas, le programmeur est libéré de la nécessité d'utiliser des structures très longues. Ces constructions ressemblent à des lignes de code représentant des appels à certaines fonctions et leur transmettent les résultats des appels à d'autres fonctions comme arguments. La documentation note que la fonction de compose doit être utilisée avec prudence.

La fonction la plus à droite dans une composition peut prendre de nombreux arguments, mais un seul argument peut être transmis aux fonctions qui la suivent. Par conséquent, en appelant la fonction résultant de l'utilisation de la compose , nous lui transmettons ce qu'elle prend des fonctions d'origine, ce qui est à la droite de toutes les autres. C'est pourquoi nous avons passé la fonction compose à la fonction withConnect comme dernier paramètre. Par conséquent, la fonction de compose peut être utilisée comme la fonction de connect .

▍ Routage et Redux


Il existe un certain nombre d'outils utilisés pour résoudre les problèmes de routage dans les applications. Dans cette section, cependant, nous nous concentrerons sur la bibliothèque react-router-dom . Nous allons étendre ses capacités afin qu'il puisse fonctionner avec Redux.

Le plus souvent, le routeur React est utilisé comme ceci: le composant racine est enfermé dans la balise BrowserRouter et les conteneurs enfants sont enveloppés dans la méthode withRouter() et exportés ( voici un exemple).

Avec cette approche, le composant enfant reçoit, via le mécanisme d' props , un objet history contenant certaines propriétés spécifiques à la session utilisateur en cours. Il existe certaines méthodes dans cet objet qui peuvent être utilisées pour contrôler la navigation.

Cette option de routage peut entraîner des problèmes dans les grandes applications. Cela est dû au fait qu'ils n'ont pas d'objet history centralisé. En outre, les composants qui ne sont pas rendus avec <Route> ne peuvent pas fonctionner avec l'objet history . Voici un exemple utilisant <Route> :

 <Route path="/" exact component={HomePage} /> 

Pour résoudre ce problème, nous utiliserons la bibliothèque connected-react-router , qui nous permettra d'établir le routage à l'aide de la méthode de dispatch . L'intégration de cette bibliothèque dans le projet nécessitera quelques modifications. En particulier, il sera nécessaire de créer un nouveau réducteur conçu spécifiquement pour les itinéraires (c'est assez évident), et également d'ajouter quelques mécanismes auxiliaires au système.

Une fois sa configuration terminée, le nouveau système de routage peut être utilisé via Redux. Ainsi, la navigation dans l'application peut être implémentée en envoyant des actions.

Afin de tirer parti des capacités de la bibliothèque de routeur connecté-réactif dans le composant, nous mappons simplement la méthode de dispatch au référentiel, ceci en fonction des besoins de l'application. Voici un exemple de code qui illustre l'utilisation de la bibliothèque connected-react-router (pour que ce code fonctionne, vous avez besoin que le reste du système soit configuré pour utiliser connected-react-router).

 import { push } from 'connected-react-router' ... const mapDispatchToProps = dispatch => ({  goTo: payload => {    dispatch(push(payload.path));  }, }); class DemoComponent extends React.Component {  render() {    return (      <Child        onClick={          () => {            this.props.goTo({ path: `/gallery/`});                      />    } ... 

Ici, la méthode goTo distribue une action qui pousse l'URL requise goTo pile d'historique goTo navigateur. Auparavant, la méthode goTo était goTo au référentiel. Par conséquent, cette méthode est transmise au DemoComponent dans l'objet props .

Interface utilisateur dynamique et besoins d'une application en pleine croissance


Au fil du temps, malgré la présence d'un backend adéquat pour l'application et d'une partie client de haute qualité, certains éléments de l'interface utilisateur commencent à affecter gravement le travail de l'utilisateur. Cela est dû à l'implémentation irrationnelle des composants qui, à première vue, semble très simple. Dans cette section, nous discuterons des recommandations pour la création de certains widgets. Leur mise en œuvre correcte, à mesure que l'application se développe, devient plus compliquée.

▍ Chargement paresseux et React.Suspense


La meilleure partie de la nature asynchrone de JavaScript est qu'il tire parti de tout le potentiel du navigateur. Peut-être que le véritable avantage est que pour démarrer un certain processus, vous n'avez pas besoin d'attendre la fin de la tâche précédente. Cependant, les développeurs ne peuvent pas influencer le réseau et la vitesse de chargement des différents matériaux nécessaires au fonctionnement des sites.

Les sous-systèmes réseau sont généralement perçus comme peu fiables et sujets aux erreurs.

Le développeur, dans un souci de rendre sa candidature la plus haute qualité possible, peut la soumettre à de nombreux contrôles et réussir son passage. Mais il y a encore certaines choses, comme l'état de la connexion réseau ou le temps de réponse du serveur, que le développeur ne peut pas influencer.

Mais les créateurs du logiciel ne cherchent pas à justifier le travail de mauvaise qualité des applications avec des phrases comme «ce n'est pas mon affaire». Ils ont trouvé des moyens intéressants de résoudre les problèmes de réseau.

Dans certaines parties de l' application frontale, vous devrez peut-être afficher certains matériaux de sauvegarde (tels que le chargement beaucoup plus rapide que les matériaux réels). Cela évitera à l'utilisateur d'envisager le "tics" des pages de chargement ou, pire encore, de telles icônes.


Les utilisateurs feraient mieux de ne rien voir de tel.

La technologie React Suspense vous permet de faire face à ces problèmes. Par exemple, il vous permet d'afficher un certain indicateur lors du chargement des données. Bien que cela puisse également être fait manuellement en définissant la propriété isLoaded sur true , l'utilisation de l'API Suspense rend le code beaucoup plus propre.

Ici, vous pouvez regarder une bonne vidéo sur Suspense, dans laquelle Jared Palmer présente au public cette technologie et montre certaines de ses fonctionnalités à l'aide d'un exemple d'une application réelle .

Voici comment fonctionne l'application sans utiliser Suspense.


Application dans laquelle Suspense n'est pas utilisé

Équiper un composant avec la prise en charge de Suspense est beaucoup plus facile que d'utiliser isLoaded échelle de l' isLoaded . Commençons par placer le conteneur d' App parent dans React.StrictMode . Nous veillons à ce que parmi les modules React utilisés dans l'application, il n'y en ait pas qui soient considérés comme obsolètes.

 <React.Suspense fallback={<Spinner size="large" />}>  <ArtistDetails id={this.props.id}/>  <ArtistTopTracks />  <ArtistAlbums id={this.props.id}/> </React.Suspense> 

Les composants enveloppés dans des balises React.Suspense , lors du chargement du contenu principal, chargent et affichent ce qui est spécifié dans la propriété de fallback . Nous devons nous efforcer de faire en sorte que les composants utilisés dans la propriété de fallback aient le plus petit volume possible et soient disposés aussi simplement que possible.


Application qui utilise Suspense

▍Composants adaptatifs


Dans les grandes applications frontales, la manifestation de motifs répétitifs est courante. Dans le même temps, au tout début des travaux, cela peut être presque totalement non évident. Il n'y a rien à faire à ce sujet, mais vous devez l'avoir rencontré.

Par exemple, il existe deux modèles dans l'application. L'un d'eux est destiné à la description des pistes de course, et le second - à la description des voitures. La page de liste de voitures utilise des éléments carrés. Chacun d'eux contient une image et une brève description.

La liste de trace utilise des éléments similaires. Leur principale caractéristique est qu'en plus de l'image et de la description de la piste, ils ont également un petit champ indiquant s'il est possible pour les spectateurs d'une course se déroulant sur cette piste d'acheter quelque chose à manger.


Élément pour décrire une voiture et élément pour décrire une piste

Ces deux composants sont légèrement différents l'un de l'autre en termes de style (ils ont des couleurs de fond différentes). Le composant qui décrit l'itinéraire contient des informations supplémentaires sur l'objet du monde réel qu'il décrit, tandis que le composant qui symbolise la voiture ne dispose pas de ces informations. Cet exemple montre seulement deux modèles. Dans une grande application, de nombreux modèles similaires peuvent être saisis, ne différant que par de petites choses.

La création de composants indépendants séparés pour chacune de ces entités est contraire au bon sens.

Un programmeur peut se sauver le besoin d'écrire des fragments de code qui se répètent presque complètement. Cela peut se faire grâce au développement de composants adaptatifs. Ils, au cours du travail, tiennent compte de l'environnement dans lequel ils ont été chargés. Considérez la barre de recherche d'une certaine application.


Barre de recherche

Il sera utilisé sur de nombreuses pages. Dans le même temps, des modifications mineures seront apportées à son apparence et à l'ordre de son travail sur différentes pages. Par exemple, sur la page d'accueil du projet, il sera légèrement plus grand que sur les autres pages. Afin de résoudre ce problème, vous pouvez créer un seul composant qui sera affiché en fonction des propriétés qui lui sont transférées.

 static propTypes = {  open: PropTypes.bool.isRequired,  setOpen: PropTypes.func.isRequired,  goTo: PropTypes.func.isRequired, }; 

En utilisant cette technique, vous pouvez contrôler l'utilisation des classes HTML lors du rendu de ces éléments, ce qui vous permet d'influencer leur apparence.

Une autre situation intéressante dans laquelle les composants adaptatifs peuvent trouver une application est le mécanisme de décomposition de certains matériaux en pages. Une barre de navigation peut être présente sur chaque page de l'application. Les instances de ce panneau sur chacune des pages seront presque exactement les mêmes que sur les autres pages.


Panneau de pagination

Supposons qu'une certaine application ait besoin d'un panneau similaire. Lorsqu'ils travaillent sur cette application, les développeurs respectent les exigences articulées en temps opportun. Dans une telle situation, le composant adaptatif utilisé pour diviser le matériau en pages n'a besoin que de quelques propriétés. Il s'agit de l' URL et du nombre d'éléments par page.

Résumé


L'écosystème React est devenu si mature ces jours-ci qu'il est peu probable que quiconque ait jamais besoin «d'inventer un vélo» à n'importe quel stade du développement de l'application. Bien que cela joue entre les mains des développeurs, cela conduit au fait qu'il devient difficile de choisir exactement ce qui fonctionne bien pour chaque projet spécifique.

Chaque projet est unique en termes de portée et de fonctionnalité. Il n'y a pas d'approche unique ni de règle universelle dans le développement d'applications React. Par conséquent, avant de commencer le développement, il est important de le planifier correctement.

Lors de la planification, il est très facile de comprendre quels outils sont directement créés pour le projet et lesquels ne sont clairement pas adaptés à lui, étant trop grands pour lui. Par exemple, une application composée de 2-3 pages et effectuant très peu de requêtes vers certaines API n'a pas besoin de magasins de données similaires à ceux dont nous avons parlé. Je suis prêt à aller encore plus loin dans ces considérations et à dire que dans les petits projets, vous n'avez pas besoin d'utiliser Redux.
Au stade de la planification de l'application, tout en dessinant les dispositions de ses pages, il est facile de voir que de nombreux composants similaires sont utilisés sur ces pages. Si vous essayez de réutiliser le code de tels composants ou que vous vous efforcez d'écrire des composants universels, cela vous permettra d'économiser beaucoup de temps et d'efforts.

Et enfin, je voudrais noter que les données sont au cœur de toute application. Et les applications React ne font pas exception. Au fur et à mesure que l'échelle de l'application augmente, les volumes de données traitées augmentent, des mécanismes logiciels supplémentaires pour travailler avec eux apparaissent. Quelque chose comme ça, si l'application est mal conçue, peut facilement «écraser» les programmeurs, les remplissant de tâches complexes et déroutantes. Si, au cours de la planification, les problèmes d'utilisation des entrepôts de données étaient décidés à l'avance, si l'ordre de travail des actions, des réducteurs, de l'affaissement était pensé à l'avance, alors travailler sur l'application serait beaucoup plus facile.

Chers lecteurs! Si vous connaissez des bibliothèques ou des méthodologies de développement qui fonctionnent bien lors de la création d'applications React à grande échelle, veuillez les partager.

Source: https://habr.com/ru/post/fr458498/


All Articles