Attiva il tuo Angular 2: aggiornamento da 1.5
Pubblicato: 2022-03-11Ho iniziato con il desiderio di scrivere una guida passo passo per aggiornare un'app da Angular 1.5 ad Angular 2, prima di essere educatamente informato dal mio editore che aveva bisogno di un articolo piuttosto che di un romanzo. Dopo molte riflessioni, ho accettato di dover iniziare con un'ampia rassegna dei cambiamenti in Angular 2, raggiungendo tutti i punti trattati nell'articolo di Jason Aden, Getting Past Hello World in Angular 2. ...Ops. Vai avanti e leggilo per avere una panoramica delle nuove funzionalità di Angular 2, ma per un approccio pratico mantieni il tuo browser qui.
Voglio che questa diventi una serie che alla fine comprenda l'intero processo di aggiornamento della nostra app demo ad Angular 2. Per ora, però, iniziamo con un unico servizio. Facciamo una passeggiata serpeggiante attraverso il codice e risponderò a qualsiasi domanda tu possa avere, come...
Angolare: La Vecchia Via
Se sei come me, la guida introduttiva di Angular 2 potrebbe essere stata la prima volta in cui hai guardato TypeScript. Molto rapidamente, secondo il proprio sito Web, TypeScript è "un superset digitato di JavaScript che si compila in semplice JavaScript". Installi il transpiler (simile a Babel o Traceur) e ti ritroverai con un linguaggio magico che supporta le funzionalità linguistiche ES2015 ed ES2016 oltre a una digitazione forte.
Potresti trovare rassicurante sapere che nessuna di questa configurazione arcana è strettamente necessaria. Non è terribilmente difficile scrivere codice Angular 2 in un semplice vecchio JavaScript, anche se non penso che ne valga la pena. È bello riconoscere un territorio familiare, ma gran parte delle novità ed eccitanti di Angular 2 è il suo nuovo modo di pensare piuttosto che la sua nuova architettura.
Quindi diamo un'occhiata a questo servizio che ho aggiornato da Angular 1.5 a 2.0.0-beta.17. È un servizio Angular 1.x abbastanza standard, con solo un paio di caratteristiche interessanti che ho cercato di notare nei commenti. È un po' più complicato della tua applicazione di giocattoli standard, ma tutto ciò che sta facendo è interrogare Zilyo, un'API disponibile gratuitamente che aggrega gli annunci di fornitori di noleggio come Airbnb. Siamo spiacenti, è un po' di codice.
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);
La ruga in questa particolare app è che mostra i risultati su una mappa. Altri servizi gestiscono più pagine di risultati implementando l'impaginazione o gli scroller pigri, che consentono loro di recuperare una pagina di risultati ordinata alla volta. Tuttavia, vogliamo mostrare tutti i risultati all'interno dell'area di ricerca e vogliamo che appaiano non appena ritornano dal server anziché apparire improvvisamente una volta caricate tutte le pagine. Inoltre, vogliamo mostrare all'utente gli aggiornamenti sui progressi in modo che abbiano un'idea di cosa sta succedendo.
Per ottenere ciò in Angular 1.5, ricorriamo ai callback. Le promesse ci portano a metà strada, come puoi vedere dal wrapper $q.all
che attiva il callback onCompleted
, ma le cose continuano a diventare piuttosto disordinate.
Quindi inseriamo lodash per creare tutte le richieste di pagina per noi e ogni richiesta è responsabile dell'esecuzione del callback onFetchPage
per assicurarsi che venga aggiunto alla mappa non appena è disponibile. Ma diventa complicato. Come puoi vedere dai commenti, mi sono perso nella mia logica e non sono riuscito a capire cosa veniva restituito a quale promessa quando.
La pulizia generale del codice soffre ancora di più (molto più di quanto sia strettamente necessario), perché una volta che divento confuso, da lì parte solo una spirale verso il basso. Dillo con me, per favore...
Angular 2: un nuovo modo di pensare
C'è un modo migliore e te lo mostrerò. Non spenderò troppo tempo sui concetti di ES6 (aka ES2015), perché ci sono posti molto migliori per conoscere queste cose e se hai bisogno di un punto di partenza, ES6-Features.org ha una buona panoramica di tutte le nuove divertenti funzionalità. Considera questo codice AngularJS 2 aggiornato:
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); } }
Freddo! Esaminiamo questa riga per riga. Ancora una volta, il transpiler TypeScript ci consente di utilizzare tutte le funzionalità ES6 che desideriamo perché converte tutto in JavaScript vanilla.
Le istruzioni di import
all'inizio utilizzano semplicemente ES6 per caricare i moduli di cui abbiamo bisogno. Dal momento che svolgo la maggior parte del mio sviluppo in ES5 (ovvero JavaScript normale), devo ammettere che è un po' fastidioso dover improvvisamente iniziare a elencare tutti gli oggetti che ho intenzione di utilizzare.
Tuttavia, tieni presente che TypeScript sta traspilando tutto in JavaScript e utilizza segretamente SystemJS per gestire il caricamento dei moduli. Le dipendenze vengono tutte caricate in modo asincrono ed è (presumibilmente) in grado di raggruppare il codice in un modo che elimina i simboli che non hai importato. Inoltre, tutto supporta la "minimizzazione aggressiva", che suona molto dolorosa. Quelle dichiarazioni di importazione sono un piccolo prezzo da pagare per evitare di affrontare tutto quel rumore.

