
Dans un
article précédent, nous avons examiné ce que sont les flux et ce avec quoi ils mangent. Dans la nouvelle partie, nous allons nous familiariser avec les méthodes que RxJS fournit pour créer des flux, quels sont les opérateurs, les tuyaux et comment travailler avec eux.
Série d'articles "Fondamentaux de la programmation réactive utilisant RxJS":
RxJS possède une
API riche. La documentation décrit plus d'une centaine de méthodes. Pour les connaître un peu, nous écrirons une application simple et en pratique nous verrons à quoi ressemble le code réactif. Vous verrez que les mêmes tâches, qui semblaient routinières et nécessitaient d'écrire beaucoup de code, ont une solution élégante si vous les regardez à travers le prisme de la réactivité. Mais avant de commencer, nous allons voir comment les flux peuvent être représentés graphiquement et se familiariser avec les méthodes pratiques pour les créer et les traiter.
Représentation graphique des threads
Pour montrer clairement comment se comporte un flux particulier, j'utiliserai la notation adoptée dans l'approche réactive. Rappelons notre exemple de l'article précédent:
const observable = new Observable((observer) => { observer.next(1); observer.next(2); observer.complete(); });
Voici à quoi ressemblera sa représentation graphique:

Le flux est généralement représenté par une ligne droite. Si le flux émet une valeur, il s'affiche sur la ligne sous forme de cercle. Une ligne droite à l'écran est le signal de fin du flux. Pour afficher l'erreur, utilisez le symbole - «×».
const observable = new Observable((observer) => { observer.error(); });

Flux d'une ligne
Dans ma pratique, je devais rarement créer mes propres instances observables directement. La plupart des méthodes de création de threads sont déjà dans RxJS. Pour créer un flux émettant les valeurs 1 et 2, il suffit d'utiliser la méthode of:
const observable = of(1, 2);
La méthode of accepte n'importe quel nombre d'arguments et retourne une instance finie de l'Observable. Après s'être abonné, il émettra les valeurs reçues et complétera:

Si vous souhaitez représenter le tableau sous forme de flux, vous pouvez utiliser la méthode from. La méthode from en tant qu'argument attend tout objet itérable (tableau, chaîne, etc.) ou promesse, et projette cet objet sur le flux. Voici à quoi ressemblera le flux obtenu à partir de la chaîne:
const observable = from('abc');

Et donc, vous pouvez envelopper une promesse dans un flux:
const promise = new Promise((resolve, reject) => { resolve(1); }); const observable = from(promise);
Remarque: souvent, les discussions sont comparées à la promesse. En fait, ils n'ont qu'une chose en commun: une
stratégie de poussée pour propager le changement. Les autres sont des entités complètement différentes. La promesse ne peut pas produire plusieurs valeurs. Il peut uniquement exécuter la résolution ou le rejet, c'est-à-dire n'ont que deux états. Un flux peut transmettre plusieurs valeurs et peut être réutilisé.
Vous souvenez-vous de l'exemple avec l'intervalle du
premier article ? Ce flux est une minuterie qui compte le temps en secondes à partir du moment de l'abonnement.
const timer = new Observable(observer => { let counter = 0; const intervalId = setInterval(() => { observer.next(counter++); }, 1000); return () => { clearInterval(intervalId); } });
Voici comment vous pouvez implémenter la même chose sur une seule ligne:
const timer = interval(1000);

