Włącz Angulara 2: Aktualizacja z 1,5

Opublikowany: 2022-03-11

Zacząłem od napisania przewodnika krok po kroku, jak zaktualizować aplikację z Angular 1.5 do Angular 2, zanim zostałem uprzejmie poinformowany przez moją redaktorkę, że potrzebuje artykułu, a nie powieści. Po długich rozważaniach zgodziłem się, że muszę zacząć od szerokiego przeglądu zmian w Angular 2, trafiając we wszystkie punkty omówione w artykule Jasona Adena Getting Past Hello World w Angular 2. …Ups. Śmiało i przeczytaj go, aby uzyskać przegląd nowych funkcji Angular 2, ale dla praktycznego podejścia trzymaj swoją przeglądarkę tutaj.

Chcę, aby stało się to serią, która ostatecznie obejmuje cały proces uaktualniania naszej aplikacji demonstracyjnej do Angular 2. Na razie jednak zacznijmy od jednej usługi. Zróbmy kręty spacer po kodzie, a odpowiem na wszelkie pytania, które możesz mieć, takie jak….

'OH NIE DLACZEGO WSZYSTKO JEST TAK INNE'

Angular: stara droga

Jeśli jesteś podobny do mnie, przewodnik szybkiego startu do Angulara 2 mógł być pierwszym, kiedy spojrzałeś na TypeScript. Według własnej strony internetowej, TypeScript to „nadzbiór kodu JavaScript, który kompiluje się do zwykłego kodu JavaScript”. Instalujesz transpiler (podobny do Babel lub Traceur) i otrzymujesz magiczny język, który obsługuje funkcje językowe ES2015 i ES2016, a także silne pisanie.

Może się okazać, że to uspokajające, że żadna z tych tajemnych konfiguracji nie jest absolutnie konieczna. Pisanie kodu Angulara 2 w zwykłym, starym JavaScript nie jest strasznie trudne, chociaż nie wydaje mi się, żeby było warto to robić. Fajnie jest rozpoznać znajome terytorium, ale wiele z tego, co nowego i ekscytującego w Angular 2, to nowy sposób myślenia, a nie nowa architektura.

Ten post opisuje aktualizację usługi do Angular 2 z wersji 1.5.

To, co nowego i ekscytującego w Angular 2, to nowy sposób myślenia, a nie nowa architektura.
Ćwierkać

Spójrzmy więc na tę usługę, którą zaktualizowałem z Angular 1.5 do 2.0.0-beta.17. Jest to dość standardowa usługa Angulara 1.x, z kilkoma interesującymi funkcjami, które starałem się odnotować w komentarzach. Jest to nieco bardziej skomplikowane niż standardowa aplikacja z zabawkami, ale tak naprawdę polega na wysyłaniu zapytań do Zilyo, bezpłatnego interfejsu API, który agreguje oferty od wypożyczalni, takich jak Airbnb. Przepraszam, to całkiem sporo kodu.

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);

Zmarszczka w tej konkretnej aplikacji polega na tym, że pokazuje wyniki na mapie. Inne usługi obsługują wiele stron wyników, implementując paginację lub leniwe przewijanie, co pozwala im pobierać jedną zgrabną stronę wyników na raz. Chcemy jednak pokazywać wszystkie wyniki w obszarze wyszukiwania i chcemy, aby pojawiały się natychmiast po powrocie z serwera, a nie nagle pojawiały się po załadowaniu wszystkich stron. Dodatkowo chcemy wyświetlać użytkownikowi aktualizacje postępu, aby miał pojęcie o tym, co się dzieje.

Powiązane: Vital Guide to AngularJS Wywiady

Aby to osiągnąć w Angularze 1.5, korzystamy z wywołań zwrotnych. Obietnice prowadzą nas tam częściowo, jak widać z opakowania $q.all , które uruchamia wywołanie zwrotne onCompleted , ale nadal jest dość bałagan.