Ad ogni modo, a parte il caricamento di funzionalità selettive dallo stesso Angular 2, prestare particolare attenzione alla riga import {Observable} from 'rxjs/Observable';
. RxJS è una libreria di programmazione reattiva strabiliante e pazzesca che fornisce parte dell'infrastruttura alla base di Angular 2. Ne sentiremo sicuramente parlare in seguito.
Ora arriviamo a @Injectable()
.
Ad essere onesti, non sono ancora del tutto sicuro di cosa significhi, ma il bello della programmazione dichiarativa è che non sempre abbiamo bisogno di capire i dettagli. Si chiama decoratore, che è un costrutto TypeScript di fantasia in grado di applicare proprietà alla classe (o altro oggetto) che lo segue. In questo caso, @Injectable()
insegna al nostro servizio come essere iniettato in un componente. La migliore dimostrazione arriva direttamente dalla bocca del cavallo, ma è piuttosto lunga, quindi ecco un'anteprima di come appare nel nostro AppComponent:
@Component({ ... providers: [HTTP_PROVIDERS, ..., ZilyoService] })
Il prossimo passo è la definizione della classe stessa. Ha una dichiarazione di export
prima di esso, il che significa, hai indovinato, possiamo import
il nostro servizio in un altro file. In pratica, importeremo il nostro servizio nel nostro componente AppComponent
, come sopra.
Subito dopo c'è il costruttore, dove puoi vedere una vera iniezione di dipendenza in azione. Il constructor(private http:Http) {}
aggiunge una variabile di istanza privata denominata http
che TypeScript riconosce magicamente come un'istanza del servizio HTTP. Il punto va a TypeScript!
Dopodiché, sono solo alcune variabili di istanza dall'aspetto regolare e una funzione di utilità prima di arrivare alla vera carne e patate, la funzione get
. Qui vediamo Http
in azione. Assomiglia molto all'approccio basato sulle promesse di Angular 1, ma sotto il cofano è molto più interessante. Essere basati su RxJS significa che otteniamo un paio di grandi vantaggi rispetto alle promesse:
- Possiamo cancellare l'
Observable
se non ci interessa più la risposta. Questo potrebbe essere il caso se stiamo costruendo un campo di completamento automatico typeahead e non ci preoccupiamo più dei risultati per "ca" una volta che hanno inserito "cat". - L'
Observable
può emettere più valori e l'abbonato verrà chiamato più e più volte per consumarli mentre vengono prodotti.
Il primo è ottimo in molte circostanze, ma è il secondo su cui ci stiamo concentrando nel nostro nuovo servizio. Esaminiamo la funzione get
riga per riga:
return this.http.get(this._countUrl, { search: this.parameterize(params) })
Sembra abbastanza simile alla chiamata HTTP basata su promessa che vedresti in Angular 1. In questo caso, stiamo inviando i parametri della query per ottenere un conteggio di tutti i risultati corrispondenti.
.map(this.extractData)
Una volta che la chiamata AJAX ritorna, invierà la risposta lungo il flusso. La map
del metodo è concettualmente simile alla funzione map
di un array, ma si comporta anche come un metodo then
di una promessa perché attende il completamento di qualsiasi cosa stesse accadendo a monte, indipendentemente dalla sincronicità o dall'asincronicità. In questo caso, accetta semplicemente l'oggetto risposta e prende in giro i dati JSON per passare a valle. Ora abbiamo:
.map(results => { if (typeof onCountResults === "function") { onCountResults(results.totalResults); } return results; })
Abbiamo ancora una richiamata imbarazzante che dobbiamo inserire lì. Vedi, non è tutta magia, ma possiamo elaborare onCountResults
non appena la chiamata AJAX ritorna, il tutto senza lasciare il nostro stream. Non è male. Per quanto riguarda la riga successiva:
.flatMap(results => Observable.range(1, results.totalPages))
Uh oh, lo senti? Un sottile silenzio è calato sulla folla in attesa e puoi dire che qualcosa di importante sta per accadere. Cosa significa anche questa linea? La parte di destra non è così pazza. Crea un intervallo RxJS, che ritengo un array glorificato Observable
-wrapped. Se results.totalPages
è uguale a 5, ti ritroverai con qualcosa come Observable.of([1,2,3,4,5])
.
flatMap
è, aspettalo, una combinazione di flatten
e map
. C'è un ottimo video che spiega il concetto su Egghead.io, ma la mia strategia è pensare a ogni Observable
come un array. Observable.range
crea il proprio wrapper, lasciandoci con l'array bidimensionale [[1,2,3,4,5]]
. flatMap
appiattisce l'array esterno, lasciandoci con [1,2,3,4,5]
, quindi esegue il map
semplicemente sull'array, passando i valori a valle uno alla volta. Quindi questa riga accetta un intero ( totalPages
) e lo converte in un flusso di interi da 1 a totalPages
. Potrebbe non sembrare molto, ma è tutto ciò che dobbiamo configurare.
Volevo davvero metterlo su una riga per aumentarne l'impatto, ma immagino che non puoi vincerli tutti. Qui vediamo cosa succede al flusso di interi che abbiamo impostato sull'ultima riga. Scorrono in questo passaggio uno per uno, quindi vengono aggiunti alla query come parametro di pagina prima di essere infine inseriti in una nuova richiesta AJAX e inviati per recuperare una pagina di risultati. Ecco quel codice:
.flatMap(i => { return this.http.get(this._searchUrl, { search: this.parameterize(Object.assign({}, params, { page: i })) }); })
Se totalPages
era 5, costruiamo 5 richieste GET e le inviamo tutte contemporaneamente. flatMap
iscrive a ogni nuovo Observable
, quindi quando le richieste ritornano (in qualsiasi ordine) vengono scartate e ogni risposta (come una pagina di risultati) viene inviata a valle una alla volta.
Diamo un'occhiata a come funziona tutta questa faccenda da un'altra angolazione. Dalla nostra richiesta di "conteggio" originaria, troviamo il numero totale di pagine di risultati. Creiamo una nuova richiesta AJAX per ogni pagina e, indipendentemente da quando ritornano (o in quale ordine), vengono inviate allo stream non appena sono pronte. Tutto ciò che il nostro componente deve fare è iscriversi all'Osservabile restituito dal nostro metodo get
e riceverà ogni pagina, una dopo l'altra, tutto da un unico flusso. Prendi quello, promesse.
Dopo è tutto un po' deludente:
.map(this.extractData).catch(this.handleError);
Quando ogni oggetto di risposta arriva da flatMap
, il relativo JSON viene estratto allo stesso modo della risposta dalla richiesta di conteggio. Aggiunto alla fine c'è l'operatore catch
, che aiuta a illustrare come funziona la gestione degli errori RxJS basata sul flusso. È abbastanza simile al tradizionale paradigma try/catch, tranne per il fatto che l'oggetto Observable
funziona anche per la gestione asincrona degli errori.
Ogni volta che viene rilevato un errore, corre a valle, ignorando gli operatori passati fino a quando non incontra un gestore di errori. Nel nostro caso, il metodo handleError
genera nuovamente l'errore, consentendoci di intercettarlo all'interno del servizio ma anche di consentire all'abbonato di fornire il proprio callback onError
che si attiva ancora più a valle. La gestione degli errori ci mostra che non abbiamo sfruttato appieno il nostro stream, anche con tutte le cose interessanti che abbiamo già realizzato. È banale aggiungere un operatore di ripetizione dopo le nostre richieste HTTP, che retry
una singola richiesta se restituisce un errore. Come misura preventiva, potremmo anche aggiungere un operatore tra il generatore di range
e le richieste, aggiungendo una qualche forma di limitazione della velocità in modo da non inviare spam al server con troppe richieste tutte in una volta.
Riepilogo: l'apprendimento di Angular 2 non riguarda solo un nuovo framework
Imparare Angular 2 è più come incontrare una famiglia completamente nuova e alcune delle loro relazioni sono complicate. Spero di essere riuscito a dimostrare che queste relazioni si sono evolute per una ragione, e c'è molto da guadagnare rispettando le dinamiche che esistono all'interno di questo ecosistema. Spero che anche questo articolo ti sia piaciuto, perché ho appena graffiato la superficie e c'è molto altro da dire su questo argomento.