Et enfin, une méthode qui vous permet de créer un flux d'événements pour les éléments DOM:
const observable = fromEvent(domElementRef, 'keyup');
En tant que valeurs, ce flux recevra et émettra des objets d'événement keyup.
Tuyaux et opérateurs
Pipe est une méthode de classe Observable ajoutée dans RxJS dans la version 5.5. Grâce à lui, nous pouvons construire des chaînes d'opérateurs pour le traitement séquentiel des valeurs reçues dans le flux. Le tuyau est un canal unidirectionnel qui relie les opérateurs. Les opérateurs eux-mêmes sont des fonctions normales décrites dans RxJS qui traitent les valeurs d'un flux.
Par exemple, ils peuvent convertir la valeur et la transmettre au flux, ou ils peuvent agir comme des filtres et ne sauter aucune valeur s'ils ne remplissent pas la condition spécifiée.
Regardons les opérateurs en action. Multipliez chaque valeur du flux par 2 à l'aide de l'opérateur de carte:
of(1,2,3).pipe( map(value => value * 2) ).subscribe({ next: console.log });
Voici à quoi ressemble le flux avant d'appliquer l'opérateur de carte:

Après l'instruction de la carte:

Utilisons l'opérateur de filtre. Cette instruction fonctionne exactement comme la fonction de filtre de la classe Array. La méthode prend une fonction comme premier argument, qui décrit une condition. Si la valeur du flux satisfait la condition, elle est transmise:
of(1, 2, 3).pipe(
Et voici à quoi ressemblera l'ensemble du schéma de notre flux:

Après le filtre:

Après la carte:
Remarque: pipe! == abonnez-vous. La méthode pipe déclare le comportement de flux, mais ne s'abonne pas. Jusqu'à ce que vous appeliez la méthode d'abonnement, votre flux ne commencera pas à fonctionner.
Nous rédigeons une candidature
Maintenant que nous avons compris ce que sont les tuyaux et les opérateurs, vous pouvez vous mettre à la pratique. Notre application effectuera une tâche simple: afficher une liste des référentiels github ouverts par le surnom du propriétaire entré.
Il y aura peu d'exigences:
- N'exécutez pas de demande d'API si la chaîne entrée en entrée contient moins de 3 caractères;
- Afin de ne pas répondre à la demande de chaque caractère entré par l'utilisateur, vous devez définir le délai (anti-rebond) sur 700 millisecondes avant d'accéder à l'API;
Pour rechercher des référentiels, nous utiliserons l'
API github . Je recommande d'exécuter les exemples eux-mêmes sur
stackblitz . Là, j'ai exposé l'implémentation terminée. Des liens sont fournis à la fin de l'article.
Commençons par le balisage html. Décrivons les éléments d'entrée et ul:
<input type="text"> <ul></ul>
Ensuite, dans le fichier js ou ts, nous obtenons des liens vers les éléments actuels en utilisant l'API du navigateur:
const input = document.querySelector('input'); const ul = document.querySelector('ul');
Nous avons également besoin d'une méthode qui exécutera une demande à l'API github. Vous trouverez ci-dessous le code de la fonction getUsersRepsFromAPI, qui accepte le surnom de l'utilisateur et exécute une demande ajax à l'aide de la récupération. Ensuite, il renvoie une promesse, convertissant la réponse réussie en json en cours de route:
const getUsersRepsFromAPI = (username) => { const url = `https://api.github.com/users/${ username }/repos`; return fetch(url) .then(response => { if(response.ok) { return response.json(); } throw new Error(''); }); }
Ensuite, nous écrivons une méthode qui listera les noms des référentiels:
const recordRepsToList = (reps) => { for (let i = 0; i < reps.length; i++) {
Les préparatifs sont terminés. Il est temps de jeter un œil à RxJS en action. Nous devons écouter l'événement keyup de notre entrée. Tout d'abord, nous devons comprendre que dans une approche réactive, nous travaillons avec des flux. Heureusement, RxJS propose déjà une option similaire. Rappelez-vous la méthode fromEvent que j'ai mentionnée ci-dessus. Nous l'utilisons:
const keyUp = fromEvent(input, 'keyup'); keyUp.subscribe({ next: console.log });
Maintenant, notre événement est présenté comme un flux. Si nous regardons ce qui est affiché dans la console, nous verrons un objet de type KeyboardEvent. Mais nous avons besoin d'une valeur entrée par l'utilisateur. C'est là que la méthode pipe et l'opérateur de carte sont utiles:
fromEvent(input, 'keyup').pipe( map(event => event.target.value) ).subscribe({ next: console.log });
Nous procédons à la mise en œuvre des exigences. Pour commencer, nous exécuterons la requête lorsque la valeur entrée contient plus de deux caractères. Pour ce faire, utilisez l'opérateur de filtrage:
fromEvent(input, 'keyup').pipe( map(event => event.target.value), filter(value => value.length > 2) )
Nous avons traité la première exigence. Nous passons à la seconde. Nous devons mettre en place un anti-rebond. RxJS a une instruction debounceTime. Cet opérateur comme premier argument prend le nombre de millisecondes pendant lequel la valeur sera conservée avant de passer. Dans ce cas, chaque nouvelle valeur réinitialisera la minuterie. Ainsi, à la sortie, nous obtenons la dernière valeur, après laquelle 700 millisecondes se sont écoulées.
fromEvent(input, 'keyup').pipe( debounceTime(700), map(event => event.target.value), filter(value => value.length > 2) )
Voici à quoi pourrait ressembler notre flux sans debounceTime:

Et voici à quoi ressemblera le même flux traversant cette instruction:

Avec debounceTime, nous serons moins susceptibles d'utiliser l'API, qui permettra d'économiser du trafic et de décharger le serveur.
Pour une optimisation supplémentaire, je suggère d'utiliser un autre opérateur - distinctUntilChanged. Cette méthode nous sauvera des doublons. Il est préférable de montrer son travail à l'aide d'un exemple:
from('aaabccc').pipe( distinctUntilChanged() )
Sans distinctUntilChanged:

Avec distinctUntilChanged:

Ajoutez cette instruction immédiatement après l'instruction debounceTime. Ainsi, nous n'accéderons pas à l'API si la nouvelle valeur coïncide pour une raison quelconque avec la précédente. Une situation similaire peut se produire lorsque l'utilisateur a entré de nouveaux caractères, puis les a effacés à nouveau. Puisque nous avons implémenté un délai, seule la dernière valeur tombera dans le flux, la réponse à laquelle nous avons déjà.
Allez sur le serveur
Déjà maintenant, nous pouvons décrire la logique de la demande et le traitement de la réponse. Alors que nous ne pouvons travailler qu'avec promesse. Par conséquent, nous décrivons un autre opérateur de carte qui appellera la méthode getUsersRepsFromAPI. Dans l'observateur, nous décrivons la logique de traitement de notre promesse:
fromEvent(input, 'keyup').pipe( debounceTime(700), map(event => event.target.value), filter(val => val.length > 2), distinctUntilChanged(), map(value => getUsersRepsFromAPI(value)) ).subscribe({ next: promise => promise.then(reps => recordRepsToList(reps)) });
Pour le moment, nous avons mis en œuvre tout ce que nous voulions. Mais notre exemple a un gros inconvénient: il n'y a pas de gestion d'erreur. Notre observateur ne reçoit qu'une promesse et n'a aucune idée que quelque chose pourrait mal tourner.
Bien sûr, nous pouvons accrocher la prise sur la promesse dans la méthode suivante, mais à cause de cela, notre code commencera à ressembler de plus en plus à un «enfer de rappel». Si soudain nous devons exécuter une autre requête, la complexité du code augmentera.
Remarque: l' utilisation de promesse dans le code RxJS est considérée comme anti-modèle. La promesse présente de nombreux inconvénients par rapport à l'observable. Il ne peut pas être annulé et ne peut pas être réutilisé. Si vous avez le choix, choisissez observable. Il en va de même pour la méthode toPromise de la classe Observable. Cette méthode a été implémentée pour assurer la compatibilité avec les bibliothèques qui ne peuvent pas fonctionner avec les flux.
Nous pouvons utiliser la méthode from pour projeter une promesse sur un flux, mais cette méthode est lourde d'appels supplémentaires à la méthode d'abonnement et entraînera également la croissance et la complexité du code.
Ce problème peut être résolu à l'aide de l'opérateur mergeMap:
fromEvent(input, 'keyup').pipe( debounceTime(700), map(event => event.target.value), filter(val => val.length > 2), distinctUntilChanged(), mergeMap(value => from(getUsersRepsFromAPI(value))) ).subscribe({ next: reps => recordRepsToList(reps), error: console.log })
Maintenant, nous n'avons pas besoin d'écrire la logique de traitement des promesses. La méthode from a généré un flux de promesses et l'opérateur mergeMap l'a traité. Si la promesse est remplie avec succès, la méthode suivante est appelée et notre observateur recevra l'objet fini. Si une erreur se produit, la méthode d'erreur sera appelée et notre observateur affichera une erreur dans la console.
L'opérateur mergeMap est légèrement différent des opérateurs avec lesquels nous avons travaillé plus tôt; il appartient aux soi-disant
observables d'ordre supérieur , dont je parlerai dans le prochain article. Mais, pour l'avenir, je dirai que la méthode mergeMap elle-même souscrit au flux.
Gestion des erreurs
Si notre thread reçoit une erreur, il se terminera. Et si nous essayons d'interagir avec l'application après une erreur, nous n'obtiendrons aucune réaction, car notre thread est terminé.
Ici, l'opérateur catchError nous aidera. catchError est déclenchée uniquement lorsqu'une erreur se produit dans le flux. Il vous permet de l'intercepter, de le traiter et de rendre au flux la valeur habituelle, ce qui ne conduira pas à son achèvement.
fromEvent(input, 'keyup').pipe( debounceTime(700), map(event => event.target.value), filter(val => val.length > 2), distinctUntilChanged(), mergeMap(value => from(getUsersRepsFromAPI(value))), catchError(err => of([])) ).subscribe({ next: reps => recordRepsToList(reps), error: console.log })
Nous interceptons l'erreur dans catchError et retournons à la place un flux avec un tableau vide. Maintenant, lorsqu'une erreur se produit, nous effaçons la liste des référentiels. Mais alors le flux se termine à nouveau.
Le fait est que catchError remplace notre flux d'origine par un nouveau. Et puis notre observateur n'écoute que lui. Lorsque le flux émet un tableau vide, la méthode complète est appelée.
Afin de ne pas remplacer notre thread d'origine, nous appelons l'opérateur catchError sur le thread from depuis l'intérieur de l'opérateur mergeMap.
fromEvent(input, 'keyup').pipe( debounceTime(700), map(event => event.target.value), filter(val => val.length > 2), distinctUntilChanged(), mergeMap(value => { return from(getUsersRepsFromAPI(value)).pipe( catchError(err => of([])) ) }) ).subscribe({ next: reps => recordRepsToList(reps), error: console.log })
Ainsi, notre flux d'origine ne remarquera rien. Au lieu d'une erreur, il obtiendra un tableau vide.
Conclusion
Nous avons finalement commencé à pratiquer et avons vu à quoi servent les tuyaux et les opérateurs. Nous avons examiné comment réduire le code en utilisant la riche API que RxJS nous fournit. Bien sûr, notre application n'est pas terminée, dans la partie suivante, nous analyserons comment il est possible d'en traiter une autre dans un thread et comment annuler notre demande http afin d'économiser encore plus de trafic et de ressources de notre application. Et pour que vous puissiez voir la différence, j'ai présenté un exemple sans utiliser RxJS, vous pouvez le voir
ici . Sur
ce lien, vous trouverez le code complet de l'application actuelle. Pour générer les circuits, j'ai utilisé le
visualiseur RxJS .
J'espère que cet article vous a aidé à mieux comprendre comment fonctionne RxJS. Je vous souhaite du succès dans votre étude!