Następnie wprowadzamy lodash, aby utworzyć dla nas wszystkie żądania stron, a każde żądanie jest odpowiedzialne za wykonanie wywołania zwrotnego onFetchPage , aby upewnić się, że zostanie dodane do mapy, gdy tylko będzie dostępne. Ale to się komplikuje. Jak widać z komentarzy, pogubiłem się we własnej logice i nie mogłem pojąć, co jest zwracane do której obietnicy i kiedy.

Ogólna schludność kodu cierpi jeszcze bardziej (o wiele bardziej niż jest to bezwzględnie konieczne), ponieważ gdy jestem zdezorientowany, zaczyna się on tylko spiralą w dół. Powiedz to ze mną, proszę…

„TAM MUSI BYĆ LEPSZY SPOSÓB”

Angular 2: Nowy sposób myślenia

Jest lepszy sposób i zamierzam ci go pokazać. Nie zamierzam spędzać zbyt dużo czasu na koncepcji ES6 (aka ES2015), ponieważ są o wiele lepsze miejsca, w których można się o tym dowiedzieć, a jeśli potrzebujesz punktu wyjścia, ES6-Features.org ma dobry przegląd wszystkich zabawnych nowych funkcji. Rozważ ten zaktualizowany kod AngularJS 2:

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); } }

Fajny! Przejdźmy przez ten wiersz po wierszu. Ponownie, transpiler TypeScript pozwala nam używać dowolnych funkcji ES6, ponieważ konwertuje wszystko do waniliowego JavaScript.

Instrukcje import na początku po prostu używają ES6 do ładowania modułów, których potrzebujemy. Ponieważ większość prac programistycznych zajmuję się w ES5 (czyli zwykłym JavaScript), muszę przyznać, że to trochę irytujące, gdy nagle muszę zacząć wypisywać każdy obiekt, którego planuję użyć.

Należy jednak pamiętać, że TypeScript transpiluje wszystko do JavaScript i potajemnie używa SystemJS do obsługi ładowania modułów. Wszystkie zależności są ładowane asynchronicznie i (rzekomo) jest w stanie spakować twój kod w sposób, który usuwa symbole, których nie zaimportowałeś. Dodatkowo to wszystko wspiera „agresywną minifikację”, co brzmi bardzo boleśnie. Te deklaracje importowe to niewielka cena, jaką trzeba zapłacić, aby uniknąć radzenia sobie z całym tym hałasem.

Wyrażenia importu w Angularze robią wiele za kulisami.

Oświadczenia importowe to niewielka cena za to, co dzieje się za kulisami.

W każdym razie, poza ładowaniem selektywnych funkcji z samego Angulara 2, zwróć szczególną uwagę na linię import {Observable} from 'rxjs/Observable'; . RxJS to niesamowita, zwariowana reaktywna biblioteka programistyczna, która zapewnia część infrastruktury leżącej u podstaw Angulara 2. Na pewno usłyszymy o niej później.

Teraz przechodzimy do @Injectable() .

Szczerze mówiąc, nadal nie jestem do końca pewien, co to oznacza, ale piękno programowania deklaratywnego polega na tym, że nie zawsze musimy rozumieć szczegóły. Nazywa się to dekoratorem, który jest fantazyjną konstrukcją TypeScript, która może zastosować właściwości do klasy (lub innego obiektu), który następuje po nim. W tym przypadku @Injectable() uczy naszą usługę, jak zostać wstrzykniętym do komponentu. Najlepsza demonstracja pochodzi prosto z pyska konia, ale jest dość długa, więc oto zapowiedź tego, jak to wygląda w naszym AppComponent:

 @Component({ ... providers: [HTTP_PROVIDERS, ..., ZilyoService] })

Następna jest sama definicja klasy. Przed nim znajduje się instrukcja export , co oznacza, że ​​zgadłeś, że możemy import naszą usługę do innego pliku. W praktyce będziemy importować naszą usługę do naszego komponentu AppComponent , jak powyżej.

@Injectable() uczy naszą usługę, jak zostać wstrzykniętym do komponentu.

@Injectable() uczy naszą usługę, jak zostać wstrzykniętym do komponentu.

