Porniți-vă Angular 2: upgrade de la 1.5
Publicat: 2022-03-11Am început să vreau să scriu un ghid pas cu pas pentru actualizarea unei aplicații de la Angular 1.5 la Angular 2, înainte de a fi informat politicos de editorul meu că are nevoie de un articol mai degrabă decât de un roman. După multă deliberare, am acceptat că trebuie să încep cu o analiză amplă a modificărilor din Angular 2, lovind toate punctele abordate în articolul lui Jason Aden, Depășirea Hello World în Angular 2. … Hopa. Continuați și citiți-l pentru a obține o prezentare generală a noilor funcții ale lui Angular 2, dar pentru o abordare practică păstrați browserul chiar aici.
Vreau ca aceasta să devină o serie care în cele din urmă să cuprindă întregul proces de actualizare a aplicației noastre demo la Angular 2. Deocamdată, totuși, să începem cu un singur serviciu. Haideți să facem o plimbare șerpuitoare prin cod și vă voi răspunde la orice întrebări pe care le aveți, cum ar fi...
Angular: Vechea Cale
Dacă sunteți ca mine, ghidul de pornire rapidă Angular 2 ar fi fost prima dată când v-ați uitat vreodată la TypeScript. Cu adevărat rapid, conform propriului său site web, TypeScript este „un superset tip de JavaScript care se compilează în JavaScript simplu”. Instalați transpilerul (similar cu Babel sau Traceur) și ajungeți cu un limbaj magic care acceptă caracteristicile de limbă ES2015 și ES2016, precum și tastare puternică.
S-ar putea să ți se pară liniștitor să știi că niciuna dintre aceste configurații arcane nu este strict necesară. Nu este îngrozitor de dificil să scrii codul Angular 2 în JavaScript vechi simplu, deși nu cred că merită să faci asta. Este plăcut să recunoaștem un teritoriu familiar, dar ceea ce este nou și interesant la Angular 2 este mai degrabă noul mod de a gândi decât noua arhitectură.
Deci, să ne uităm la acest serviciu pe care l-am actualizat de la Angular 1.5 la 2.0.0-beta.17. Este un serviciu Angular 1.x destul de standard, cu doar câteva caracteristici interesante pe care am încercat să le notez în comentarii. Este puțin mai complicată decât aplicația dvs. standard de jucării, dar tot ceea ce face cu adevărat este să interogheze Zilyo, un API disponibil gratuit care adună listări de la furnizorii de închirieri precum Airbnb. Îmi pare rău, este destul de puțin cod.
zilyo.service.js (1.5.5)
'use strict'; function zilyoService($http, $filter, $q) { // it's a singleton, so set up some instance and static variables in the same place var baseUrl = "https://zilyo.p.mashape.com/search"; var countUrl = "https://zilyo.p.mashape.com/count"; var state = { callbacks: {}, params: {} }; // interesting function - send the parameters to the server and ask // how many pages of results there will be, then process them in handleCount function get(params, callbacks) { // set up the state object if (params) { state.params = params; } if (callbacks) { state.callbacks = callbacks; } // get a count of the number of pages of search results return $http.get(countUrl + "?" + parameterize(state.params)) .then(extractData, handleError) .then(handleCount); } // make the factory return { get : get }; // boring function - takes an object of URL query params and stringifies them function parameterize(params) { return Object.keys(params).map(key => `${key}=${params[key]}`).join("&"); } // interesting function - takes the results of the "count" AJAX call and // spins off a call for each results page - notice the unpleasant imperativeness function handleCount(response) { var pages = response.data.result.totalPages; if (typeof state.callbacks.onCountResults === "function") { state.callbacks.onCountResults(response.data); } // request each page var requests = _.times(pages, function (i) { var params = Object.assign({}, { page : i + 1 }, state.params); return fetch(baseUrl, params); }); // and wrap all requests in a promise return $q.all(requests).then(function (response) { if (typeof state.callbacks.onCompleted === "function") { state.callbacks.onCompleted(response); } return response; }); } // interesting function - fetch an individual page of results // notice how a special callback is required because the $q.all wrapper // will only return once ALL pages have been fetched function fetch(url, params) { return $http.get(url + "?" + parameterize(params)).then(function(response) { if (typeof state.callbacks.onFetchPage == "function") { // emit each page as it arrives state.callbacks.onFetchPage(response.data); } return response.data; // took me 15 minutes to realize I needed this }, (response) => console.log(response)); } // boring function - takes the result object and makes sure it's defined function extractData(res) { return res || { }; } // boring function - log errors, provide teaser for greater ambitions function handleError (error) { // In a real world app, we might send the error to remote logging infrastructure var errMsg = error.message || 'Server error'; console.error(errMsg); // log to console instead return errMsg; } } // register the service angular.module('angularZilyoApp').factory('zilyoService', zilyoService);
Problema acestei aplicații este că arată rezultatele pe o hartă. Alte servicii gestionează mai multe pagini de rezultate prin implementarea de paginare sau defilare leneșă, ceea ce le permite să recupereze câte o pagină ordonată de rezultate la un moment dat. Cu toate acestea, dorim să afișăm toate rezultatele în zona de căutare și dorim ca acestea să apară imediat ce revin de pe server, mai degrabă decât să apară brusc odată ce toate paginile sunt încărcate. În plus, dorim să afișăm actualizările de progres pentru utilizator, astfel încât să aibă o idee despre ce se întâmplă.
Pentru a realiza acest lucru în Angular 1.5, recurgem la apeluri inverse. Promisiunile ne duc parțial acolo, așa cum puteți vedea din pachetul $q.all
care declanșează apelul onCompleted
, dar lucrurile devin încă destul de dezordonate.
Apoi aducem lodash pentru a crea toate solicitările de pagină pentru noi și fiecare solicitare este responsabilă pentru executarea onFetchPage
pentru a ne asigura că este adăugată pe hartă de îndată ce este disponibilă. Dar asta devine complicat. După cum puteți vedea din comentarii, m-am pierdut în propria mea logică și nu am putut să mă pricepem la ceea ce era returnat la care promisiune când.
Curățenia generală a codului suferă și mai mult (mult mai mult decât este strict necesar), deoarece odată ce devin confuz, de acolo nu face decât să spiraleze în jos. Spune-mi cu mine, te rog...
Angular 2: un nou mod de a gândi
Există o modalitate mai bună și o să ți-o arăt. Nu voi petrece prea mult timp pe conceptele ES6 (alias ES2015), pentru că există locuri mult mai bune pentru a afla despre aceste lucruri și, dacă aveți nevoie de un punct de plecare, ES6-Features.org are o imagine de ansamblu bună. dintre toate noile funcții distractive. Luați în considerare acest cod AngularJS 2 actualizat:
zilyo.service.ts (2.0.0-beta.17)
import {Injectable} from 'angular2/core'; import {Http, Response, Headers, RequestOptions} from 'angular2/http'; import {Observable} from 'rxjs/Observable'; import 'rxjs/Rx'; @Injectable() export class ZilyoService { constructor(private http: Http) {} private _searchUrl = "https://zilyo.p.mashape.com/search"; private _countUrl = "https://zilyo.p.mashape.com/count"; private parameterize(params: {}) { return Object.keys(params).map(key => `${key}=${params[key]}`).join("&"); } get(params: {}, onCountResults) { return this.http.get(this._countUrl, { search: this.parameterize(params) }) .map(this.extractData) .map(results => { if (typeof onCountResults === "function") { onCountResults(results.totalResults); } return results; }) .flatMap(results => Observable.range(1, results.totalPages)) .flatMap(i => { return this.http.get(this._searchUrl, { search: this.parameterize(Object.assign({}, params, { page: i })) }); }) .map(this.extractData) .catch(this.handleError); } private extractData(res: Response) { if (res.status < 200 || res.status >= 300) { throw new Error('Bad response status: ' + res.status); } let body = res.json(); return body.result || { }; } private handleError (error: any) { // In a real world app, we might send the error to remote logging infrastructure let errMsg = error.message || 'Server error'; console.error(errMsg); // log to console instead return Observable.throw(errMsg); } }
Misto! Să trecem prin această linie cu rând. Din nou, transpilerul TypeScript ne permite să folosim orice caracteristică ES6 pe care o dorim, deoarece convertește totul în JavaScript vanilla.
Declarațiile de import
de la început folosesc pur și simplu ES6 pentru a încărca modulele de care avem nevoie. Deoarece îmi fac cea mai mare parte a dezvoltării în ES5 (aka JavaScript obișnuit), trebuie să recunosc că este puțin enervant să trebuiască brusc să încep să listez fiecare obiect pe care intenționez să îl folosesc.
Cu toate acestea, rețineți că TypeScript transpune totul în JavaScript și utilizează în secret SystemJS pentru a gestiona încărcarea modulelor. Toate dependențele sunt încărcate în mod asincron și (se presupune) poate să vă grupeze codul într-un mod care să elimine simbolurile pe care nu le-ați importat. În plus, totul acceptă „minificarea agresivă”, care sună foarte dureros. Acele declarații de import sunt un preț mic de plătit pentru a evita de a face cu tot acest zgomot.

Oricum, în afară de încărcarea caracteristicilor selective din Angular 2 în sine, luați o atenție specială la import {Observable} from 'rxjs/Observable';
. RxJS este o bibliotecă de programare reactivă uluitoare, nebunească, care oferă o parte din infrastructura care stă la baza Angular 2. Cu siguranță vom afla de ea mai târziu.
Acum ajungem la @Injectable()
.
Încă nu sunt complet sigur ce înseamnă asta, să fiu sincer, dar frumusețea programării declarative este că nu trebuie întotdeauna să înțelegem detaliile. Se numește decorator, care este un construct TypeScript fantezist capabil să aplice proprietăți clasei (sau altui obiect) care îl urmează. În acest caz, @Injectable()
învață serviciul nostru cum să fie injectat într-o componentă. Cea mai bună demonstrație vine direct din gura calului, dar este destul de lungă, așa că iată o scurtă privire a modului în care arată în AppComponent:
@Component({ ... providers: [HTTP_PROVIDERS, ..., ZilyoService] })
Următorul este definiția clasei în sine. Are o declarație de export
înainte, ceea ce înseamnă, ați ghicit, putem import
serviciul nostru într-un alt fișier. În practică, vom importa serviciul nostru în componenta AppComponent
, ca mai sus.
Imediat după aceea este constructorul, unde puteți vedea o injecție de dependență reală în acțiune. Constructorul de linie constructor(private http:Http) {}
adaugă o variabilă de instanță privată numită http
pe care TypeScript o recunoaște în mod magic ca o instanță a serviciului Http. Punctul merge la TypeScript!
După aceea, sunt doar câteva variabile de instanță cu aspect obișnuit și o funcție de utilitate înainte de a ajunge la carnea și cartofii adevărate, funcția get
. Aici vedem Http
în acțiune. Seamănă mult cu abordarea bazată pe promisiuni a lui Angular 1, dar sub capotă este mult mai rece. A fi construit pe RxJS înseamnă că obținem câteva avantaje mari față de promisiuni:
- Putem anula
Observable
dacă nu ne mai pasă de răspuns. Acesta ar putea fi cazul dacă construim un câmp de completare automată înainte de tip și nu ne mai interesează rezultatele pentru „ca” odată ce au introdus „cat”. -
Observable
poate emite mai multe valori, iar abonatul va fi chemat din nou și din nou să le consume pe măsură ce sunt produse.
Primul este grozav în multe circumstanțe, dar este al doilea pe care ne concentrăm în noul nostru serviciu. Să trecem prin funcția get
linie cu linie:
return this.http.get(this._countUrl, { search: this.parameterize(params) })
Arată destul de asemănător cu apelul HTTP bazat pe promisiuni pe care l-ați vedea în Angular 1. În acest caz, trimitem parametrii de interogare pentru a obține o contorizare a tuturor rezultatelor potrivite.
.map(this.extractData)
Odată ce apelul AJAX revine, acesta va trimite răspunsul în flux. map
metodei este similară conceptual cu funcția de map
a unui tablou, dar se comportă și ca metoda then
a unei promisiuni, deoarece așteaptă ca orice s-a întâmplat în amonte să se termine, indiferent de sincronicitate sau asincronicitate. În acest caz, pur și simplu acceptă obiectul răspuns și scoate datele JSON pentru a le transmite în aval. Acum avem:
.map(results => { if (typeof onCountResults === "function") { onCountResults(results.totalResults); } return results; })
Mai avem un apel invers incomod pe care trebuie să îl introducem acolo. Vedeți, nu este totul magie, dar putem procesa onCountResults
imediat ce apelul AJAX revine, totul fără a părăsi fluxul nostru. Nu e prea rău. Cât despre următoarea linie:
.flatMap(rezultate => Observable.range(1, results.totalPages))
Uh, o simți? O liniște subtilă a venit peste mulțimea care se uită și vă puteți da seama că ceva major este pe cale să se întâmple. Ce înseamnă această linie? Partea dreaptă nu e chiar atât de nebună. Creează o gamă RxJS, pe care o consider o matrice înfășurată în Observable
glorificată. Dacă results.totalPages
este egal cu 5, ajungeți cu ceva de genul Observable.of([1,2,3,4,5])
.
flatMap
este, așteptați, o combinație de flatten
și map
. Există un videoclip grozav care explică conceptul la Egghead.io, dar strategia mea este să mă gândesc la fiecare Observable
ca pe o matrice. Observable.range
își creează propriul wrapper, lăsându-ne cu matricea bidimensională [[1,2,3,4,5]]
. flatMap
aplatizează matricea exterioară, lăsându-ne cu [1,2,3,4,5]
, apoi map
pur și simplu hărți peste matrice, trecând valorile în aval una câte una. Deci această linie acceptă un număr întreg ( totalPages
) și îl convertește într-un flux de numere întregi de la 1 la totalPages
. Poate că nu pare mult, dar asta este tot ce avem nevoie pentru a configura.
Mi-am dorit foarte mult să aduc asta pe o singură linie pentru a-i crește impactul, dar cred că nu le poți câștiga pe toate. Aici vedem ce se întâmplă cu fluxul de numere întregi pe care l-am configurat pe ultima linie. Acestea curg în acest pas unul câte unul, apoi sunt adăugate la interogare ca parametru de pagină înainte de a fi în cele din urmă împachetate într-o cerere AJAX nouă și trimise pentru a prelua o pagină de rezultate. Iată acel cod:
.flatMap(i => { return this.http.get(this._searchUrl, { search: this.parameterize(Object.assign({}, params, { page: i })) }); })
Dacă totalPages
a fost 5, construim 5 solicitări GET și le trimitem pe toate simultan. flatMap
se abonează la fiecare nou Observable
, astfel încât, atunci când solicitările revin (în orice ordine), acestea sunt despachetate și fiecare răspuns (ca o pagină de rezultate) este împins în aval pe rând.
Să vedem cum funcționează toată treaba din alt unghi. Din cererea noastră de „număr” inițială, găsim numărul total de pagini de rezultate. Creăm o nouă solicitare AJAX pentru fiecare pagină și, indiferent când revin (sau în ce ordine), acestea sunt trimise în flux de îndată ce sunt gata. Tot ceea ce trebuie să facă componenta noastră este să se aboneze la Observable returnat de metoda noastră get
și va primi fiecare pagină, una după alta, toate dintr-un singur flux. Ia asta, promisiuni.
Totul este puțin anti-climatic după aceea:
.map(this.extractData).catch(this.handleError);
Pe măsură ce fiecare obiect de răspuns ajunge din flatMap
, JSON-ul său este extras în același mod ca răspunsul de la cererea de numărare. Încheiat până la capăt este operatorul catch
, care ajută la ilustrarea modului în care funcționează gestionarea erorilor RxJS bazată pe flux. Este destul de similar cu paradigma tradițională try/catch, cu excepția faptului că obiectul Observable
funcționează și pentru gestionarea asincronă a erorilor.
Ori de câte ori se întâlnește o eroare, aceasta se deplasează în aval, sărind peste operatori până când întâlnește un handler de erori. În cazul nostru, metoda handleError
re-aruncă eroarea, permițându-ne să o interceptăm în cadrul serviciului, dar și să lăsăm abonatului să ofere propriul său callback onError
care se declanșează și mai în aval. Gestionarea erorilor ne arată că nu am profitat din plin de fluxul nostru, chiar și cu toate lucrurile interesante pe care le-am realizat deja. Este trivial să adăugați un operator de retry
după solicitările noastre HTTP, care reîncearcă o solicitare individuală dacă returnează o eroare. Ca măsură preventivă, am putea adăuga și un operator între generatorul de range
și cereri, adăugând o formă de limitare a ratei, astfel încât să nu trimitem spam serverului cu prea multe solicitări simultan.
Recapitulare: Învățarea Angular 2 nu este doar despre un nou cadru
Învățarea Angular 2 este mai degrabă ca întâlnirea cu o familie complet nouă, iar unele dintre relațiile lor sunt complicate. Sper că am reușit să demonstrez că aceste relații au evoluat dintr-un motiv și că sunt multe de câștigat respectând dinamica care există în cadrul acestui ecosistem. Sper că v-a plăcut și acest articol, pentru că abia am zgâriat suprafața și mai sunt multe de spus despre acest subiect.