Включите Angular 2: обновление с версии 1.5

Опубликовано: 2022-03-11

Сначала я хотел написать пошаговое руководство по обновлению приложения с Angular 1.5 до Angular 2, прежде чем мой редактор вежливо сообщил мне, что ей нужна статья, а не роман. После долгих размышлений я решил, что мне нужно начать с широкого обзора изменений в Angular 2, затронув все моменты, описанные в статье Джейсона Адена «Getting Past Hello World in Angular 2». …Ой. Прочитайте его, чтобы получить обзор новых функций Angular 2, но для практического подхода оставьте свой браузер прямо здесь.

Я хочу, чтобы это стало серией, которая в конечном итоге охватывала бы весь процесс обновления нашего демонстрационного приложения до Angular 2. А пока давайте начнем с одного сервиса. Давайте пройдемся по коду, и я отвечу на любые ваши вопросы, такие как….

"О НЕТ, ПОЧЕМУ ВСЕ ТАК РАЗНОЕ"

Угловой: старый способ

Если вы похожи на меня, краткое руководство по Angular 2, возможно, было первым разом, когда вы смотрели на TypeScript. Очень быстро, согласно его собственному веб-сайту, TypeScript — это «типизированный расширенный набор JavaScript, который компилируется в простой JavaScript». Вы устанавливаете транспайлер (похожий на Babel или Traceur) и получаете волшебный язык, который поддерживает языковые функции ES2015 и ES2016, а также строгую типизацию.

Вы можете найти успокоение, зная, что ни одна из этих тайных установок не является строго необходимой. Написать код Angular 2 на старом добром JavaScript не так уж и сложно, хотя я не думаю, что это того стоит. Приятно узнавать знакомую территорию, но так много нового и интересного в Angular 2 связано с его новым мышлением, а не с новой архитектурой.

В этом посте рассматривается обновление службы до Angular 2 с версии 1.5.

Что нового и интересного в Angular 2, так это его новый образ мышления, а не новая архитектура.
Твитнуть

Итак, давайте посмотрим на этот сервис, который я обновил с Angular 1.5 до 2.0.0-beta.17. Это довольно стандартный сервис Angular 1.x, всего с парой интересных фич, которые я постарался отметить в комментариях. Это немного сложнее, чем ваше стандартное игрушечное приложение, но на самом деле все, что оно делает, — это запросы к Zilyo, свободно доступному API, который собирает списки от поставщиков аренды, таких как Airbnb. Извините, это довольно много кода.

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

Недостаток этого конкретного приложения в том, что оно показывает результаты на карте. Другие сервисы обрабатывают несколько страниц результатов, реализуя нумерацию страниц или ленивые скроллеры, что позволяет им извлекать по одной аккуратной странице результатов за раз. Однако мы хотим показать все результаты в области поиска, и мы хотим, чтобы они появлялись, как только возвращаются с сервера, а не появлялись внезапно после загрузки всех страниц. Кроме того, мы хотим отображать обновления прогресса для пользователя, чтобы он имел некоторое представление о том, что происходит.

Связанный: жизненно важное руководство по собеседованию в AngularJS

Чтобы добиться этого в Angular 1.5, мы прибегаем к обратным вызовам. Обещания частично подводят нас к этому, как вы можете видеть из оболочки $q.all , которая запускает обратный вызов onCompleted , но все равно все становится довольно запутанно.

Затем мы вводим lodash для создания всех запросов страниц для нас, и каждый запрос отвечает за выполнение обратного вызова onFetchPage , чтобы убедиться, что он добавлен на карту, как только он станет доступен. Но это усложняется. Как вы можете видеть из комментариев, я потерялся в своей собственной логике и не мог понять, что и какому промису когда возвращалось.

Общая опрятность кода страдает еще больше (гораздо больше, чем это строго необходимо), потому что, как только я запутался, он только начинает двигаться вниз по спирали. Скажи это со мной, пожалуйста…

'ТАМ ДОЛЖЕН БЫТЬ ЛУЧШИЙ СПОСОБ'

Angular 2: новый способ мышления

Есть лучший способ, и я покажу его вам. Я не собираюсь тратить слишком много времени на концепции ES6 (он же ES2015), потому что есть гораздо лучшие места, где можно узнать об этом, и если вам нужна отправная точка, ES6-Features.org имеет хороший обзор. всех интересных новых функций. Рассмотрим этот обновленный код AngularJS 2:

zilyo.service.ts (2.0.0-бета.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); } }

