Angular 2 켜기: 1.5에서 업그레이드
게시 됨: 2022-03-11Angular 1.5에서 Angular 2로 앱을 업그레이드하기 위한 단계별 가이드를 작성하고 싶었습니다. 편집자로부터 소설보다 기사가 필요하다는 정중한 정보를 받기 전이었습니다. 많은 숙고 끝에 Angular 2의 변경 사항에 대한 광범위한 조사부터 시작해야 하며 Jason Aden의 Angular 2에서 Hello World 살펴보기 기사에서 다룬 모든 요점에 도달해야 한다는 점을 인정했습니다. …앗. Angular 2의 새로운 기능에 대한 개요를 보려면 이 문서를 읽으십시오. 그러나 실습 접근을 위해서는 브라우저를 여기에서 유지하십시오.
데모 앱을 Angular 2로 업그레이드하는 전체 프로세스를 궁극적으로 포함하는 시리즈가 되기를 바랍니다. 하지만 지금은 단일 서비스로 시작하겠습니다. 코드를 천천히 살펴보고 다음과 같은 질문에 답변해 드리겠습니다.
Angular: 예전 방식
나와 같다면 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 서비스이며, 주석에서 언급하려고 했던 몇 가지 흥미로운 기능이 있습니다. 표준 장난감 응용 프로그램보다 약간 더 복잡하지만 실제로 하는 일은 Airbnb와 같은 임대 제공업체의 목록을 집계하는 무료 API인 Zilyo를 쿼리하는 것입니다. 죄송합니다. 꽤 많은 코드입니다.
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);
이 특정 앱의 주름은 지도에 결과를 표시한다는 것입니다. 다른 서비스는 페이지 매김 또는 지연 스크롤러를 구현하여 결과의 여러 페이지를 처리하므로 한 번에 하나의 깔끔한 결과 페이지를 검색할 수 있습니다. 그러나 우리는 검색 영역 내의 모든 결과를 표시하고 모든 페이지가 로드된 후 갑자기 표시되지 않고 서버에서 반환되는 즉시 표시되기를 원합니다. 또한 사용자에게 진행 상황 업데이트를 표시하여 사용자가 무슨 일이 일어나고 있는지 알 수 있도록 하고 싶습니다.
Angular 1.5에서 이를 수행하기 위해 우리는 콜백에 의존합니다. onCompleted
콜백을 트리거하는 $q.all
래퍼에서 볼 수 있듯이 Promise는 우리를 중간에 데려다 주지만 상황은 여전히 매우 복잡해집니다.
그런 다음 lodash를 가져와서 모든 페이지 요청을 생성하고 각 요청은 onFetchPage
콜백을 실행하여 사용 가능한 즉시 지도에 추가되도록 합니다. 그러나 그것은 복잡해집니다. 댓글에서 알 수 있듯이 나는 내 논리에 빠져 언제 어떤 약속으로 반환되는지 처리할 수 없었습니다.
코드의 전반적인 깔끔함은 훨씬 더(꼭 필요한 것보다 훨씬 더 많이) 문제가 됩니다. 한 번 혼란스러워지면 거기에서 아래로 나선형으로만 흐르기 때문입니다. 저와 함께 말씀해주세요...
Angular 2: 새로운 사고 방식
더 좋은 방법이 있어서 보여드리겠습니다. 나는 ES6(일명 ES2015) 개념에 대해 너무 많은 시간을 할애하지 않을 것입니다. 왜냐하면 그것에 대해 배울 수 있는 훨씬 더 좋은 곳이 있기 때문입니다. 그리고 시작점이 필요한 경우 ES6-Features.org에 좋은 개요가 있습니다. 모든 재미있는 새 기능의. 이 업데이트된 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); } }
멋있는! 이 라인을 따라 가자. 다시 말하지만 TypeScript 변환기를 사용하면 모든 것을 바닐라 JavaScript로 변환하기 때문에 원하는 ES6 기능을 사용할 수 있습니다.

