Comment créer un jeu d'association de cartes en utilisant Angular et RxJS
Aujourd'hui, je voudrais me concentrer sur les flux de données résultant des événements de clic sur l'interface utilisateur. Le traitement de ces flux de clics est particulièrement utile pour les applications avec une interaction utilisateur intensive où de nombreux événements doivent être traités. Je voudrais également vous présenter un peu plus RxJS; c'est une bibliothèque JavaScript qui peut être utilisée pour exprimer les routines de gestion des événements de manière compacte et concise dans un style réactif.
Que construisons-nous?
Les jeux d'apprentissage et les questionnaires de connaissances sont populaires à la fois pour les utilisateurs plus jeunes et plus âgés. Un exemple est le jeu de «jumelage de paires», où l'utilisateur doit trouver des paires apparentées dans un mélange d'images et / ou d'extraits de texte.
L'animation ci-dessous montre une version simple du jeu: l'utilisateur sélectionne deux éléments sur le les côtés gauche et droit du terrain de jeu l'un après l'autre et dans n'importe quel ordre. Les paires correctement appariées sont déplacées vers une zone distincte du terrain de jeu, tandis que les affectations incorrectes sont immédiatement dissoutes afin que l'utilisateur doive faire une nouvelle sélection.
Dans ce tutoriel, nous allons construire un tel jeu d'apprentissage étape par étape. Dans la première partie nous allons construire un composant angulaire qui ne fait que montrer le terrain de jeu. Notre objectif est que le composant puisse être configuré pour différents cas d'utilisation et groupes cibles – d'un quiz animal à un entraîneur de vocabulaire dans une application d'apprentissage des langues. À cette fin, Angular propose le concept de projection de contenu avec des modèles personnalisables, que nous utiliserons. Pour illustrer le principe, je vais construire deux versions du jeu ("game1" et "game2") avec des dispositions différentes.
Dans la deuxième partie du tutoriel, nous nous concentrerons sur la programmation réactive. Chaque fois qu'une paire est appariée, l'utilisateur doit obtenir une sorte de rétroaction de l'application; c'est cette gestion d'événements qui est réalisée à l'aide de la bibliothèque RxJS.
1. Création d'un composant angulaire pour le jeu d'apprentissage
Comment créer le cadre de base
Commençons par créer un nouveau projet nommé «learning-app». Avec la CLI angulaire, vous pouvez le faire avec la commande ng new learning-app
. Dans le fichier app.component.html je remplace le code source pré-généré comme suit:
L'apprentissage est amusant!
Dans l'étape suivante, le composant du jeu d'apprentissage est créé. Je l'ai appelé "match de jeu" et j'ai utilisé la commande ng générer un jeu de correspondance de composants
. Cela créera un sous-dossier séparé pour le composant de jeu avec les fichiers HTML, CSS et Typescript requis.
Comme déjà mentionné, le jeu éducatif doit être configurable à différentes fins. Pour le démontrer, je crée deux composants supplémentaires ( game1
et game2
) en utilisant la même commande. J'ajoute le composant de jeu en tant que composant enfant en remplaçant le code pré-généré dans le fichier game1.component.html ou game2.component.html par la balise suivante:
Au début, je n'utilise que le composant game1
. Afin de m'assurer que le jeu 1 s'affiche immédiatement après le démarrage de l'application, j'ajoute cette balise au fichier app.component.html :
Lors du démarrage de l'application avec ng serve - ouvert
le navigateur affichera le message "Matching Game Works". (Il s'agit actuellement du seul contenu de matching-game.component.html .)
Maintenant, nous devons tester les données. Dans le dossier / app
je crée un fichier nommé pair.ts où je définis la classe Pair
:
export class Pair Pair {
partie gauche: chaîne;
partie droite: chaîne;
id: nombre;
}
Un objet paire comprend deux textes apparentés ( partie gauche
et partie droite
) et un ID.
Le premier jeu est censé être un quiz sur les espèces dans lequel les espèces (par exemple chien
) doivent être affectés à la classe animale appropriée (c.-à-d. mammifère
).
Dans le fichier animaux.ts je définis un tableau avec des données de test :
importez {Pair} de './pair';
export const ANIMALS: Pair [] = [
{ id: 1, leftpart: 'dog', rightpart: 'mammal'},
{ id: 2, leftpart: 'blickbird', rightpart: 'bird'},
{ id: 3, leftpart: 'spider', rightpart: 'insect'},
{ id: 4, leftpart: 'turtle', rightpart: 'reptile' },
{ id: 5, leftpart: 'guppy', rightpart: 'fish'},
];
Le composant game1
a besoin d'accéder à nos données de test. Ils sont stockés dans la propriété animaux
. Le fichier game1.component.ts a maintenant le contenu suivant:
import {Component, OnInit} de '@ angular / core';
importez {ANIMALS} depuis '../animals';
@Composant({
sélecteur: 'app-game1',
templateUrl: './game1.component.html',
styleUrls: ['./game1.component.css']
})
la classe d'exportation Game1Component implémente OnInit {
animaux = ANIMAUX;
constructeur () {}
ngOnInit () {
}
}
La première version du composant de jeu
Notre prochain objectif: le composant de jeu match-game
doit accepter les données de jeu du composant parent (par exemple game1
) comme contribution. L'entrée est un tableau d'objets «paire». L'interface utilisateur du jeu doit être initialisée avec les objets transmis lors du démarrage de l'application.
Pour cela, nous devons procéder comme suit:
- Ajouter le La propriété
associe
au composant de jeu en utilisant le décorateur@Input
. - Ajoutez les tableaux
solvedPairs
etunsolvedPairs
comme propriétés privées supplémentaires de le composant. (Il est nécessaire de faire la distinction entre les paires déjà «résolues» et «pas encore résolues».) - Au démarrage de l'application (voir fonction
ngOnInit
) toutes les paires sont toujours «non résolues» et sont donc déplacées au tableauunsolvedPairs
.
import {Component, OnInit, Input} de '@ angular / core';
importer {Pair} à partir de '../pair';
@Composant({
sélecteur: 'app-matching-game',
templateUrl: './matching-game.component.html',
styleUrls: ['./matching-game.component.css']
})
la classe d'exportation MatchingGameComponent implémente OnInit {
@Input () pairs: Pair [];
private solvedPairs: Pair [] = [];
private unsolvedPairs: Pair [] = [];
constructeur () {}
ngOnInit () {
pour (soit i = 0; i <this.pairs.length; i ++) {
this.unsolvedPairs.push (this.pairs [i]);
}
}
}
De plus, je définis le modèle HTML du composant match-game
. Il existe des conteneurs pour les paires non résolues et résolues. La directive ngIf
garantit que le conteneur respectif ne s'affiche que s'il existe au moins une paire non résolue ou résolue.
Dans le conteneur des paires non résolues (classe conteneur non résolu
), d'abord tous les composants gauche
(voir le cadre gauche dans le GIF ci-dessus), puis tous les composants droit
(voir le cadre droit dans le GIF) des paires sont répertoriés. (J'utilise la directive ngFor
pour répertorier les paires.) Pour le moment, un simple bouton suffit comme modèle.
Avec l'expression de modèle {{{pair.leftpart}}
et { {{pair.rightpart}}}
les valeurs des propriétés partie gauche
et partie droite
des objets paire individuels sont interrogés lors de l'itération de la paire
tableau. Ils sont utilisés comme étiquettes pour les boutons générés.
Les paires affectées sont répertoriées dans le deuxième conteneur (classe conteneur résolu
). Une barre verte (connecteur de classe
) indique qu'ils appartiennent ensemble.
Le code CSS correspondant du fichier matching-game.component.css se trouve dans le code source à l'adresse le début de l'article.
0 ">
0 ">
Dans le composant game1
le tableau animaux
est maintenant lié au ] pairs
propriété du composant match-game
(liaison de données unidirectionnelle).
Le résultat est illustré dans l'image ci-dessous.
Évidemment, notre jeu d'association n'est pas encore trop difficile, car les parties gauche et droite des paires sont directement opposées. Pour que l'appariement ne soit pas trop trivial, les bonnes pièces doivent être mélangées. Je résous le problème avec un tuyau auto-défini shuffle
que j'applique au tableau unsolvedPairs
sur le côté droit (le paramètre test
est nécessaire plus tard pour forcer la pipe à mettre à jour):
...
...
Le code source de la pipe est stocké dans le fichier shuffle.pipe.ts dans le dossier de l'application (voir le code source au début de l'article). Notez également le fichier app.module.ts où le tuyau doit être importé et répertorié dans les déclarations du module. La vue souhaitée apparaît maintenant dans le navigateur.
Version étendue: utilisation de modèles personnalisables pour autoriser une conception individuelle du jeu
Au lieu d'un bouton, il devrait être possible de spécifier des extraits de modèle arbitraires pour personnaliser le jeu. Dans le fichier matching-game.component.html je remplace le modèle de bouton pour les côtés gauche et droit du jeu par une balise ng-template
. J'attribue ensuite le nom d'une référence de modèle à la propriété ngTemplateOutlet
. Cela me donne deux espaces réservés, qui sont remplacés par le contenu de la référence de modèle respective lors du rendu de la vue.
Nous traitons ici du concept de projection de contenu : certaines parties du modèle de composant sont données de l'extérieur et sont «projetés» dans le modèle aux emplacements marqués.
Lors de la génération de la vue, Angular doit insérer les données du jeu dans le modèle. Avec le paramètre ngTemplateOutletContext
je dis à Angular qu'une variable contextPair
est utilisée dans le modèle, à laquelle il faut attribuer la valeur actuelle de la variable pair
à partir de la variable ] Directive ngFor
.
La liste suivante montre le remplacement du conteneur non résolu
. Dans le conteneur résolu
les boutons doivent également être remplacés par les balises ng-template
.
0 ">
...
Dans le fichier matching-game.component.ts les variables des deux références de modèle ( leftpart_temp
et rightpart_temp
) doivent être déclarées. Le décorateur @ContentChild
indique qu'il s'agit d'une projection de contenu, c.-à-d. Angular s'attend maintenant à ce que les deux extraits de modèle avec le sélecteur respectif ( partie gauche
ou partie droite
) soient fournis. dans le composant parent entre les balises
de l'élément hôte (voir @ViewChild
).
@ContentChild ('leftpart', {static: false}) leftpart_temp: TemplateRef ;
@ContentChild ('rightpart', {statique: faux}) rightpart_temp: TemplateRef ;
N'oubliez pas: les types ContentChild
et TemplateRef
doivent être importés du package de base.
Dans le composant parent game1
les deux les extraits de modèle requis avec les sélecteurs partie gauche
et partie droite
sont maintenant insérés.
Par souci de simplicité, je vais réutiliser les boutons ici à nouveau:
L'attribut let-animalPair = "contextPair"
est utilisé pour spécifier que la variable de contexte contextPair
est utilisée dans l'extrait de modèle avec le nom animalPair
.
Les extraits de modèle peuvent maintenant être modifiés à votre goût. Pour le démontrer, j'utilise le composant game2
. Le fichier game2.component.ts obtient le même contenu que game1.component.ts . Dans game2.component.html j'utilise un élément div
conçu individuellement au lieu d'un bouton. Les classes CSS sont stockées dans le fichier game2.component.css .
{{animalPair.leftpart}}
{{animalPair.rightpart}}
Après avoir ajouté les balises
sur la page d'accueil app.component.html la deuxième version du jeu apparaît lorsque je lance l'application:
Les possibilités de conception sont maintenant presque illimitées. Il serait possible, par exemple, de définir une sous-classe de Pair
qui contient des propriétés supplémentaires. Par exemple, les adresses d'images peuvent être stockées pour les parties gauche et / ou droite. Les images peuvent être affichées dans le modèle avec le texte ou à la place du texte.
2. Contrôle de l'interaction utilisateur avec RxJS
Avantages d'une programmation réactive avec RxJS
Pour transformer l'application en un jeu interactif, les événements (par exemple les événements de clic de souris) qui sont déclenchés sur l'interface utilisateur doivent être traités. Dans la programmation réactive, des séquences continues d'événements, appelés «flux», sont prises en compte. Un flux peut être observé (c'est un «observable»), c'est-à-dire qu'il peut y avoir un ou plusieurs «observateurs» ou «abonnés» abonnés au flux. Ils sont informés (généralement de manière asynchrone) de chaque nouvelle valeur dans le flux et peuvent y réagir d'une certaine manière.
Avec cette approche, un faible niveau de couplage entre les parties d'une application peut être atteint. Les observateurs et observables existants sont indépendants les uns des autres et leur couplage peut être modifié au moment de l'exécution.
La bibliothèque JavaScript RxJS fournit une implémentation mature du modèle de conception Observer. De plus, RxJS contient de nombreux opérateurs pour convertir les flux (par exemple filtre, carte) ou pour les combiner en de nouveaux flux (par exemple fusionner, concat). Les opérateurs sont des «fonctions pures» au sens de la programmation fonctionnelle: ils ne produisent pas d'effets secondaires et sont indépendants de l'état extérieur à la fonction. Une logique de programme composée uniquement d'appels à des fonctions pures n'a pas besoin de variables auxiliaires globales ou locales pour stocker des états intermédiaires. Ceci, à son tour, favorise la création de blocs de code sans état et à couplage lâche. Il est donc souhaitable de réaliser une grande partie de la gestion des événements par une combinaison intelligente d'opérateurs de flux. Des exemples de cela sont donnés dans la section suivante, basée sur notre jeu de correspondance.
Intégration de RxJS dans la gestion des événements d'un composant angulaire
Le framework Angular fonctionne avec les classes de la bibliothèque RxJS.
L'image ci-dessous montre les principales classes et fonctions qui jouent un rôle dans nos considérations:
Nom de classe | Fonction | |
---|---|---|
Observable (RxJS) | Classe de base qui représente un flux; en d'autres termes, une séquence continue de données. Un observable peut être souscrit. La fonction pipe est utilisée pour appliquer une ou plusieurs fonctions d'opérateur à l'instance observable. | |
Subject (RxJS) | La sous-classe d'observable fournit la fonction suivante pour publier de nouvelles données dans le flux. [19659095] EventEmitter (Angular) | Il s'agit d'une sous-classe angulaire spécifique qui n'est généralement utilisée qu'en conjonction avec le décorateur @Output pour définir une sortie de composant. Comme la fonction suivante, la fonction emit est utilisée pour envoyer des données aux abonnés. |
Subscription (RxJS) | La fonction subscribe d'une observable renvoie une instance d'abonnement. Il est nécessaire d'annuler l'abonnement après avoir utilisé le composant. |
Avec l'aide de ces classes, nous voulons implémenter l'interaction utilisateur dans notre jeu. La première étape consiste à s'assurer qu'un élément sélectionné par l'utilisateur à gauche ou à droite est mis en évidence visuellement.
La représentation visuelle des éléments est contrôlée par les deux extraits de modèle dans le composant parent. La décision quant à leur affichage dans l'état sélectionné doit donc également être laissée au composant parent. Il doit recevoir les signaux appropriés dès qu'une sélection est effectuée sur le côté gauche ou droit ou dès qu'une sélection doit être annulée.
Pour cela, je définis quatre valeurs de sortie de type EventEmitter
dans le fichier matching-game.component.ts . Les types Output
et EventEmitter
doivent être importés du package de base.
@Output () leftpartSelected = new EventEmitter ();
@Output () rightpartSelected = new EventEmitter ();
@Output () leftpartUnselected = new EventEmitter ();
@Output () rightpartUnselected = new EventEmitter ();
Dans le modèle matching-game.component.html je réagis à l'événement mousedown
à gauche et à droite, puis envoie l'ID de l'élément sélectionné à tous récepteurs.
...
Dans notre cas, les récepteurs sont les composants game1
et game2
. Vous pouvez maintenant définir la gestion des événements pour les événements leftpartSelected
rightpartSelected
leftpartUnselected
et rightpartUnselected
. La variable $ event
représente la valeur de sortie émise, dans notre cas l'ID. Ci-dessous, vous pouvez voir la liste pour game1.component.html pour game2.component.html les mêmes changements s'appliquent.
Dans game1.component.ts (et de même dans game2.component.ts ), les fonctions de gestionnaire d'événement
sont désormais implémentées. Je stocke les ID des éléments sélectionnés. Dans le modèle HTML (voir ci-dessus), ces éléments se voient attribuer la classe sélectionnée
. Le fichier CSS game1.component.css définit les changements visuels que cette classe entraînera (par exemple les changements de couleur ou de police). La réinitialisation de la sélection (désélectionner) est basée sur l'hypothèse que les objets paire ont toujours des ID positifs.
onLeftpartSelected (id: number): void {
this.leftpartSelectedId = id;
}
onRightpartSelected (id: number): void {
this.rightpartSelectedId = id;
}
onLeftpartUnselected (): void {
this.leftpartSelectedId = -1;
}
onRightpartUnselected (): void {
this.rightpartSelectedId = -1;
}
Dans l'étape suivante, la gestion des événements est requise dans le composant de jeu correspondant. Il faut déterminer si une affectation est correcte, c'est-à-dire si l'élément sélectionné à gauche correspond à l'élément sélectionné à droite. Dans ce cas, la paire affectée peut être déplacée dans le conteneur pour les paires résolues.
Je voudrais formuler la logique d'évaluation en utilisant les opérateurs RxJS (voir la section suivante). Pour la préparation, je crée un sujet assignStream
dans matching-game.component.ts . Il doit émettre les éléments sélectionnés par l'utilisateur sur le côté gauche ou droit. Le but est d'utiliser des opérateurs RxJS pour modifier et diviser le flux de manière à obtenir deux nouveaux flux: un flux solvedStream
qui fournit les paires correctement attribuées et un second flux failedStream
qui fournit les mauvaises affectations. Je souhaite m'abonner à ces deux flux avec subscribe
afin de pouvoir effectuer un traitement d'événement approprié dans chaque cas.
J'ai également besoin d'une référence aux objets d'abonnement créés, afin de pouvoir annuler les abonnements avec «unsubscribe» à la sortie du jeu (voir ngOnDestroy
). Les classes Subject
et Subscription
doivent être importées du package «rxjs».
private cessionStream = new Subject ();
private solvedStream = new Observable ();
private failedStream = new Observable ();
s_Subscription privé: abonnement;
f_Subscription privé: abonnement;
ngOnInit () {
...
// TODO: appliquez les opérateurs de flux sur
// leftpartClicked und rightpartClicked
this.s_Subscription = this.solvedStream.subscribe (paire =>
handleSolvedAssignment (paire));
this.f_Subscription = this.failedStream.subscribe (() =>
handleFailedAssignment ());
}
ngOnDestroy () {
this.s_Subscription.unsubscribe ();
this.f_Subscription.unsubscribe ();
}
Si l'affectation est correcte, les étapes suivantes sont effectuées:
- La paire affectée est déplacée vers le conteneur pour les paires résolues.
- Les événements
leftpartUnselected
et rightpartUnselected
] sont envoyés au composant parent.
Aucune paire n'est déplacée si l'affectation est incorrecte. Si la mauvaise affectation a été exécutée de gauche à droite ( side1
a la valeur left
), la sélection doit être annulée pour l'élément du côté gauche (voir le GIF au début de l'article). Si une affectation est effectuée de droite à gauche, la sélection est annulée pour l'élément du côté droit. Cela signifie que le dernier élément sur lequel vous avez cliqué reste dans un état sélectionné.
Dans les deux cas, je prépare les fonctions de gestionnaire correspondantes handleSolvedAssignment
et handleFailedAssignment
(supprimer la fonction: voir la source code à la fin de cet article):
private handleSolvedAssignment (paire: Pair): void {
this.solvedPairs.push (paire);
this.remove (this.unsolvedPairs, pair);
this.leftpartUnselected.emit ();
this.rightpartUnselected.emit ();
// solution de contournement pour forcer la mise à jour du shuffle pipe
this.test = Math.random () * 10;
}
private handleFailedAssignment (side1: string): void {
if (side1 == "left") {
this.leftpartUnselected.emit ();
}autre{
this.rightpartUnselected.emit ();
}
}
Maintenant, nous devons changer le point de vue du consommateur qui souscrit aux données au producteur qui génère les données. Dans le fichier matching-game.component.html je m'assure qu'en cliquant sur un élément, l'objet paire associé est poussé dans le flux assignStream
. Il est logique d'utiliser un flux commun pour les côtés gauche et droit car l'ordre de l'affectation n'est pas important pour nous.
...
Conception de l'interaction du jeu avec les opérateurs RxJS
Il ne reste plus qu'à convertir le flux assignStream
en flux solvedStream
et failedStream
. J'applique les opérateurs suivants dans l'ordre:
par paire
Il y a toujours deux paires dans une affectation. L'opérateur par paire
sélectionne les données par paires dans le flux. La valeur actuelle et la valeur précédente sont combinées en une paire.
Du flux suivant…
„{pair1, left}, {pair3, right}, {pair2, left}, {pair2, right}, { pair1, gauche}, {pair1, droite} "
… donne ce nouveau flux:
„({pair1, gauche}, {pair3, droite}), ({pair3, droite}, {pair2, gauche}), ({pair2, gauche}, {pair2 , droite}), ({pair2, droite}, {pair1, gauche}), ({pair1, gauche}, {pair1, droite}) "
Par exemple, nous obtenons la combinaison ({pair1, gauche}, {pair3, droite})
lorsque l'utilisateur sélectionne chien
(id = 1) sur le côté gauche et insecte
(id = 3) sur le côté droit (voir tableau ANIMALS
au début de l'article). Ces combinaisons et les autres résultent de la séquence de jeu montrée dans le GIF ci-dessus.
filtre
Vous devez supprimer toutes les combinaisons du flux qui ont été faites du même côté du terrain de jeu comme ({pair1, left}, {pair1, left})
ou ({pair1, left}, {pair4, left})
.
La condition de filtre pour une combinaison peigne
est donc peigne [0] .side! = Comb [1] .side
.
partition
Cet opérateur prend un flux et une condition et crée deux flux à partir de celui-ci . Le premier flux contient les données qui remplissent la condition et le second flux contient les données restantes. Dans notre cas, les flux doivent contenir des affectations correctes ou incorrectes. La condition pour une combinaison peigne
est donc peigne [0] .pair === peigne [1] .pair
.
L'exemple donne un flux «correct» avec [19659129] ({pair2, gauche}, {pair2, droite}), ({pair1, gauche}, {pair1, droite})
et un flux "incorrect" avec
({pair1, left}, {pair3, right}), ({pair3, right}, {pair2, left}), ({pair2, right}, {pair1 , la gauche})
map
Seul l'objet paire individuel est requis pour le traitement ultérieur d'une affectation correcte, comme pair2
. L'opérateur de carte peut être utilisé pour exprimer que la combinaison comb
doit être mappée sur comb [0] .pair
. Si l'affectation est incorrecte, la combinaison comb
est mappée à la chaîne comb [0] .side
car la sélection doit être réinitialisée du côté spécifié par side
La fonction pipe
est utilisée pour concaténer les opérateurs ci-dessus. Les opérateurs par paire
filtre
partition
carte
doivent être importés du package rxjs / operators
. [19659145] ngOnInit () {
…
const stream = this.assignmentStream.pipe (
par paire (),
filtre (comb => comb [0] .side! = comb [1] .side)
);
// la notation pipe conduit à un message d'erreur (Angular 8.2.2, RxJS 6.4.0)
const [stream1, stream2] = partition (comb =>
peigne [0] .pair === peigne [1] .pair) (flux);
this.solvedStream = stream1.pipe (
map (comb => comb [0] .pair)
);
this.failedStream = stream2.pipe (
map (comb => comb [0] .side)
);
this.s_Subscription = this.solvedStream.subscribe (paire =>
this.handleSolvedAssignment (paire));
this.f_Subscription = this.failedStream.subscribe (côté =>
this.handleFailedAssignment (côté));
}
Maintenant, le jeu fonctionne déjà!
En utilisant les opérateurs, la logique du jeu pourrait être décrite de manière déclarative. Nous avons seulement décrit les propriétés de nos deux flux cibles (combinés en paires, filtrés, partitionnés, remappés) et n'avons pas eu à nous soucier de la mise en œuvre de ces opérations. Si nous les avions implémentés nous-mêmes, nous aurions également dû stocker des états intermédiaires dans le composant (par exemple, les références aux derniers éléments cliqués à gauche et à droite). Au lieu de cela, les opérateurs RxJS encapsulent la logique d'implémentation et les états requis pour nous et élèvent ainsi la programmation à un niveau d'abstraction plus élevé.
Conclusion
En utilisant un jeu d'apprentissage simple comme exemple, nous avons testé l'utilisation de RxJS dans une composante angulaire. L'approche réactive est bien adaptée pour traiter les événements qui se produisent sur l'interface utilisateur. Avec RxJS, les données nécessaires à la gestion des événements peuvent être facilement organisées sous forme de flux. De nombreux opérateurs, tels que filtre
carte
ou partition
sont disponibles pour transformer les flux. Les flux résultants contiennent des données préparées dans leur forme finale et auxquelles vous pouvez vous abonner directement. Il faut un peu de compétence et d'expérience pour sélectionner les opérateurs appropriés pour le cas respectif et pour les relier efficacement. Cet article devrait en fournir une introduction.
Autres ressources
Lecture connexe sur SmashingMag:
(ra, il)
Source link