Bringen Sie Ihr Angular 2 zum Laufen: Upgrade von 1.5
Veröffentlicht: 2022-03-11Ich wollte ursprünglich eine Schritt-für-Schritt-Anleitung für das Upgrade einer App von Angular 1.5 auf Angular 2 schreiben, bevor ich von meiner Lektorin höflich darüber informiert wurde, dass sie eher einen Artikel als einen Roman brauchte. Nach langem Überlegen akzeptierte ich, dass ich mit einem breiten Überblick über die Änderungen in Angular 2 beginnen musste und dabei alle Punkte traf, die in Jason Adens Artikel „Getting Past Hello World in Angular 2“ behandelt wurden. …Hoppla. Machen Sie weiter und lesen Sie es, um einen Überblick über die neuen Funktionen von Angular 2 zu erhalten, aber für einen praktischen Ansatz behalten Sie Ihren Browser genau hier.
Ich möchte, dass dies eine Serie wird, die schließlich den gesamten Prozess des Upgrades unserer Demo-App auf Angular 2 umfasst. Beginnen wir jedoch zunächst mit einem einzelnen Dienst. Lassen Sie uns einen mäandernden Spaziergang durch den Code machen und ich werde alle Fragen beantworten, die Sie haben könnten, wie z.
Eckig: Der alte Weg
Wenn Sie wie ich sind, war die Angular 2-Schnellstartanleitung möglicherweise das erste Mal, dass Sie sich mit TypeScript befasst haben. Ganz schnell, laut seiner eigenen Website, ist TypeScript „eine typisierte Obermenge von JavaScript, die zu einfachem JavaScript kompiliert wird“. Sie installieren den Transpiler (ähnlich wie Babel oder Traceur) und erhalten am Ende eine magische Sprache, die ES2015- und ES2016-Sprachfunktionen sowie starkes Tippen unterstützt.
Vielleicht finden Sie es beruhigend zu wissen, dass keine dieser arkanen Einstellungen unbedingt erforderlich ist. Es ist nicht sehr schwierig, Angular-2-Code in einfachem, altem JavaScript zu schreiben, obwohl ich nicht denke, dass es sich lohnt, dies zu tun. Es ist schön, vertrautes Terrain wiederzuerkennen, aber so viel Neues und Aufregendes an Angular 2 ist eher seine neue Denkweise als seine neue Architektur.
Schauen wir uns also diesen Dienst an, den ich von Angular 1.5 auf 2.0.0-beta.17 aktualisiert habe. Es ist ein ziemlich normaler Angular 1.x-Dienst mit nur ein paar interessanten Funktionen, die ich versucht habe, in den Kommentaren zu erwähnen. Es ist etwas komplizierter als Ihre Standard-Spielzeuganwendung, aber alles, was es wirklich tut, ist Zilyo abzufragen, eine frei verfügbare API, die Angebote von Vermietungsanbietern wie Airbnb zusammenfasst. Entschuldigung, es ist ziemlich viel Code.
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);
Der Haken an dieser speziellen App ist, dass sie die Ergebnisse auf einer Karte anzeigt. Andere Dienste verarbeiten mehrere Ergebnisseiten, indem sie Paginierung oder Lazy Scroller implementieren, wodurch sie jeweils eine ordentliche Ergebnisseite abrufen können. Wir möchten jedoch alle Ergebnisse innerhalb des Suchbereichs anzeigen, und wir möchten, dass sie erscheinen, sobald sie vom Server zurückkehren, anstatt plötzlich zu erscheinen, sobald alle Seiten geladen sind. Außerdem möchten wir dem Benutzer Fortschrittsaktualisierungen anzeigen, damit er eine Vorstellung davon hat, was passiert.
Um dies in Angular 1.5 zu erreichen, greifen wir auf Callbacks zurück. Promises bringen uns auf halbem Weg dorthin, wie Sie am $q.all
Wrapper sehen können, der den onCompleted
Callback auslöst, aber die Dinge werden immer noch ziemlich chaotisch.
Dann bringen wir lodash ein, um alle Seitenanforderungen für uns zu erstellen, und jede Anforderung ist für die Ausführung des onFetchPage
Rückrufs verantwortlich, um sicherzustellen, dass sie der Karte hinzugefügt wird, sobald sie verfügbar ist. Aber das wird kompliziert. Wie Sie an den Kommentaren sehen können, habe ich mich in meiner eigenen Logik verirrt und konnte nicht in den Griff bekommen, was wann auf welches Versprechen zurückgegeben wurde.
Die allgemeine Sauberkeit des Codes leidet noch mehr (weit mehr als unbedingt notwendig), denn sobald ich verwirrt bin, geht es von dort aus nur noch nach unten. Sag es mit mir, bitte…
Winkel 2: Eine neue Art des Denkens
Es gibt einen besseren Weg, und ich werde ihn Ihnen zeigen. Ich werde nicht zu viel Zeit mit den Konzepten von ES6 (auch bekannt als ES2015) verbringen, da es weitaus bessere Orte gibt, um etwas über diese Dinge zu lernen, und wenn Sie einen Ausgangspunkt brauchen, bietet ES6-Features.org einen guten Überblick aller lustigen neuen Funktionen. Betrachten Sie diesen aktualisierten AngularJS 2-Code:
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); } }
Cool! Lassen Sie uns diese Zeile für Zeile durchgehen. Auch hier lässt uns der TypeScript-Transpiler alle gewünschten ES6-Funktionen verwenden, da er alles in Vanilla-JavaScript konvertiert.
Die import
am Anfang verwenden einfach ES6, um die Module zu laden, die wir benötigen. Da ich den größten Teil meiner Entwicklung in ES5 (auch bekannt als reguläres JavaScript) durchführe, muss ich zugeben, dass es ein bisschen nervig ist, plötzlich alle Objekte auflisten zu müssen, die ich verwenden möchte.
Denken Sie jedoch daran, dass TypeScript alles in JavaScript transpiliert und heimlich SystemJS verwendet, um das Laden von Modulen zu handhaben. Die Abhängigkeiten werden alle asynchron geladen, und es ist (angeblich) in der Lage, Ihren Code so zu bündeln, dass Symbole entfernt werden, die Sie nicht importiert haben. Außerdem unterstützt das alles „aggressive Minification“, was sehr schmerzhaft klingt. Diese wichtigen Aussagen sind ein kleiner Preis, den man zahlen muss, um nicht mit all diesem Lärm fertig zu werden.