처음에 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) {}
는 TypeScript가 마술처럼 Http 서비스의 인스턴스로 인식하는 http
라는 비공개 인스턴스 변수를 추가합니다. 포인트는 TypeScript로 이동합니다!
그 후에는 실제 고기와 감자인 get
함수에 도달하기 전에 일반 인스턴스 변수와 유틸리티 함수일 뿐입니다. 여기에서 Http
가 작동하는 것을 볼 수 있습니다. Angular 1의 약속 기반 접근 방식과 많이 비슷해 보이지만 내부적으로는 훨씬 더 시원합니다. RxJS를 기반으로 구축된다는 것은 약속보다 몇 가지 큰 이점을 얻을 수 있음을 의미합니다.
- 응답에 더 이상 신경 쓰지 않는다면
Observable
을 취소할 수 있습니다. 이것은 자동 완성 필드를 구축하고 "cat"을 입력하면 "ca"에 대한 결과에 더 이상 신경 쓰지 않는 경우일 수 있습니다. -
Observable
은 여러 값을 방출할 수 있으며 구독자는 생성되는 대로 소비하기 위해 계속해서 호출됩니다.
첫 번째는 많은 상황에서 훌륭하지만 우리가 새로운 서비스에서 집중하고 있는 것은 두 번째입니다. get
함수를 한 줄씩 살펴보겠습니다.
return this.http.get(this._countUrl, { search: this.parameterize(params) })
Angular 1에서 볼 수 있는 약속 기반 HTTP 호출과 매우 유사해 보입니다. 이 경우 쿼리 매개변수를 전송하여 일치하는 모든 결과의 수를 얻습니다.
.map(this.extractData)
AJAX 호출이 반환되면 스트림을 통해 응답을 보냅니다. 메서드 map
은 개념적으로 배열의 map
기능과 유사하지만 동기성 또는 비동기성에 관계없이 업스트림에서 발생한 모든 것이 완료되기를 기다리기 때문에 약속의 then
메서드처럼 작동합니다. 이 경우 단순히 응답 객체를 수락하고 JSON 데이터를 추출하여 다운스트림으로 전달합니다. 이제 다음이 있습니다.
.map(results => { if (typeof onCountResults === "function") { onCountResults(results.totalResults); } return results; })
거기에 밀어 넣어야 하는 어색한 콜백이 아직 하나 있습니다. 모든 것이 마술은 아니지만 AJAX 호출이 반환되는 즉시 스트림을 떠나지 않고 onCountResults
를 처리할 수 있습니다. 나쁘지 않아. 다음 줄에 관해서는:
.flatMap(results => Observable.range(1, results.totalPages))
어, 느껴지나요? 구경하는 군중 위에 미묘한 침묵이 흘렀고 큰 일이 곧 일어날 것임을 알 수 있습니다. 이 줄은 무엇을 의미합니까? 오른쪽 부분은 그렇게 미친 것이 아닙니다. 그것은 내가 영광스러운 Observable
-wrapped 배열로 생각하는 RxJS 범위를 생성합니다. results.totalPages
가 5이면 Observable.of([1,2,3,4,5])
와 같은 결과를 얻게 됩니다.
flatMap
은 flatten
과 map
의 조합입니다. Egghead.io에 개념을 설명하는 훌륭한 비디오가 있지만 제 전략은 모든 Observable
을 배열로 생각하는 것입니다. Observable.range
는 자체 래퍼를 생성하여 2차원 배열 [[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
을 구독하므로 요청이 (순서에 관계없이) 반환되면 래핑이 해제되고 각 응답(예: 결과 페이지)은 한 번에 하나씩 다운스트림으로 푸시됩니다.
이 모든 것이 다른 각도에서 어떻게 작동하는지 봅시다. 원래 "count" 요청에서 결과의 총 페이지 수를 찾습니다. 우리는 각 페이지에 대해 새로운 AJAX 요청을 생성하고 언제(또는 어떤 순서로) 반환되는지에 관계없이 준비되는 즉시 스트림으로 푸시됩니다. 컴포넌트가 해야 할 일은 get
메소드에서 반환된 Observable을 구독하는 것뿐이며 단일 스트림에서 각 페이지를 차례로 수신합니다. 약속하세요.
그 후에는 모두 약간 반감정적입니다.
.map(this.extractData).catch(this.handleError);
각 응답 객체가 flatMap
에서 도착하면 해당 JSON이 count 요청의 응답과 동일한 방식으로 추출됩니다. 끝에는 스트림 기반 RxJS 오류 처리가 작동하는 방식을 설명하는 데 도움이 되는 catch
연산자가 있습니다. Observable
개체가 비동기 오류 처리에도 작동한다는 점을 제외하면 기존의 try/catch 패러다임과 매우 유사합니다.
오류가 발생할 때마다 다운스트림으로 경주하며 오류 처리기를 만날 때까지 과거 연산자를 건너뜁니다. 우리의 경우 handleError
메소드는 오류를 다시 발생시켜 서비스 내에서 오류를 가로챌 수 있을 뿐만 아니라 구독자가 다운스트림을 더 많이 발생시키는 자체 onError
콜백을 제공할 수 있도록 합니다. 오류 처리는 우리가 이미 달성한 모든 멋진 일에도 불구하고 스트림을 최대한 활용하지 않았음을 보여줍니다. HTTP 요청 뒤에 retry
도 연산자를 추가하는 것은 간단합니다. 이 연산자는 오류를 반환하면 개별 요청을 재시도합니다. 예방 조치로 range
생성기와 요청 사이에 연산자를 추가하여 한 번에 너무 많은 요청으로 서버를 스팸하지 않도록 일종의 속도 제한을 추가할 수도 있습니다.
요약: Angular 2를 배우는 것은 새로운 프레임워크에 관한 것이 아닙니다.
Angular 2를 배우는 것은 완전히 새로운 가족을 만나는 것과 같으며 그들의 관계 중 일부는 복잡합니다. 바라건대 저는 이러한 관계가 이유가 있고 이 생태계 내에 존재하는 역학을 존중함으로써 얻을 수 있는 것이 많다는 것을 증명할 수 있었습니다. 이 기사도 재미있게 읽으셨기를 바랍니다. 저는 거의 표면을 긁지 못했고 이 주제에 대해 할 말이 더 많기 때문입니다.