Cum să creezi un joc de potrivire a cărților folosind Angular și RxJS
Publicat: 2022-03-10Astăzi, aș dori să mă concentrez pe fluxurile de date rezultate din evenimentele de clic pe interfața cu utilizatorul. Procesarea unor astfel de fluxuri de clic este deosebit de utilă pentru aplicațiile cu o interacțiune intensă a utilizatorului în care multe evenimente trebuie procesate. De asemenea, aș dori să vă prezint puțin mai mult RxJS; este o bibliotecă JavaScript care poate fi folosită pentru a exprima rutinele de gestionare a evenimentelor în mod compact și concis într-un stil reactiv.
Ce Construim?
Jocurile de învățare și chestionarele de cunoștințe sunt populare atât pentru utilizatorii mai tineri, cât și pentru cei mai în vârstă. Un exemplu este jocul „potrivirea perechilor”, în care utilizatorul trebuie să găsească perechi înrudite într-un amestec de imagini și/sau fragmente de text.
Animația de mai jos arată o versiune simplă a jocului: utilizatorul selectează două elemente din stânga și din dreapta terenului de joc unul după altul și în orice ordine. Perechile potrivite corect sunt mutate într-o zonă separată a terenului de joc, în timp ce orice atribuire greșită este imediat dizolvată, astfel încât utilizatorul trebuie să facă o nouă selecție.
În acest tutorial, vom construi un astfel de joc de învățare pas cu pas. În prima parte, vom construi o componentă Angular care arată doar câmpul de joc al jocului. Scopul nostru este ca componenta să poată fi configurată pentru diferite cazuri de utilizare și grupuri țintă — de la un test cu animale până la un antrenor de vocabular într-o aplicație de învățare a limbilor străine. În acest scop, Angular oferă conceptul de proiecție de conținut cu șabloane personalizabile, de care vom folosi. Pentru a ilustra principiul, voi construi două versiuni ale jocului („game1” și „game2”) cu aspecte diferite.
În a doua parte a tutorialului, ne vom concentra pe programarea reactivă. Ori de câte ori se potrivește o pereche, utilizatorul trebuie să obțină un fel de feedback de la aplicație; această gestionare a evenimentelor este realizată cu ajutorul bibliotecii RxJS.
- Cerințe
Pentru a urma acest tutorial, trebuie instalat Angular CLI. - Cod sursa
Codul sursă al acestui tutorial poate fi găsit aici (14KB).
1. Construirea unei componente unghiulare pentru jocul de învățare
Cum se creează cadrul de bază
Mai întâi, să creăm un nou proiect numit „learning-app”. Cu Angular CLI, puteți face acest lucru cu comanda ng new learning-app
. În fișierul app.component.html , înlocuiesc codul sursă pregenerat după cum urmează:
<div> <h1>Learning is fun!</h1> </div>
În pasul următor, este creată componenta pentru jocul de învățare. L-am numit „match-game” și am folosit comanda ng generate component matching-game
. Aceasta va crea un subdosar separat pentru componenta jocului cu fișierele HTML, CSS și Typescript necesare.
După cum am menționat deja, jocul educațional trebuie să fie configurabil în diferite scopuri. Pentru a demonstra acest lucru, creez două componente suplimentare ( game1
și game2
) utilizând aceeași comandă. Adaug componenta de joc ca componentă secundară prin înlocuirea codului pre-generat din fișierul game1.component.html sau game2.component.html cu următoarea etichetă:
<app-matching-game></app-matching-game>
La început, folosesc doar game1
component1 . Pentru a mă asigura că jocul 1 este afișat imediat după pornirea aplicației, adaug această etichetă în fișierul app.component.html :
<app-game1></app-game1>
La pornirea aplicației cu ng serve --open
, browserul va afișa mesajul „match-game works”. (Acesta este în prezent singurul conținut al matching-game.component.html .)
Acum, trebuie să testăm datele. În folderul /app
, creez un fișier numit pair.ts unde definesc clasa Pair
:
export class Pair { leftpart: string; rightpart: string; id: number; }
Un obiect pereche cuprinde două texte înrudite ( leftpart
și rightpart
) și un ID.
Primul joc ar trebui să fie un test de specii în care speciile (de ex. dog
) trebuie să fie atribuite clasei de animale corespunzătoare (adică mammal
).
În fișierul animals.ts , definesc o matrice cu date de testare:
import { Pair } from './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'}, ];
Componenta game1
are nevoie de acces la datele noastre de testare. Acestea sunt depozitate în proprietatea animals
. Fișierul game1.component.ts are acum următorul conținut:
import { Component, OnInit } from '@angular/core'; import { ANIMALS } from '../animals'; @Component({ selector: 'app-game1', templateUrl: './game1.component.html', styleUrls: ['./game1.component.css'] }) export class Game1Component implements OnInit { animals = ANIMALS; constructor() { } ngOnInit() { } }
Prima versiune a componentei de joc
Următorul nostru obiectiv: jocul matching-game
trebuie să accepte datele jocului de la componenta părinte (de exemplu, game1
) ca intrare. Intrarea este o matrice de obiecte „pereche”. Interfața de utilizator a jocului ar trebui să fie inițializată cu obiectele trecute la pornirea aplicației.
În acest scop, trebuie să procedăm după cum urmează:
- Adăugați
pairs
de proprietăți la componenta jocului folosind decoratorul@Input
. - Adăugați matricele
solvedPairs
șiunsolvedPairs
ca proprietăți private suplimentare ale componentei. (Este necesar să se facă distincția între perechile deja „rezolvate” și „încă nerezolvate”.) - Când aplicația este pornită (vezi funcția
ngOnInit
) toate perechile sunt încă „nerezolvate” și, prin urmare, sunt mutate în matriceaunsolvedPairs
.
import { Component, OnInit, Input } from '@angular/core'; import { Pair } from '../pair'; @Component({ selector: 'app-matching-game', templateUrl: './matching-game.component.html', styleUrls: ['./matching-game.component.css'] }) export class MatchingGameComponent implements OnInit { @Input() pairs: Pair[]; private solvedPairs: Pair[] = []; private unsolvedPairs: Pair[] = []; constructor() { } ngOnInit() { for(let i=0; i<this.pairs.length; i++){ this.unsolvedPairs.push(this.pairs[i]); } } }
Mai mult, definesc șablonul HTML al componentei matching-game
. Există containere pentru perechile nerezolvate și rezolvate. Directiva ngIf
asigură că respectivul container este afișat numai dacă există cel puțin o pereche nerezolvată sau rezolvată.
În containerul pentru perechile nerezolvate ( container unsolved
), mai întâi sunt listate componentele perechilor toate left
(vezi cadrul din stânga din GIF de mai sus) și apoi din right
(vezi cadrul din dreapta din GIF). (Folosesc directiva ngFor
pentru a enumera perechile.) În acest moment, un simplu buton este suficient ca șablon.
Cu expresia șablon {{{pair.leftpart}}
și { {{pair.rightpart}}}
, valorile proprietăților leftpart
și rightpart
ale obiectelor perechi individuale sunt interogate la iterarea matricei pair
. Sunt folosite ca etichete pentru butoanele generate.
Perechile alocate sunt listate în al doilea container ( container solved
). O bară verde ( connector
de clasă) indică faptul că aparțin împreună.
Codul CSS corespunzător al fișierului matching-game.component.css poate fi găsit în codul sursă de la începutul articolului.
<div> <div class="container unsolved" *ngIf="unsolvedPairs.length>0"> <div class="pair_items left"> <button *ngFor="let pair of unsolvedPairs" class="item"> {{pair.leftpart}} </button> </div> <div class="pair_items right"> <button *ngFor="let pair of unsolvedPairs" class="item"> {{pair.rightpart}} </button> </div> </div> <div class="container solved" *ngIf="solvedPairs.length>0"> <div *ngFor="let pair of solvedPairs" class="pair"> <button>{{pair.leftpart}}</button> <div class="connector"></div> <button>{{pair.rightpart}}</button> </div> </div> </div>
În jocul component1, game1
de animals
este acum legată de proprietatea de pairs
a matching-game
a componentei (legare de date unidirecțională).
<app-matching-game [pairs]="animals"></app-matching-game>
Rezultatul este prezentat în imaginea de mai jos.
Evident, jocul nostru de potrivire nu este încă prea dificil, deoarece părțile din stânga și dreapta ale perechilor sunt direct opuse. Pentru ca împerecherea să nu fie prea banală, părțile potrivite trebuie amestecate. Rezolv problema cu un pipe shuffle
auto-definit, pe care îl aplic matricei unsolvedPairs
din partea dreaptă ( test
parametrilor este necesar mai târziu pentru a forța actualizarea conductei):
... <div class="pair_items right"> <button *ngFor="let pair of unsolvedPairs | shuffle:test" class="item"> {{pair.rightpart}} </button> </div> ...
Codul sursă al pipei este stocat în fișierul shuffle.pipe.ts din folderul aplicației (vezi codul sursă la începutul articolului). De asemenea, rețineți fișierul app.module.ts , unde conducta trebuie importată și listată în declarațiile modulului. Acum, vizualizarea dorită apare în browser.
Versiune extinsă: Folosind șabloane personalizabile pentru a permite un design individual al jocului
În loc de un buton, ar trebui să fie posibil să se specifice fragmente de șablon arbitrare pentru a personaliza jocul. În fișierul matching-game.component.html înlocuiesc șablonul de buton pentru partea stângă și dreaptă a jocului cu o etichetă ng-template
. Apoi atribui numele unei referințe de șablon proprietății ngTemplateOutlet
. Acest lucru îmi oferă doi substituenți, care sunt înlocuiți cu conținutul referinței șablonului respectiv atunci când redați vizualizarea.
Aici avem de-a face cu conceptul de proiecție de conținut : anumite părți ale șablonului componente sunt date din exterior și sunt „proiectate” în șablon în pozițiile marcate.
La generarea vizualizării, Angular trebuie să insereze datele jocului în șablon. Cu parametrul ngTemplateOutletContext
îi spun lui Angular că în șablon este utilizată o variabilă contextPair
, căreia ar trebui să i se atribuie valoarea curentă a variabilei pair
din directiva ngFor
.
Următoarea listă arată înlocuirea containerului unsolved
. În containerul solved
, butoanele trebuie înlocuite și cu etichetele ng-template
.
<div class="container unsolved" *ngIf="unsolvedPairs.length>0"> <div class="pair_items left"> <div *ngFor="let pair of unsolvedPairs" class="item"> <ng-template [ngTemplateOutlet]="leftpart_temp" [ngTemplateOutletContext]="{contextPair: pair}"> </ng-template> </div> </div> <div class="pair_items right"> <div *ngFor="let pair of unsolvedPairs | shuffle:test" class="item"> <ng-template [ngTemplateOutlet]="leftpart_temp" [ngTemplateOutletContext]="{contextPair: pair}"> </ng-template> </div> </div> </div> ...
În fișierul matching-game.component.ts , variabilele ambelor referințe de șablon ( leftpart_temp
și rightpart_temp
) trebuie declarate. Decoratorul @ContentChild
indică faptul că aceasta este o proiecție de conținut, adică Angular se așteaptă acum ca cele două fragmente de șablon cu selectorul respectiv ( leftpart
sau rightpart
) să fie furnizate în componenta părinte între etichetele <app-matching-game></app-matching-game>
al elementului gazdă (vezi @ViewChild
).
@ContentChild('leftpart', {static: false}) leftpart_temp: TemplateRef<any>; @ContentChild('rightpart', {static: false}) rightpart_temp: TemplateRef<any>;
Nu uitați: tipurile ContentChild
și TemplateRef
trebuie importate din pachetul de bază.
În jocul de componentă game1
, cele două fragmente de șablon necesare cu selectoarele partea leftpart
și rightpart
sunt acum inserate.
De dragul simplității, voi reutiliza butoanele de aici din nou:
<app-matching-game [pairs]="animals"> <ng-template #leftpart let-animalPair="contextPair"> <button>{{animalPair.leftpart}}</button> </ng-template> <ng-template #rightpart let-animalPair="contextPair"> <button>{{animalPair.rightpart}}</button> </ng-template> </app-matching-game>
Atributul let-animalPair="contextPair"
este utilizat pentru a specifica faptul că variabila context contextPair
este utilizată în fragmentul de șablon cu numele animalPair
.
Fragmentele de șablon pot fi acum modificate după propriul gust. Pentru a demonstra acest lucru folosesc componenta game2
. Fișierul game2.component.ts primește același conținut ca game1.component.ts . În game2.component.html folosesc un element div
conceput individual în loc de un buton. Clasele CSS sunt stocate în fișierul game2.component.css .
<app-matching-game [pairs]="animals"> <ng-template #leftpart let-animalPair="contextPair"> <div class="myAnimal left">{{animalPair.leftpart}}</div> </ng-template> <ng-template #rightpart let-animalPair="contextPair"> <div class="myAnimal right">{{animalPair.rightpart}}</div> </ng-template> </app-matching-game>
După adăugarea etichetelor <app-game2></app-game2>
pe pagina de pornire app.component.html , a doua versiune a jocului apare când pornesc aplicația:
Posibilitățile de proiectare sunt acum aproape nelimitate. Ar fi posibil, de exemplu, să se definească o subclasă de Pair
care conține proprietăți suplimentare. De exemplu, adresele imaginilor pot fi stocate pentru părțile din stânga și/sau din dreapta. Imaginile pot fi afișate în șablon împreună cu textul sau în locul textului.
2. Controlul interacțiunii utilizatorului cu RxJS
Avantajele programării reactive cu RxJS
Pentru a transforma aplicația într-un joc interactiv, evenimentele (de exemplu, evenimentele de clic pe mouse) care sunt declanșate la interfața cu utilizatorul trebuie procesate. În programarea reactivă, sunt luate în considerare secvențe continue de evenimente, așa-numitele „fluxuri”. Un flux poate fi observat (este un „observabil”), adică pot exista unul sau mai mulți „observatori” sau „abonați” abonați la flux. Aceștia sunt notificați (de obicei în mod asincron) despre fiecare valoare nouă din flux și pot reacționa la aceasta într-un anumit mod.
Cu această abordare, se poate obține un nivel scăzut de cuplare între părțile unei aplicații. Observatorii și observabilele existenți sunt independenți unul de celălalt și cuplarea lor poate fi variată în timpul rulării.
Biblioteca JavaScript RxJS oferă o implementare matură a modelului de proiectare Observer. Mai mult, RxJS conține numeroși operatori pentru a converti fluxuri (de exemplu, filtrare, hartă) sau pentru a le combina în fluxuri noi (de exemplu, merge, concat). Operatorii sunt „funcții pure” în sensul programării funcționale: nu produc efecte secundare și sunt independenți de starea din afara funcției. O logica de program compusa doar din apeluri la functii pure nu are nevoie de variabile auxiliare globale sau locale pentru a stoca stari intermediare. Aceasta, la rândul său, promovează crearea de blocuri de cod fără stat și cuplate liber. Prin urmare, este de dorit să se realizeze o mare parte a gestionării evenimentelor printr-o combinație inteligentă de operatori de flux. Exemple în acest sens sunt date în secțiunea următoare, pe baza jocului nostru de potrivire.
Integrarea RxJS în gestionarea evenimentelor unei componente unghiulare
Cadrul Angular funcționează cu clasele bibliotecii RxJS. Prin urmare, RxJS este instalat automat atunci când este instalat Angular.
Imaginea de mai jos arată principalele clase și funcții care joacă un rol în considerațiile noastre:
Numele clasei | Funcţie |
---|---|
Observabil (RxJS) | Clasa de bază care reprezintă un flux; cu alte cuvinte, o secvență continuă de date. Un observabil poate fi abonat. Funcția pipe este utilizată pentru a aplica una sau mai multe funcții operator la instanța observabilă. |
Subiect (RxJS) | Subclasa de observabile oferă următoarea funcție de a publica date noi în flux. |
EventEmitter (unghiular) | Aceasta este o subclasă specifică unghiulară care este de obicei folosită numai împreună cu decoratorul @Output pentru a defini o ieșire a componentei. Ca și următoarea funcție, funcția emit este folosită pentru a trimite date către abonați. |
Abonament (RxJS) | Funcția de subscribe a unui observabil returnează o instanță de abonare. Este necesar să anulați abonamentul după utilizarea componentei. |
Cu ajutorul acestor clase, dorim să implementăm interacțiunea utilizatorului în jocul nostru. Primul pas este să vă asigurați că un element care este selectat de utilizator în partea stângă sau dreaptă este evidențiat vizual.
Reprezentarea vizuală a elementelor este controlată de cele două fragmente de șablon din componenta părinte. Decizia despre modul în care sunt afișate în starea selectată ar trebui, prin urmare, să fie lăsată și la componenta părinte. Ar trebui să primească semnale adecvate de îndată ce se face o selecție în partea stângă sau dreaptă sau de îndată ce o selecție urmează să fie anulată.
În acest scop, definesc patru valori de ieșire de tip EventEmitter
în fișierul matching-game.component.ts . Tipurile Output
și EventEmitter
trebuie importate din pachetul de bază.
@Output() leftpartSelected = new EventEmitter<number>(); @Output() rightpartSelected = new EventEmitter<number>(); @Output() leftpartUnselected = new EventEmitter(); @Output() rightpartUnselected = new EventEmitter();
În șablonul matching-game.component.html , reacționez la evenimentul mousedown
din partea stângă și dreaptă, apoi trimit ID-ul articolului selectat tuturor receptorilor.
<div *ngFor="let pair of unsolvedPairs" class="item" (mousedown)="leftpartSelected.emit(pair.id)"> ... <div *ngFor="let pair of unsolvedPairs | shuffle:test" class="item" (mousedown)="rightpartSelected.emit(pair.id)">
În cazul nostru, receptorii sunt componentele game1
și game2
. Acolo puteți defini acum gestionarea evenimentelor pentru evenimentele leftpartSelected
, rightpartSelected
, leftpartUnselected
și rightpartUnselected
. Variabila $event
reprezintă valoarea de ieșire emisă, în cazul nostru ID-ul. În cele ce urmează, puteți vedea lista pentru game1.component.html , pentru game2.component.html se aplică aceleași modificări.
<app-matching-game [pairs]="animals" (leftpartSelected)="onLeftpartSelected($event)" (rightpartSelected)="onRightpartSelected($event)" (leftpartUnselected)="onLeftpartUnselected()" (rightpartUnselected)="onRightpartUnselected()"> <ng-template #leftpart let-animalPair="contextPair"> <button [class.selected]="leftpartSelectedId==animalPair.id"> {{animalPair.leftpart}} </button> </ng-template> <ng-template #rightpart let-animalPair="contextPair"> <button [class.selected]="rightpartSelectedId==animalPair.id"> {{animalPair.rightpart}} </button> </ng-template> </app-matching-game>
În game1.component.ts (și în mod similar în game2.component.ts ), funcțiile de gestionare a event
sunt acum implementate. Stoc ID-urile elementelor selectate. În șablonul HTML (vezi mai sus), acestor elemente li se atribuie clasa selected
. Fișierul CSS game1.component.css definește modificările vizuale pe care le va aduce această clasă (de exemplu, modificări de culoare sau font). Resetarea selecției (deselectare) se bazează pe presupunerea că perechile de obiecte au întotdeauna ID-uri pozitive.
onLeftpartSelected(id:number):void{ this.leftpartSelectedId = id; } onRightpartSelected(id:number):void{ this.rightpartSelectedId = id; } onLeftpartUnselected():void{ this.leftpartSelectedId = -1; } onRightpartUnselected():void{ this.rightpartSelectedId = -1; }
În pasul următor, gestionarea evenimentelor este necesară în componenta jocului de potrivire. Trebuie determinat dacă o atribuire este corectă, adică dacă elementul selectat din stânga se potrivește cu elementul selectat din dreapta. În acest caz, perechea alocată poate fi mutată în containerul pentru perechile rezolvate.
Aș dori să formulez logica de evaluare folosind operatori RxJS (vezi secțiunea următoare). Pentru pregătire, creez un subiect assignmentStream
în matching-game.component.ts . Ar trebui să emită elementele selectate de utilizator pe partea stângă sau dreaptă. Scopul este de a folosi operatorii RxJS pentru a modifica și împărți fluxul în așa fel încât să obțin două fluxuri noi: un flux solvedStream
care furnizează perechile alocate corect și un al doilea flux failedStream
care furnizează atribuiri greșite. Aș dori să mă abonez la aceste două fluxuri cu subscribe
pentru a putea efectua gestionarea adecvată a evenimentelor în fiecare caz.
De asemenea, am nevoie de o referință la obiectele de abonament create, astfel încât să pot anula abonamentele cu „unsubscribe” când părăsesc jocul (vezi ngOnDestroy
). Clasele Subject
și Subscription
trebuie importate din pachetul „rxjs”.
private assignmentStream = new Subject<{pair:Pair, side:string}>(); private solvedStream = new Observable<Pair>(); private failedStream = new Observable<string>(); private s_Subscription: Subscription; private f_Subscription: Subscription; ngOnInit(){ ... //TODO: apply stream-operators on //assignmentStream this.s_Subscription = this.solvedStream.subscribe(pair => handleSolvedAssignment(pair)); this.f_Subscription = this.failedStream.subscribe(() => handleFailedAssignment()); } ngOnDestroy() { this.s_Subscription.unsubscribe(); this.f_Subscription.unsubscribe(); }
Dacă sarcina este corectă, se parcurg următorii pași:
- Perechea alocată este mutată în containerul pentru perechile rezolvate.
- Evenimentele
leftpartUnselected
șirightpartUnselected
sunt trimise la componenta părinte.
Nicio pereche nu este mutată dacă atribuirea este incorectă. Dacă alocarea greșită a fost executată de la stânga la dreapta ( side1
are valoarea left
), selecția ar trebui să fie anulată pentru elementul din partea stângă (vezi GIF-ul de la începutul articolului). Dacă o atribuire este făcută de la dreapta la stânga, selecția este anulată pentru elementul din partea dreaptă. Aceasta înseamnă că ultimul element pe care s-a făcut clic rămâne într-o stare selectată.
Pentru ambele cazuri, pregătesc funcțiile handler corespunzătoare handleSolvedAssignment
și handleFailedAssignment
(eliminare funcția: vezi codul sursă la sfârșitul acestui articol):
private handleSolvedAssignment(pair: Pair):void{ this.solvedPairs.push(pair); this.remove(this.unsolvedPairs, pair); this.leftpartUnselected.emit(); this.rightpartUnselected.emit(); //workaround to force update of the shuffle pipe this.test = Math.random() * 10; } private handleFailedAssignment(side1: string):void{ if(side1=="left"){ this.leftpartUnselected.emit(); }else{ this.rightpartUnselected.emit(); } }
Acum trebuie să schimbăm punctul de vedere de la consumatorul care subscrie la date la producătorul care generează datele. În fișierul matching-game.component.html , mă asigur că atunci când dau clic pe un element, obiectul pereche asociat este împins în fluxul assignmentStream
. Este logic să folosim un flux comun pentru partea stângă și dreapta, deoarece ordinea sarcinii nu este importantă pentru noi.
<div *ngFor="let pair of unsolvedPairs" class="item" (mousedown)="leftpartSelected.emit(pair.id)" (click)="assignmentStream.next({pair: pair, side: 'left'})"> ... <div *ngFor="let pair of unsolvedPairs | shuffle:test" class="item" (mousedown)="rightpartSelected.emit(pair.id)" (click)="assignmentStream.next({pair: pair, side: 'right'})">
Proiectarea interacțiunii jocului cu operatorii RxJS
Tot ce rămâne este să convertiți fluxul assignmentStream
în fluxurile solvedStream
și failedStream
. Aplic următorii operatori în secvență:
pairwise
Există întotdeauna două perechi într-o temă. Operatorul pairwise
alege datele în perechi din flux. Valoarea curentă și valoarea anterioară sunt combinate într-o pereche.
Din următorul flux...
„{pair1, left}, {pair3, right}, {pair2, left}, {pair2, right}, {pair1, left}, {pair1, right}“
…rezultă acest nou flux:
„({pair1, left}, {pair3, right}), ({pair3, right}, {pair2, left}), ({pair2, left}, {pair2, right}), ({pair2, right}, {pair1, left}), ({pair1, left}, {pair1, right})“
De exemplu, obținem combinația ({pair1, left}, {pair3, right})
când utilizatorul selectează dog
(id=1) în partea stângă și insect
(id=3) în partea dreaptă (vezi matricea ANIMALS
la începutul articolului). Acestea și celelalte combinații rezultă din secvența de joc afișată în GIF-ul de mai sus.
filter
Trebuie să eliminați toate combinațiile din flux care au fost făcute pe aceeași parte a terenului de joc, cum ar fi ({pair1, left}, {pair1, left})
sau ({pair1, left}, {pair4, left})
.
Condiția de filtrare pentru un comb
combinat este deci comb[0].side != comb[1].side
.
partition
Acest operator ia un flux și o condiție și creează două fluxuri din aceasta. Primul flux conține datele care îndeplinesc condiția, iar al doilea flux conține datele rămase. În cazul nostru, fluxurile ar trebui să conțină atribuiri corecte sau incorecte. Deci condiția pentru un comb
combinat este comb[0].pair===comb[1].pair
.
Exemplul are ca rezultat un flux „corect” cu
({pair2, left}, {pair2, right}), ({pair1, left}, {pair1, right})
și un flux „greșit” cu
({pair1, left}, {pair3, right}), ({pair3, right}, {pair2, left}), ({pair2, right}, {pair1, left})
map
Numai obiectul pereche individual este necesar pentru procesarea ulterioară a unei atribuiri corecte, cum ar fi pair2
. Operatorul hartă poate fi folosit pentru a exprima faptul că combinația comb
ar trebui mapată la comb[0].pair
. Dacă atribuirea este incorectă, comb
combinat este mapat la comb[0].side
, deoarece selecția ar trebui să fie resetată pe partea specificată de side
.
Funcția pipe
este utilizată pentru a concatena operatorii de mai sus. Operatorii pairwise
, filter
, partition
, map
trebuie importați din pachetul rxjs/operators
.
ngOnInit() { ... const stream = this.assignmentStream.pipe( pairwise(), filter(comb => comb[0].side != comb[1].side) ); //pipe notation leads to an error message (Angular 8.2.2, RxJS 6.4.0) const [stream1, stream2] = partition(comb => comb[0].pair === comb[1].pair)(stream); this.solvedStream = stream1.pipe( map(comb => comb[0].pair) ); this.failedStream = stream2.pipe( map(comb => comb[0].side) ); this.s_Subscription = this.solvedStream.subscribe(pair => this.handleSolvedAssignment(pair)); this.f_Subscription = this.failedStream.subscribe(side => this.handleFailedAssignment(side)); }
Acum jocul funcționează deja!
Prin utilizarea operatorilor, logica jocului ar putea fi descrisă declarativ. Am descris doar proprietățile celor două fluxuri țintă (combinate în perechi, filtrate, partiționate, remapate) și nu a trebuit să ne îngrijorăm cu privire la implementarea acestor operațiuni. Dacă le-am fi implementat noi înșine, ar fi trebuit să stocăm și stări intermediare în componentă (de ex. referințe la ultimele elemente pe care s-a făcut clic pe partea stângă și dreaptă). În schimb, operatorii RxJS încapsulează logica de implementare și stările necesare pentru noi și astfel ridică programarea la un nivel superior de abstractizare.
Concluzie
Folosind un joc simplu de învățare ca exemplu, am testat utilizarea RxJS într-o componentă Angular. Abordarea reactivă este potrivită pentru procesarea evenimentelor care apar pe interfața cu utilizatorul. Cu RxJS, datele necesare pentru gestionarea evenimentelor pot fi aranjate convenabil ca fluxuri. Numeroși operatori, cum ar fi filter
, map
sau partition
, sunt disponibili pentru transformarea fluxurilor. Fluxurile rezultate conțin date care sunt pregătite în forma sa finală și la care se pot abona direct. Este nevoie de puțină îndemânare și experiență pentru a selecta operatorii potriviți pentru cazul respectiv și pentru a le lega eficient. Acest articol ar trebui să ofere o introducere în acest sens.
Resurse suplimentare
- „Introducerea în programarea reactivă de care ați lipsit”, scrisă de Andre Staltz
Lectură similară pe SmashingMag:
- Gestionarea punctelor de întrerupere a imaginii cu Angular
- Stilizarea unei aplicații unghiulare cu Bootstrap
- Cum să creați și să implementați aplicația de material unghiular