Прохладный! Давайте пройдемся по этому построчно. Опять же, транспилятор TypeScript позволяет нам использовать любые функции ES6, которые мы хотим, потому что он преобразует все в ванильный JavaScript.

Операторы import в начале просто используют ES6 для загрузки модулей, которые нам нужны. Поскольку я делаю большую часть своей разработки в ES5 (он же обычный JavaScript), я должен признать, что это немного раздражает, когда внезапно нужно начать перечислять каждый объект, который я планирую использовать.

Однако имейте в виду, что TypeScript транспилирует все в JavaScript и тайно использует SystemJS для обработки загрузки модулей. Все зависимости загружаются асинхронно, и он (предположительно) может связать ваш код таким образом, чтобы исключить символы, которые вы не импортировали. Плюс все это поддерживает «агрессивную минификацию», что звучит очень болезненно. Эти заявления об импорте — небольшая цена, которую нужно заплатить, чтобы избежать всего этого шума.

Операторы импорта в Angular многое делают за кулисами.

Заявления об импорте — это небольшая цена за то, что происходит за кулисами.

В любом случае, помимо загрузки отдельных функций из самого Angular 2, обратите особое внимание на строку import {Observable} from 'rxjs/Observable'; . RxJS — умопомрачительная, безумно крутая библиотека реактивного программирования, которая предоставляет часть инфраструктуры, лежащей в основе Angular 2. Мы обязательно услышим о ней позже.

Теперь мы подошли к @Injectable() .

Честно говоря, я до сих пор не совсем уверен, что это делает, но прелесть декларативного программирования в том, что нам не всегда нужно разбираться в деталях. Он называется декоратором и представляет собой причудливую конструкцию TypeScript, способную применять свойства к классу (или другому объекту), следующему за ним. В этом случае @Injectable() обучает наш сервис тому, как внедряться в компонент. Лучшая демонстрация идет прямо из уст лошади, но она довольно длинная, поэтому вот краткий обзор того, как это выглядит в нашем AppComponent:

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

Далее следует само определение класса. Перед ним есть оператор export , что означает, как вы уже догадались, мы можем import наш сервис в другой файл. На практике мы будем импортировать наш сервис в наш компонент AppComponent , как указано выше.

@Injectable() учит наш сервис, как внедряться в компонент.

@Injectable() учит наш сервис, как внедряться в компонент.

Сразу после этого находится конструктор, где вы можете увидеть настоящую инъекцию зависимостей в действии. Конструктор строк constructor(private http:Http) {} добавляет частную переменную экземпляра с именем http , которую TypeScript волшебным образом распознает как экземпляр службы Http. Точка переходит на TypeScript!

После этого это просто обычные переменные экземпляра и служебная функция, прежде чем мы перейдем к настоящему мясу и картошке, функции get . Здесь мы видим Http в действии. Это очень похоже на подход Angular 1, основанный на промисах, но внутри все гораздо круче. То, что мы построены на RxJS, означает, что мы получаем несколько больших преимуществ по сравнению с промисами:

  • Мы можем отменить Observable , если нам больше не нужен ответ. Это может иметь место, если мы создаем поле автозаполнения с опережением ввода и больше не заботимся о результатах для «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 также работает для асинхронной обработки ошибок.

Всякий раз, когда встречается ошибка, она мчится вниз по течению, пропуская предыдущие операторы, пока не встретит обработчик ошибки. В нашем случае метод handleError повторно выдает ошибку, что позволяет нам перехватить ее внутри службы, а также позволяет подписчику предоставить собственный обратный вызов onError , который срабатывает еще ниже по течению. Обработка ошибок показывает нам, что мы не использовали все преимущества нашего потока, даже несмотря на то, что мы уже сделали все самое интересное. Тривиально добавить оператор retry после наших HTTP-запросов, который повторяет отдельный запрос, если он возвращает ошибку. В качестве превентивной меры мы могли бы также добавить оператор между генератором range и запросами, добавив некую форму ограничения скорости, чтобы не загружать сервер слишком большим количеством запросов одновременно.

По теме: наймите 3% лучших внештатных разработчиков AngularJS.

Резюме: изучение Angular 2 — это не только новый фреймворк

Изучение Angular 2 больше похоже на знакомство с совершенно новой семьей, и некоторые из их отношений сложны. Надеюсь, мне удалось продемонстрировать, что эти отношения развивались не просто так, и можно многого добиться, уважая динамику, существующую в этой экосистеме. Надеюсь, вам тоже понравилась эта статья, потому что я только коснулся поверхности, и есть еще много чего сказать по этой теме.

Связанный: Все льготы, никаких хлопот: учебник по Angular 9