開啟 Angular 2:從 1.5 升級

已發表: 2022-03-11

在我的編輯禮貌地告訴我她需要一篇文章而不是小說之前,我開始想寫一個將應用程序從 Angular 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 語言功能以及強類型的神奇語言。

知道這些神秘的設置都不是絕對必要的,您可能會感到欣慰。 用普通的舊 JavaScript 編寫 Angular 2 代碼並不難,儘管我認為這樣做不值得。 很高興認識熟悉的領域,但 Angular 2 的許多新奇和令人興奮的是它的新思維方式,而不是它的新架構。

這篇文章介紹了將服務從 1.5 升級到 Angular 2。

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 中實現這一點,我們求助於回調。 從觸發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 轉譯器允許我們使用我們想要的任何 ES6 功能,因為它將所有內容都轉換為原生 JavaScript。

開頭的import語句只是使用 ES6 加載我們需要的模塊。 由於我的大部分開發工作都是在 ES5(也稱為常規 JavaScript)中完成的,我必須承認突然需要開始列出我計劃使用的每個對像有點煩人。

但是,請記住,TypeScript 正在將所有內容都轉換為 JavaScript,並且正在秘密使用 SystemJS 來處理模塊加載。 依賴項都是異步加載的,並且(據稱)能夠以一種去除您尚未導入的符號的方式捆綁您的代碼。 加上它都支持“aggressive minification”,這聽起來很痛苦。 為了避免處理所有這些噪音,這些導入語句是一個很小的代價。

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 的基於 Promise 的方法,但在底層它更酷。 建立在 RxJS 上意味著我們比 Promise 有幾個很大的優勢:

  • 如果我們不再關心響應,我們可以取消Observable 。 如果我們正在構建一個預先輸入的自動完成字段,並且在他們輸入“cat”後不再關心“ca”的結果,則可能會出現這種情況。
  • Observable可以發出多個值,並且訂閱者將被一遍又一遍地調用以在它們產生時使用它們。

第一個在很多情況下都很棒,但這是我們在新服務中關注的第二個。 讓我們逐行瀏覽get函數:

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

它看起來與您在 Angular 1 中看到的基於 Promise 的 HTTP 調用非常相似。在這種情況下,我們發送查詢參數以獲取所有匹配結果的計數。

 .map(this.extractData)

一旦 AJAX 調用返回,它將沿著流向下發送響應。 map方法在概念上類似於數組的map函數,但它的行為也類似於 Promise 的then方法,因為它等待上游發生的任何事情完成,而不管同步性或異步性如何。 在這種情況下,它只接受響應對象並梳理出 JSON 數據以傳遞到下游。 現在我們有:

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

我們仍然有一個尷尬的回調需要滑入其中。 瞧,這並不全是魔法,但我們可以在 AJAX 調用返回後立即處理onCountResults ,而無需離開我們的流。 這還不錯。 至於下一行:

.flatMap(results => Observable.range(1, results.totalPages))

哦哦,你能感覺到嗎? 圍觀的人群中傳來一陣微妙的安靜,你可以看出有大事要發生了。 這條線甚至意味著什麼? 右手部分沒那麼瘋狂。 它創建了一個 RxJS 範圍,我認為它是一個美化的Observable包裝的數組。 如果results.totalPages等於 5,你最終會得到類似Observable.of([1,2,3,4,5])的東西。

flatMap是,等待它, flattenmap的組合。 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 請求,無論它們何時返回(或以何種順序),一旦它們準備好就會被推送到流中。 我們的組件需要做的就是訂閱我們的get方法返回的 Observable,它將一個接一個地接收來自單個流的每個頁面。 拿那個,承諾。

每個響應一次被推送到下游。

該組件將一個接一個地從單個流中接收每個頁面。

之後就有點反高潮了:

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

當每個響應對像從flatMap到達時,其 JSON 的提取方式與計數請求的響應相同。 附加到最後的是catch運算符,它有助於說明基於流的 RxJS 錯誤處理是如何工作的。 它與傳統的 try/catch 範例非常相似,除了Observable對像也適用於異步錯誤處理。

每當遇到錯誤時,它都會向下游競爭,跳過過去的運算符,直到遇到錯誤處理程序。 在我們的例子中, handleError方法重新拋出錯誤,允許我們在服務中攔截它,但也讓訂閱者提供自己的onError回調,該回調會在下游觸發。 錯誤處理向我們展示了我們沒有充分利用我們的流,即使我們已經完成了所有很酷的事情。 在我們的 HTTP 請求之後添加一個retry運算符很簡單,如果它返回錯誤,它會重試單個請求。 作為預防措施,我們還可以在range生成器和請求之間添加一個運算符,添加某種形式的速率限制,這樣我們就不會一次向服務器發送過多請求的垃圾郵件。

相關:聘請前 3% 的自由 AngularJS 開發人員。

回顧:學習 Angular 2 不僅僅是一個新框架

學習 Angular 2 更像是認識一個全新的家庭,他們的一些關係很複雜。 希望我已經成功地證明了這些關係的演變是有原因的,尊重這個生態系統中存在的動態會有很多好處。 希望您也喜歡這篇文章,因為我幾乎沒有觸及到表面,關於這個主題還有很多話要說。

相關:所有特權,無後顧之憂:Angular 9 教程