Започнах да искам да напиша ръководство стъпка по стъпка за надграждане на приложение от Angular 1.5 до Angular 2, преди да бъда учтиво информиран от моя редактор, че се нуждае от статия, а не от роман. След дълги размишления, аз приех, че трябва да започна с обширно проучване на промените в Angular 2, удряйки всички точки, обхванати в Jason Aden’s Преминаване на миналия Hello World в Angular 2 статия . ... Ами сега. Продължете и го прочетете, за да получите общ преглед на новите функции на Angular 2, но за практически подход, оставете браузъра си точно тук.
Искам това да се превърне в поредица, която в крайна сметка обхваща целия процес на надстройка на нашето демонстрационно приложение до Angular 2. Засега обаче нека започнем с една услуга. Нека направим една лъкатушеща разходка из кода и ще отговоря на всички въпроси, които имате, като ...
‘О, НЕ ЗАЩО ВСИЧКО Е ТОЛКОВА РАЗЛИЧНО’Ако си като мен, ръководството за бърз старт Angular 2 може би е бил първият път, когато някога сте разглеждали TypeScript. Реално бързо, според собствен уебсайт , TypeScript е „типизиран набор от JavaScript, който се компилира в обикновен JavaScript“. Инсталирате транспилера (подобно на Babel или Traceur) и завършвате с магически език, който поддържа функции на езика ES2015 и ES2016, както и силно писане.
Може да ви успокои да знаете, че нито една от тези тайни настройки не е строго необходима. Не е ужасно трудно напишете Angular 2 код в обикновен стар JavaScript , въпреки че не мисля, че си заслужава да го направя. Хубаво е да разпознаете позната територия, но толкова много от новото и вълнуващото за Angular 2 е по-скоро новият му начин на мислене, отколкото новата му архитектура.
Така че нека разгледаме тази услуга, която надстроих от Angular 1.5 до 2.0.0-beta.17. Това е доста стандартна услуга Angular 1.x, само с няколко интересни функции, които се опитах да отбележа в коментарите. Това е малко по-сложно от вашето стандартно приложение за играчки, но всичко, което всъщност прави, е да запитва Zilyo, a свободно достъпен API който обобщава списъци от доставчици под наем като Airbnb. Съжаляваме, това е доста малко код.
'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) 'Server error'; console.error(errMsg); // log to console instead return errMsg; } // register the service angular.module('angularZilyoApp').factory('zilyoService', zilyoService);
Бръчката в това конкретно приложение е, че показва резултати на карта. Други услуги обработват множество страници с резултати, като прилагат странични или мързеливи скролери, което им позволява да извличат по една чиста страница с резултати наведнъж. Искаме обаче да покажем всички резултати в областта за търсене и искаме те да се появят веднага щом се върнат от сървъра, вместо да се появят внезапно, след като всички страници бъдат заредени. Освен това искаме да показваме актуализации на напредъка на потребителя, така че той да има някаква представа за случващото се.
Свързани: Виталното ръководство за интервюиране с AngularJSЗа да постигнем това в Angular 1.5, прибягваме до обратно извикване. Обещания отведете ни до там, както можете да видите от $q.all
обвивка, която задейства onCompleted
обратно обаждане, но нещата все още стават доста объркани.
След това внасяме лодаш за да създадем всички заявки за страници за нас и всяка заявка е отговорна за изпълнението на onFetchPage
обаждане, за да се уверите, че е добавено към картата веднага щом е налице. Но това се усложнява. Както можете да видите от коментарите, аз се изгубих в собствената си логика и не успях да се справя с това, кое кога се връща на какво обещание.
Цялостната изчистеност на кода страда дори повече (много повече, отколкото е строго необходимо), защото след като се объркам, той само спирала надолу от там. Кажи ми, моля те ...
‘ТРЯБВА ДА ИМА ПО-ДОБЪР НАЧИН’Има по-добър начин и ще ви го покажа. Няма да прекарвам твърде много време на концепциите за ES6 (известен още като ES2015), защото има много по-добри места за научаване на тези неща и ако имате нужда от точка за прескачане, ES6-Features.org има добър преглед на всички забавни нови функции. Помислете за този актуализиран код на AngularJS 2:
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 = 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 }
Готино! Нека да преминем през този ред по ред. Отново, TypeScript преводачът ни позволява да използваме всички функции на ES6, които искаме, защото преобразува всичко във ванилов JavaScript.
import
изявленията в началото просто използват ES6 за зареждане на необходимите ни модули. Тъй като правя по-голямата част от разработките си в ES5 (известен още като обикновен JavaScript), трябва да призная, че е малко досадно изведнъж да започна да изброявам всеки обект, който планирам да използвам.
Имайте предвид обаче, че TypeScript пренася всичко в JavaScript и тайно използва SystemJS за справяне с натоварването на модула. Всички зависимости се зареждат асинхронно и той (уж) може да групира вашия код по начин, който премахва символи, които не сте импортирали. Плюс това всичко поддържа „агресивна минификация“, което звучи много болезнено. Тези декларации за внос са малка цена, която трябва да се плати, за да се избегне справянето с целия този шум.
Както и да е, освен зареждането в селективни функции от самия Angular 2, обърнете специално внимание на линията import {Observable} from 'rxjs/Observable';
. RxJS е умопомрачителна, безумно готина библиотека за реактивно програмиране, която предоставя част от инфраструктурата, лежаща в основата на Angular 2. Определено ще чуем от нея по-късно.
Сега стигаме до @Injectable()
.
Все още не съм напълно сигурен какво прави това, за да бъда честен, но красотата на декларативното програмиране е, че не винаги трябва да разбираме подробностите. Нарича се декоратор, което е изискана конструкция на TypeScript, способна да приложи свойства към класа (или друг обект), който го следва. В този случай, @Injectable()
учи нашата служба как да се инжектира в даден компонент. Най-добрата демонстрация идва направо от устата на коня, но е доста дълга, така че ето един кратък поглед как изглежда в нашия AppComponent:
@Component({ ... providers: [HTTP_PROVIDERS, ..., ZilyoService] })
Следва самото определение на класа. Той има export
изявление преди него, което означава, познахте, можем import
нашата услуга в друг файл. На практика ще импортираме нашата услуга в нашата AppComponent
компонент, както по-горе.
Веднага след това е конструкторът, където можете да видите действително инжектиране на зависимост в действие. Линията constructor(private http:Http) {}
добавя променлива на частен екземпляр, наречена http
че TypeScript магически разпознава като екземпляр на услугата Http. Точката отива към TypeScript!
След това са само някои редовно изглеждащи променливи на екземпляра и полезна функция, преди да стигнем до истинското месо и картофи, get
функция. Тук виждаме Http
в действие. Прилича много на основания на обещания подход на Angular 1, но под капака е доста по-хладно. Изграждането на RxJS означава, че получаваме няколко големи предимства пред обещанията:
Observable
ако вече не ни интересува отговорът. Това може да е така, ако изграждаме поле за автоматично попълване на typeahead и вече не се интересуваме от резултатите за „ca“, след като въведат „cat“.Observable
може да излъчва множество стойности и абонатът ще бъде извикан отново и отново, за да ги консумира, докато се произвеждат.Първият е страхотен при много обстоятелства, но вторият, върху който се фокусираме в новата ни услуга. Нека да преминем през get
функция ред по ред:
return this.http.get(this._countUrl, { search: this.parameterize(params) })
Изглежда доста подобно на HTTP повикване, основано на обещание, което бихте видели в Angular 1. В този случай изпращаме параметрите на заявката, за да получим броя на всички съвпадащи резултати.
.map(this.extractData)
След като повикването AJAX се върне, то ще изпрати отговора надолу по потока. Методът map
е концептуално подобен на масив map
функция, но също така се държи като обещание then
метод, тъй като чака всичко, което се случва нагоре по веригата, да завърши, независимо от синхронността или асинхронността. В този случай той просто приема обекта за отговор и дразни данните JSON, за да ги предаде надолу по веригата. Сега имаме:
.map(results => { if (typeof onCountResults === 'function') { onCountResults(results.totalResults); } return results; })
Все още имаме едно неудобно обратно извикване, което трябва да вмъкнем там. Вижте, не всичко е магия, но можем да обработим onCountResults
веднага щом повикването AJAX се върне, и то без да напуска нашия поток. Това не е лошо. Що се отнася до следващия ред:
.flatMap (резултати => Observable.range (1, results.totalPages))
О, о, можеш ли да го почувстваш? Една фина тишина се появи над гледащата тълпа и можете да разберете, че нещо голямо ще се случи. Какво означава тази линия? Дясната част не е толкова луда. Създава диапазон RxJS, който мисля като прославен Observable
-обвит масив. Ако results.totalPages
е равно на 5, в крайна сметка получавате нещо като Observable.of([1,2,3,4,5])
.
flatMap
е, изчакайте, комбинация от flatten
и map
. Има страхотно видео, обясняващо концепцията на Egghead.io , но моята стратегия е да мисля за всеки Observable
като масив. Observable.range
създава своя собствена обвивка, оставяйки ни с двумерния масив [[1,2,3,4,5]]
flatMap
изравнява външния масив, оставяйки ни с [1,2,3,4,5]
, след това map
просто съпоставя масива, като предава стойностите надолу по верига една по една. Така че този ред приема цяло число (totalPages
) и го преобразува в поток от цели числа от 1 до totalPages
. Може да не изглежда много, но това е всичко, което трябва да настроим.
Наистина исках да разкажа това на една линия, за да увеличи въздействието му, но предполагам, че не можете да ги спечелите всички. Тук виждаме какво се случва с потока от цели числа, който зададохме на последния ред. Те се вливат в тази стъпка един по един, след което се добавят към заявката като параметър на страницата, преди накрая да бъдат пакетирани в чисто нова заявка AJAX и изпратени, за да извлекат страница с резултати. Ето този код:
.flatMap(i => { return this.http.get(this._searchUrl, { search: this.parameterize(Object.assign({}, params, { page: i })) }); })
Ако totalPages
беше 5, конструираме 5 GET заявки и ги изпращаме едновременно. flatMap
се абонира за всеки нов Observable
, така че когато заявките се върнат (в произволен ред), те се разгъват и всеки отговор (като страница с резултати) се изтласква един по един надолу по веригата.
Нека да разгледаме как цялото това нещо работи от друг ъгъл. От нашата първоначална заявка за „преброяване“ намираме общия брой страници с резултати. Създаваме нова AJAX заявка за всяка страница и без значение кога се връщат (или в какъв ред), те се изтласкват в потока веднага щом са готови. Всичко, което трябва да направи нашият компонент, е да се абонира за Observable, върнато от нашите get
метод и той ще получи всяка страница, една след друга, всички от един поток. Вземете това, обещания.
Всичко това е малко антиклиматично след това:
.map(this.extractData).catch(this.handleError);
Тъй като всеки обект на отговор пристига от flatMap
, неговият JSON се извлича по същия начин като отговора от заявката за преброяване. Прикрепен към края има catch
оператор, който помага да се илюстрира как се базира потока RxJS обработка на грешки върши работа. Това е доста подобно на традиционната парадигма try / catch, с изключение на това, че Observable
object работи и за асинхронна обработка на грешки.
Всеки път, когато се срещне грешка, тя се състезава надолу по веригата, прескачайки минали оператори, докато не срещне манипулатор на грешки. В нашия случай handleError
метод повторно изхвърля грешката, позволявайки ни да я прихванем в рамките на услугата, но също така да позволим на абоната да предостави своя собствена onError
обратен разговор, който се задейства още по-надолу по течението. Обработката на грешки ни показва, че не сме се възползвали напълно от потока си, дори с всички страхотни неща, които вече сме постигнали. Тривиално е да добавите retry
оператор след нашите HTTP заявки, който изпробва индивидуална заявка, ако върне грешка. Като превантивна мярка бихме могли да добавим и оператор между range
генератор и заявките, добавяйки някаква форма на ограничаване на скоростта, така че да не спамим сървъра с твърде много заявки наведнъж.
Изучаването на Angular 2 е по-скоро като среща с изцяло ново семейство и някои от отношенията им са сложни. Надяваме се, че успях да докажа, че тези взаимоотношения са се развили по някаква причина и има много да се спечели, като се зачита динамиката, която съществува в тази екосистема. Надяваме се, че и тази статия ви е харесала, защото едвам съм надраскал повърхността и има много още какво да кажа по този въпрос.
Свързани: Всички предимства, без караница: Урок за ъглови 9