BFF-ul tău atunci când ai de-a face cu mai multe fluxuri observabile.

Nu există nicio îndoială că „RxJS” este una dintre cele mai populare biblioteci în dezvoltarea web de astăzi. Nu există un proiect din care fac parte în care RxJS să nu fi fost folosit și cu un motiv întemeiat. În special cu utilizarea punctelor de integrare direcționate într-un număr tot mai mare de cadre, biblioteci și utilități. Oferă o abordare puternică și funcțională pentru a face față evenimentelor. Pentru mine, a avea o înțelegere solidă asupra RxJS și a ceea ce are de oferit pare a fi o idee deloc.

Învățarea RxJS și programarea reactivă poate fi dificilă, mai ales dacă sunteți nou în dezvoltarea web. Există o multitudine de concepte noi, un serviciu API mare și să nu uităm o schimbare fundamentală a mentalității de la un stil imperativ la un stil declarativ. Deși a-ți înfășura capul în jurul RxJ-urilor poate fi o provocare, știu că a fost pentru mine, nu trebuie să fie o durere în culo.

În această postare, voi trece peste ceea ce consider că este unul dintre cei mai obișnuiți operatori de transformare, voi descrie ce este un operator de transformare și voi oferi câteva exemple care să-l însoțească

Ce sunt operatorii de transformare?

Operatorii de transformare, după cum sugerează și numele, vă permit să transformați valorile pe măsură ce acestea curg printr-un flux observabil. Pentru dezvoltarea mea, operatorii de transformare au fost cheie atunci când se ocupă de solicitările API și gestionează datele primite. Veți găsi acești operatori salvând ziua, astfel încât să nu aveți de-a face cu mai multe abonamente imbricate. Cei mai obișnuiți operatori de transformare se încadrează într-o anumită subsecțiune, care sunt cunoscuți ca operatori de aplatizare, cum ar fi: mergeMap, concatMap, switchMap și exhaustMap.

Ce este un operator de aplatizare?

Un caz obișnuit de utilizare pe care îl puteți întâlni tot timpul când construiți o aplicație web modernă cu RxJS este nevoia de a emite dintr-un flux în altul.

Să ne imaginăm că avem un flux observabil dintr-o intrare de text și înregistrăm intrarea utilizatorului.