Zaraz za nim jest konstruktor, w którym można zobaczyć prawdziwy wstrzykiwanie zależności w działaniu. Konstruktor linii constructor(private http:Http) {} dodaje prywatną zmienną instancji o nazwie http , którą TypeScript magicznie rozpoznaje jako instancję usługi HTTP. Punkt idzie do TypeScript!

Potem jest tylko kilka normalnie wyglądających zmiennych instancji i funkcja użyteczności, zanim przejdziemy do prawdziwego mięsa i ziemniaków, funkcji get . Tutaj widzimy Http w akcji. Wygląda bardzo podobnie do opartego na obietnicach podejścia Angulara 1, ale pod maską jest o wiele fajniejszy. Zbudowanie na RxJS oznacza, że ​​otrzymujemy kilka dużych przewag nad obietnicami:

  • Możemy anulować Observable , jeśli nie zależy nam już na odpowiedzi. Może tak być w przypadku, gdy budujemy pole autouzupełniania z wyprzedzeniem i nie zależy nam już na wynikach dla „ca” po wpisaniu „cat”.
  • Observable może emitować wiele wartości, a subskrybent będzie wielokrotnie wywoływany, aby je wykorzystać w miarę ich wytwarzania.

Pierwsza z nich jest świetna w wielu okolicznościach, ale jest to druga, na której skupiamy się w naszej nowej usłudze. Przejdźmy przez funkcję get linijka po linijce:

 return this.http.get(this._countUrl, { search: this.parameterize(params) })

Wygląda bardzo podobnie do wywołania HTTP opartego na obietnicach, które można zobaczyć w Angular 1. W tym przypadku wysyłamy parametry zapytania, aby uzyskać liczbę wszystkich pasujących wyników.

 .map(this.extractData)

Gdy wywołanie AJAX powróci, wyśle ​​odpowiedź w dół strumienia. map metod jest koncepcyjnie podobna do funkcji map tablicy, ale zachowuje się również jak metoda then obietnicy, ponieważ czeka na zakończenie wszystkiego, co dzieje się wcześniej, niezależnie od synchroniczności lub asynchroniczności. W takim przypadku po prostu akceptuje obiekt odpowiedzi i udostępnia dane JSON w celu przekazania dalej. Teraz mamy:

 .map(results => { if (typeof onCountResults === "function") { onCountResults(results.totalResults); } return results; })

Nadal mamy jedno niezręczne wywołanie zwrotne, które musimy tam wsunąć. Widzisz, to nie wszystko magia, ale możemy przetwarzać onCountResults , gdy tylko wywołanie AJAX powróci, a wszystko to bez opuszczania naszego strumienia. Nie aż tak źle. Co do następnej linii:

.flatMap(wyniki => Obserwowalny.zakres(1, wyniki.suma stron))

Och, czujesz to? W tłumie zapadła subtelna cisza i można powiedzieć, że wydarzy się coś ważnego. Co w ogóle oznacza ta linia? Prawa część nie jest aż tak szalona. Tworzy zakres Observable , który uważam za uwielbioną tablicę owiniętą w obserwowalną. Jeśli results.totalPages równa się 5, otrzymujesz coś takiego jak Observable.of([1,2,3,4,5]) .

flatMap jest, czekaj na to, połączeniem flatten i map . Jest świetny film wyjaśniający koncepcję na Egghead.io, ale moją strategią jest myślenie o każdym Observable jako o tablicy. Observable.range tworzy własne opakowanie, pozostawiając nam dwuwymiarową tablicę [[1,2,3,4,5]] . flatMap spłaszcza zewnętrzną tablicę, pozostawiając nam [1,2,3,4,5] , a następnie map po prostu odwzorowuje tablicę, przekazując wartości w dół po jednej na raz. Tak więc ten wiersz akceptuje liczbę całkowitą ( totalPages ) i konwertuje ją na strumień liczb całkowitych od 1 do totalPages . Może się wydawać, że to niewiele, ale to wszystko, co musimy skonfigurować.

PRESTIŻ