Abgesehen vom Laden ausgewählter Features aus Angular 2 selbst, beachten Sie auf jeden Fall besonders die Zeile import {Observable} from 'rxjs/Observable';
. RxJS ist eine umwerfende, verrückt-coole reaktive Programmierbibliothek, die einen Teil der Infrastruktur bereitstellt, die Angular 2 zugrunde liegt. Wir werden definitiv später davon hören.
Jetzt kommen wir zu @Injectable()
.
Ehrlich gesagt bin ich mir immer noch nicht ganz sicher, was das bewirkt, aber das Schöne an der deklarativen Programmierung ist, dass wir nicht immer die Details verstehen müssen. Es heißt Decorator und ist ein ausgefallenes TypeScript-Konstrukt, das in der Lage ist, Eigenschaften auf die Klasse (oder ein anderes Objekt) anzuwenden, die ihm folgt. In diesem Fall @Injectable()
unserem Dienst bei, wie er in eine Komponente injiziert wird. Die beste Demonstration kommt direkt aus dem Maul des Pferdes, aber es ist ziemlich lang, also hier ist ein kleiner Vorgeschmack, wie es in unserer AppComponent aussieht:
@Component({ ... providers: [HTTP_PROVIDERS, ..., ZilyoService] })
Als nächstes kommt die Klassendefinition selbst. Es hat eine export
-Anweisung davor, was bedeutet, Sie haben es erraten, dass wir unseren Service in eine andere Datei import
können. In der Praxis importieren wir unseren Dienst wie oben in unsere AppComponent
Komponente.
Direkt danach ist der Konstruktor, wo Sie eine echte Abhängigkeitsinjektion in Aktion sehen können. Die Zeile constructor(private http:Http) {}
fügt eine private Instanzvariable namens http
hinzu, die TypeScript auf magische Weise als Instanz des Http-Dienstes erkennt. Punkt geht an TypeScript!
Danach sind es nur noch ein paar normal aussehende Instanzvariablen und eine Utility-Funktion, bevor wir zum eigentlichen Fleisch und den Kartoffeln kommen, der get
-Funktion. Hier sehen wir Http
in Aktion. Es sieht dem auf Versprechungen basierenden Ansatz von Angular 1 sehr ähnlich, aber unter der Haube ist es viel cooler. Da wir auf RxJS aufbauen, haben wir einige große Vorteile gegenüber Versprechungen:
- Wir können das
Observable
stornieren, wenn uns die Antwort nicht mehr interessiert. Dies könnte der Fall sein, wenn wir ein Autocomplete-Feld mit Textvervollständigung erstellen und uns nicht mehr um die Ergebnisse für „ca“ kümmern, sobald sie „cat“ eingegeben haben. - Das
Observable
kann mehrere Werte ausgeben und der Abonnent wird immer wieder aufgefordert, sie zu konsumieren, während sie produziert werden.
Ersteres ist in vielen Situationen großartig, aber es ist das Zweite, auf das wir uns in unserem neuen Dienst konzentrieren. Gehen wir die get
-Funktion Zeile für Zeile durch:
return this.http.get(this._countUrl, { search: this.parameterize(params) })
Es sieht ziemlich ähnlich aus wie der Promise-basierte HTTP-Aufruf, den Sie in Angular 1 sehen würden. In diesem Fall senden wir die Abfrageparameter, um eine Zählung aller übereinstimmenden Ergebnisse zu erhalten.
.map(this.extractData)
Sobald der AJAX-Aufruf zurückkehrt, sendet er die Antwort über den Stream. Die Methode map
ähnelt konzeptionell der map
-Funktion eines Arrays, verhält sich aber auch wie die then
-Methode eines Promise, da sie darauf wartet, dass alles, was stromaufwärts passiert, abgeschlossen wird, unabhängig von Synchronität oder Asynchronität. In diesem Fall akzeptiert es einfach das Antwortobjekt und neckt die JSON-Daten, um sie stromabwärts zu übergeben. Jetzt haben wir:
.map(results => { if (typeof onCountResults === "function") { onCountResults(results.totalResults); } return results; })
Wir haben noch einen unangenehmen Rückruf, den wir dort einfügen müssen. Sehen Sie, es ist nicht alles Zauberei, aber wir können onCountResults
verarbeiten, sobald der AJAX-Aufruf zurückkehrt, ohne unseren Stream zu verlassen. Das ist nicht so schlecht. Was die nächste Zeile angeht:
.flatMap(results => Observable.range(1, results.totalPages))
Uh oh, kannst du es fühlen? Eine subtile Stille ist über die zuschauende Menge gekommen, und Sie können sagen, dass etwas Großes passieren wird. Was bedeutet diese Zeile überhaupt? Der rechte Teil ist nicht so verrückt. Es erstellt einen RxJS-Bereich, den ich als ein glorifiziertes Observable
-wrapped Array betrachte. Wenn results.totalPages
gleich 5 ist, erhalten Sie am Ende etwas wie Observable.of([1,2,3,4,5])
.
flatMap
ist, warten Sie mal, eine Kombination aus flatten
und map
. Es gibt ein großartiges Video, das das Konzept bei Egghead.io erklärt, aber meine Strategie besteht darin, sich jedes Observable
als Array vorzustellen. Observable.range
erstellt seinen eigenen Wrapper und hinterlässt uns das zweidimensionale Array [[1,2,3,4,5]]
. flatMap
flacht das äußere Array ab und lässt uns [1,2,3,4,5]
zurück, mappt dann einfach map
über das Array und übergibt die Werte einzeln nacheinander. Diese Zeile akzeptiert also eine ganze Zahl ( totalPages
) und konvertiert sie in einen Strom von ganzen Zahlen von 1 bis totalPages
. Es scheint nicht viel zu sein, aber das ist alles, was wir einrichten müssen.
Ich wollte das wirklich auf eine Linie bringen, um seine Wirkung zu verstärken, aber ich schätze, man kann sie nicht alle gewinnen. Hier sehen wir, was mit dem Integer-Stream passiert, den wir in der letzten Zeile eingerichtet haben. Sie fließen nacheinander in diesen Schritt ein und werden dann als Seitenparameter zur Abfrage hinzugefügt, bevor sie schließlich in eine brandneue AJAX-Anforderung verpackt und zum Abrufen einer Seite mit Ergebnissen gesendet werden. Hier ist dieser Code:
.flatMap(i => { return this.http.get(this._searchUrl, { search: this.parameterize(Object.assign({}, params, { page: i })) }); })
Wenn totalPages
5 war, konstruieren wir 5 GET-Anforderungen und senden sie alle gleichzeitig ab. flatMap
abonniert jedes neue Observable
, wenn also die Anfragen (in beliebiger Reihenfolge) zurückkehren, werden sie entpackt und jede Antwort (wie eine Seite mit Ergebnissen) wird einzeln nach unten geschoben.
Schauen wir uns aus einem anderen Blickwinkel an, wie das Ganze funktioniert. Aus unserer ursprünglichen „count“-Anfrage finden wir die Gesamtzahl der Ergebnisseiten. Wir erstellen eine neue AJAX-Anforderung für jede Seite, und egal wann sie zurückkehren (oder in welcher Reihenfolge), sie werden in den Stream geschoben, sobald sie fertig sind. Alles, was unsere Komponente tun muss, ist das Observable zu abonnieren, das von unserer get
-Methode zurückgegeben wird, und sie empfängt jede Seite, eine nach der anderen, alles aus einem einzigen Stream. Nimm das, versprochen.
Danach ist alles ein bisschen enttäuschend:
.map(this.extractData).catch(this.handleError);
Wenn jedes Antwortobjekt von flatMap
ankommt, wird sein JSON auf die gleiche Weise wie die Antwort aus der Zählanforderung extrahiert. Am Ende ist der catch
-Operator angehängt, der hilft zu veranschaulichen, wie die Stream-basierte RxJS-Fehlerbehandlung funktioniert. Es ist dem traditionellen Try/Catch-Paradigma ziemlich ähnlich, außer dass das Observable
-Objekt auch für die asynchrone Fehlerbehandlung funktioniert.
Immer wenn ein Fehler auftritt, rast es stromabwärts und überspringt Operatoren, bis es auf einen Fehlerbehandler trifft. In unserem Fall löst die Methode handleError
den Fehler erneut aus, sodass wir ihn innerhalb des Dienstes abfangen, aber auch den Abonnenten seinen eigenen onError
Callback bereitstellen lassen können, der noch weiter stromabwärts ausgelöst wird. Die Fehlerbehandlung zeigt uns, dass wir unseren Stream nicht voll ausgenutzt haben, trotz all der coolen Dinge, die wir bereits erreicht haben. Es ist trivial, nach unseren HTTP-Anforderungen einen retry
hinzuzufügen, der eine einzelne Anforderung erneut versucht, wenn sie einen Fehler zurückgibt. Als vorbeugende Maßnahme könnten wir auch einen Operator zwischen dem range
und den Anfragen hinzufügen, indem wir eine Art Ratenbegrenzung hinzufügen, damit wir den Server nicht mit zu vielen Anfragen auf einmal spammen.
Recap: Beim Erlernen von Angular 2 geht es nicht nur um ein neues Framework
Das Erlernen von Angular 2 ist eher wie das Treffen mit einer völlig neuen Familie, und einige ihrer Beziehungen sind kompliziert. Hoffentlich ist es mir gelungen zu zeigen, dass sich diese Beziehungen aus einem bestimmten Grund entwickelt haben und dass es viel zu gewinnen gibt, wenn man die Dynamik innerhalb dieses Ökosystems respektiert. Hoffentlich hat Ihnen dieser Artikel auch gefallen, denn ich habe kaum an der Oberfläche gekratzt und es gibt noch viel mehr zu diesem Thema zu sagen.