const textInput = document.getElementByID('search-box');
const input$ = fromEvent(textInput, 'keyup)
input$.pipe(
   map(event => event.target.value)
).subscribe(console.log)

Folosim operatorul map pentru a transforma fluxul de evenimente și pentru a returna doar valoarea țintă. Cu toate acestea, încă emitem doar o valoare de bază. Într-o aplicație din lumea reală, este foarte probabil să nu înregistrăm doar valoarea, ci am putea inițializa o solicitare de rețea pe baza intrării utilizatorului. Când tocmai începeam, aș rezolva această problemă prin imbricarea abonamentelor, astfel:

const textInput = document.getElementByID('search-box');
const input$ = fromEvent(textInput, 'keyup)
input$.pipe(
   map(event => {
       const term = event.target.value;
       return ajax.getJSON(`https://api.github.com/users/${term}`)
      })
).subscribe(response => { 
     response.subscribe(console.log)
});

Deși acest lucru funcționează, cu răspunsul conectat cu succes la consolă, nu este într-adevăr ideal din câteva motive.

În primul rând, abonamentele imbricate pot arăta destul de urât, înlătură lizibilitatea și pot fi dificil de urmărit și de depanat. Rezultatul este că aveți acest al doilea abonament imbricat pe care trebuie să îl luați în considerare.

În plus, este posibil să trebuiască chiar să transformați acest răspuns, ceea ce duce la nevoia de a face față unui observabil cu proprii operatori imbricați într-un abonament cu observabile multiple. Ceva de genul:

input$.pipe(
   map(event => {
       const term = event.target.value;
       return ajax.getJSON(`https://api.github.com/users/${term}`)
      })
).subscribe(response => { 
      response.pipe(
         map(({login, id}) => { 
          return of(`Do not do this. This is bad.`);
         })
      ).subscribe(response2 => { 
                  response2.subscribe(console.log)
                 })
});

Puteți vedea cum asta începe să devină o durere de cap.

Aici vin operatorii noștri de aplatizare pentru a salva situația. În concluzie: ei iau un observabil care emite observabile și returnează un observabil doar din valorile observabile emise. Ceea ce sună foarte confuz acum că o spun cu voce tare, așa că permiteți-mi să vă arăt, începând cu cel mai simplu operator de aplatizare: mergeAll.

input$.pipe(
   map(event => {
      const term = event.target.value;
      return ajax.getJSON(`https://api.github.com/users/${term}`)
   }), 
   mergeAll()
).subscribe(console.log);

Când mergeAll primește un observabil de la sursă, acesta se abonează intern, emițând orice rezultat al observabilului respectiv. În acest caz, se va abona la observabilul ajax și va emite răspunsul pe acest flux. Nu este nevoie să aveți un abonament imbricat și să începeți să creați iadul de apel invers, în schimb utilizați operatorul mergeAll și permiteți-i să facă asta pentru dvs.

Înțelegerea operatorului mergeMap

Cel mai comun operator de aplatizare (după părerea mea) trebuie să fie operatorul mergeMap. Să luăm exemplul de mai sus și să-l curățăm și mai mult. Deci, în loc să folosiți map, apoi mergeAll. Combinăm cele două și folosim mergeMap:

input$.pipe(
   mergeMap(event => {
      const term = event.target.value;
      return ajax.getJSON(`https://api.github.com/users/${term}`)
   }), 
).subscribe(console.log);

Acum avem exact același comportament ca înainte, dar cu un operator în loc de doi. Sunt sigur că acum puteți vedea puterea operatorilor de transformare și modul în care aceștia vă îmbunătățesc fluxurile observabile.

Să trecem la alt operator, switchMap. Pentru a înțelege diferențele dintre mergeMap și switchMap, vedeți unde operatorul mergeMap ar putea fi scurt și unde switchMap ar putea fi alegerea mai bună.

Înțelegerea operatorului switchMap

Operatorul switchMap mapează fiecare valoare la un observabil și apoi aplatizează acel observabil. Spre deosebire de mergeMap, switchMap menține doar un abonament intern activ la un moment dat.

Aceasta înseamnă că de fiecare dată când mapăm la un nou observabil interior, cel anterior este finalizat.

Ceea ce face switchMap unic este evident pe următoarea valoare emisă de la sursă observabilă. În acest moment, observabilul interior activ anterior va fi completat și următorul observabil returnat va fi comutat la.

Pentru a înțelege cât de diferită switchMap de mergeMap, să începem cu un nou exemplu. În acest exemplu, avem un flux de evenimente de clic pe care le mapam la un interval interior la fiecare clic.

const interval$ = interval(1000);
const click$ = fromEvent(document, 'click');
click$.pipe(
    mergeMap(() => interval$)
).subscribe(console.log)

La primul clic, se creează un interval. La al doilea clic, un al doilea interval este creat în timp ce primul interval rămâne. Dacă există un alt clic; se creează un al treilea interval. Consola va arăta cam așa:

0 1 2 3 0 4 1 5 2 6 0 3 7 1

Acum, să schimbăm mergeMap cu switchMap aici.

Veți vedea că comportamentul va diferi.

La primul clic, intervalul va începe să ruleze la fel ca mergeMap, dar când există un al doilea clic, în loc să fie creat un al doilea abonament interior, veți găsi că intervalul repornește:

click$.pipe(
    switchMap(() => interval$)
).subscribe(console.log)
// outcome: 0 1 2 3 0 1 2 0 1 2 3

Repornirea se va întâmpla la fiecare omisiune din sursa observabilă (click$) se mapa noul interval, apoi se abona, apoi se finalizează abonamentul activ anterior.

Acum că înțelegem mai bine comportamentul operatorului switchMap, să aruncăm o privire asupra unui exemplu real:

const searchInput = document.getElementByID('search-box');
const input$ = fromEvent(searchInput, 'keyup')
input$.pipe(
  debounceTime(200),
  pluck('target', 'value'),
  distinctUntilChanged()
).subscribe(console.log)

Similar cu primul nostru exemplu, avem un alt flux de evenimente „keyup” din intrarea de căutare.

În primul rând, anulăm fluxul, cu operatorul debounceTime, pentru a emite cea mai recentă valoare numai după o pauză de 200 ms. Apoi folosim operatorul pluck pentru a alege proprietatea cuib a valorii țintă, apoi aplicăm operatorul distinctUntilChanged() pentru a ne asigura că sunt emise numai valori unice. Deci, atunci când un utilizator introduce intrarea de căutare, după ce utilizatorul face o pauză de 200 ms sau mai mult, valoarea este emisă în consolă.

Să adăugăm switchMap pentru a finaliza această funcționalitate:

input$.pipe(
  debounceTime(200),
  pluck('target', 'value'),
  distinctUntilChanged(), 
  switchMap(searchTerm => { 
     return ajax.getJSON(`http://api.som.org/${searchTerm}`)
  })
).subscribe(console.log)

Aceasta reflectă maparea oricărui termen de căutare pe care îl primim de la intrare la un observabil ajax, aplatindu-l cu operatorul switchMap și apoi emite răspunsul.

Am auzit că întrebați, de ce să folosiți switchMap peste mergeMap pentru această funcționalitate?

Cu mergeMap, veți primi mai multe răspunsuri la server, iar răspunsurile ar putea reveni din nou în ordine, ceea ce va echivala cu o performanță proastă și o experiență de utilizator suboptimă. Veți efectua mai multe apeluri inutile către server 👎

Cu operatorul switchMap, trece la un nou observabil în timp ce anulează observabilul interior anterior, inclusiv solicitarea dacă este emisă o valoare actualizată.

Operatorul switchMap este văzut ca cel mai sigur implicit pentru aplatizare, este cu adevărat util pentru solicitările HTTP care pot fi anulate și excelent pentru funcționalitatea de odihnă, pauză și reluare.

Referințe: Unele dintre exemplele de cod au fost preluate din cursul RxJs Basic de Ultimate Courses.