Naprawdę chciałem umieścić to w jednej linii, aby zwiększyć jego wpływ, ale myślę, że nie możesz wygrać ich wszystkich. Tutaj widzimy, co dzieje się ze strumieniem liczb całkowitych, który ustawiliśmy w ostatnim wierszu. Przechodzą one do tego kroku jeden po drugim, a następnie są dodawane do zapytania jako parametr strony, a następnie są pakowane w zupełnie nowe żądanie AJAX i wysyłane w celu pobrania strony z wynikami. Oto ten kod:

 .flatMap(i => { return this.http.get(this._searchUrl, { search: this.parameterize(Object.assign({}, params, { page: i })) }); })

Jeśli totalPages wynosi 5, tworzymy 5 żądań GET i wysyłamy je wszystkie jednocześnie. flatMap subskrybuje każdy nowy Observable , więc gdy żądania powracają (w dowolnej kolejności), są one rozpakowywane, a każda odpowiedź (jak strona wyników) jest przesyłana w dół, pojedynczo.

Spójrzmy, jak to wszystko działa pod innym kątem. Na podstawie naszego początkowego żądania „liczba” znajdujemy całkowitą liczbę stron wyników. Tworzymy nowe żądanie AJAX dla każdej strony i bez względu na to, kiedy wracają (lub w jakiej kolejności), są wypychane do strumienia, gdy tylko są gotowe. Wszystko, co musi zrobić nasz komponent, to zasubskrybować Observable zwrócone przez naszą metodę get i otrzyma każdą stronę, jedną po drugiej, z jednego strumienia. Weź to, obiecuje.

Każda odpowiedź jest przesyłana pojedynczo w dół.

Komponent otrzyma każdą stronę, jedną po drugiej, z jednego strumienia.

Po tym wszystko jest trochę antyklimatyczne:

 .map(this.extractData).catch(this.handleError);

Gdy każdy obiekt odpowiedzi przychodzi z flatMap , jego JSON jest wyodrębniany w taki sam sposób, jak odpowiedź z żądania count. Na końcu dołączony jest operator catch , który pomaga zilustrować, jak działa obsługa błędów RxJS oparta na strumieniu. Jest bardzo podobny do tradycyjnego paradygmatu try/catch, z tą różnicą, że obiekt Observable działa również w przypadku asynchronicznej obsługi błędów.

Za każdym razem, gdy napotkany zostanie błąd, pędzi w dół, omijając operatory, aż napotka program obsługi błędów. W naszym przypadku metoda handleError ponownie zgłasza błąd, co pozwala nam przechwycić go w ramach usługi, ale także pozwolić subskrybentowi na dostarczenie własnego wywołania zwrotnego onError , które jest uruchamiane jeszcze dalej. Obsługa błędów pokazuje nam, że nie wykorzystaliśmy w pełni naszego streamu, nawet ze wszystkimi fajnymi rzeczami, które już zrobiliśmy. Dodanie operatora retry po naszych żądaniach HTTP jest trywialne, który ponawia próbę pojedynczego żądania, jeśli zwróci błąd. Jako środek zapobiegawczy, moglibyśmy również dodać operator między generatorem range a żądaniami, dodając pewną formę ograniczenia szybkości, aby nie spamować serwera zbyt wieloma żądaniami naraz.

Powiązane: Zatrudnij najlepszych 3% niezależnych programistów AngularJS.

Podsumowanie: nauka Angulara 2 to nie tylko nowy framework

Nauka Angulara 2 przypomina bardziej poznawanie zupełnie nowej rodziny, a niektóre z ich relacji są skomplikowane. Mam nadzieję, że udało mi się wykazać, że te relacje ewoluowały z jakiegoś powodu i można wiele zyskać, szanując dynamikę, która istnieje w tym ekosystemie. Mam nadzieję, że podobał Ci się również ten artykuł, ponieważ ledwo dotknąłem powierzchni, a na ten temat jest o wiele więcej do powiedzenia.

Powiązane: Wszystkie korzyści, bez kłopotów: samouczek Angular 9