Angular處理http是使用rx(Reactive Programming)來實作的,類別的名稱為rxjs
在閱讀此篇之前,建議可以了解一下何謂Reactive Programming,其核心概念為何,這樣會比較容易理解本篇的內容
推薦閱讀:Reactive Programming 簡介與教學(以 RxJS 為例)、官網 ReactiveX
RxJS教學:30 天精通 RxJS
使用http來取得api資料
將src/app/hero.service.ts取得資料的方式改由API取得
import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import { Hero } from './hero'; import { HEROES } from './mock-heroes'; import { MessageService } from './message.service'; import { HttpClient, HttpHeaders } from '@angular/common/http'; //加入http類別 @Injectable() export class HeroService { private heroesUrl = 'api/heroes';//設定要讀的api的位置 //於constructor增加private http: HttpClient constructor(private http: HttpClient, private messageService: MessageService) { } /** 改由api取得資料,註解掉舊程式 getHeroes(): Observable<hero[]> { // Todo: send the message _after_ fetching the heroes this.messageService.add('HeroService: fetched heroes'); return of(HEROES); }*/ getHeroes (): Observable<hero[]> { return this.http.get<hero[]>(this.heroesUrl) } getHero(id: number): Observable<hero> { const url = `${this.heroesUrl}/${id}`; return this.http.get<hero>(url); } }
上面程式碼註解地方代表有修改過的地方。
所有的HttpClient方法都返回一個Observable的物件。一般來說,Observable物件會傳送多次資料給接收者,但http.get所取得的值為例外,因為http方法是發出要求並收到回應後就不會再有動作,因此它只會傳一次資料給取得這個資料的接收者。並且需要在被subscribe後才會啟動動作。
http.get預設接收的格式為json,會自動對照所取得的json轉化成一個對應屬性的物件以方便取得資料。
處理錯誤
如果在取得api時發生網路錯誤或其他問題導致無法順利取得伺服器資料時,可以用下面的方法來偵聽錯誤
首先,導入rxjs/operators類別
import { catchError, map, tap } from 'rxjs/operators';
接著,使用pipe方法擴展Observable並在裡面下達catchError()來處理錯誤的狀況
getHeroes (): Observable<hero[]> { return this.http.get<hero[]>(this.heroesUrl) .pipe( catchError(this.handleError('getHeroes', [])) ); }
下面則是catchError的內容
/** * 處理http發生的錯誤,讓程式可以繼續正確的運作而不產生exception * @param operation - 失敗的操作,這邊是getHeroes * @param result - 可不傳入,最後要回傳出去的Observable物件內容,可在裡面塞一些與api連線失敗時要回傳的資料 */ private handleError<t> (operation = 'operation', result?: T) { return (error: any): Observable<t> => { return of(result as T); }; }
在上面的T是類型參數,在這個例子中,T代表Hero[]。這可以讓程式在打api失敗時依舊可取得符合應用程式所期望的類型的回傳值。
使用http修改伺服器資料
取得資料時使用get,而修改資料時使用put,其參數意義如下:
/** 更新伺服器上的資料 */ updateHero (hero: Hero): Observable<any> { // 該HttpClient.put()方法有三個參數:網址、要更新的數據、選項 return this.http.put(this.heroesUrl, hero, httpOptions).pipe( tap(_ => this.log(`updated hero id=${hero.id}`)), catchError(this.handleError<any>('updateHero')) ); }
其中httpOptions較常設定的是headers,也就是宣告傳去的格式
const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) };
下面是使用updateHero的範例
save(): void { this.heroService.updateHero(this.hero) .subscribe(() => this.goBack()); }
在RxJS中,有兩個角色,Observable和Subscription,Observable負責產生資料,創建後不會馬上啟動,而在關注(subscribe)後開始啟動。
使用http新增伺服器資料
新增資料在rxjs裡是使用http.post()
addHero (hero: Hero): Observable<hero> { return this.http.post<hero>(this.heroesUrl, hero, httpOptions).pipe( catchError(this.handleError<hero>('addHero')) ); }
使用addHero方法的範例如下:
add(name: string): void { name = name.trim(); if (!name) { return; } //假如輸入的名稱為空白則不處理 this.heroService.addHero({ name } as Hero) //{ name } as Hero > 代表創建一個Hero,並且其name的值為輸入值,id為空白 .subscribe(hero => { this.heroes.push(hero); // 將所回傳的物件塞回入列表內 }); }
使用http刪除伺服器資料
刪除資料我們使用http.delete(),範例如下
deleteHero (hero: Hero | number): Observable<hero> { const id = typeof hero === 'number' ? hero : hero.id; const url = `${this.heroesUrl}/${id}`; return this.http.delete<hero>(url, httpOptions); }
而呼叫deleteHero的地方則要這樣寫:
delete(hero: Hero): void { this.heroes = this.heroes.filter(h => h !== hero); this.heroService.deleteHero(hero).subscribe(); }
上面我們可以看到在呼叫deleteHero時,即便刪除完成後沒有要多做任何時,仍然需要加上subscribe()
就像上面說過的,所有rxjs的動作都會在有人subscribe後才會呼叫,因此如果忽略subscribe(),http將不會將刪除請求發送到伺服器!
Observable需要等到有東西subscribe它,才會被執行。
使用搜尋功能
於src/app/hero.service.ts增加搜索功能
searchHeroes(term: string): Observable<hero[]> { if (!term.trim()) { // 假如沒有傳值則回傳空資料 return of([]); } return this.http.get<hero[]>(`api/heroes/?name=${term}`).pipe( tap(_ => this.log(`found heroes matching "${term}"`)), catchError(this.handleError<hero[]>('searchHeroes', [])) ); }
創建一個HeroSearchComponent的class
ng generate component hero-search
修改src/app/hero-search/hero-search.component.html
<div id="search-component"> <h4>Hero Search</h4> <input #searchBox id="search-box" (keyup)="search(searchBox.value)" /> <ul class="search-result"> <li *ngFor="let hero of heroes$ | async" > <a routerLink="/detail/{{hero.id}}"> {{hero.name}} </a> </li> </ul> </div>
上面的程式碼中,要注意的是這一行
<li *ngFor="let hero of heroes$ | async" >
如果光使用for迴圈去使用heroes$,Observable不會做任何事,async透過|這個pipe來自動做subscribe的動作,我們可以不用再次的透過subscribe()來讓Observable被執行
而heroes$則是告知這個for迴圈操作的對象是一個Observable而不是一般的值。
接著我們修改src/app/hero-search/hero-search.component.ts
import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; import { of } from 'rxjs/observable/of'; import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; import { Hero } from '../hero'; import { HeroService } from '../hero.service'; @Component({ selector: 'app-hero-search', templateUrl: './hero-search.component.html', styleUrls: [ './hero-search.component.css' ] }) export class HeroSearchComponent implements OnInit { heroes$: Observable<hero[]>; private searchTerms = new Subject<string>(); constructor(private heroService: HeroService) {} // Push a search term into the observable stream. search(term: string): void { this.searchTerms.next(term); } ngOnInit(): void { this.heroes$ = this.searchTerms.pipe( // wait 300ms after each keystroke before considering the term debounceTime(300), // ignore new term if same as previous term distinctUntilChanged(), // switch to new search observable each time the term changes switchMap((term: string) => this.heroService.searchHeroes(term)), ); } }
上面的程式碼中,heroes也需要寫為heroes$並宣告它是一個Observable
heroes$: Observable<hero[]>;
searchTerms在這邊被宣告為RxJS裡的Subject.
private searchTerms = new Subject<string>(); search(term: string): void { this.searchTerms.next(term); }
一個Subject自己本身是observable,並且接收一個observable為參數。因此我們可以對Subject做許多和Observable相同的動作。
也可以使用next(value)去傳值至searchTerms裡,對任何的Observable物件也都能這樣操作。
<input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
在這邊的search()方法與keyup事件綁定,searchTerms會返回一個Observable的觀察結果。
另外searchTerms的寫法
this.heroes$ = this.searchTerms.pipe( // 每次擊鍵後等待300毫秒,然後再搜尋他 debounceTime(300), // 假如與上次的值相同則忽略 distinctUntilChanged(), // 當term變更時更新搜索結果,它取消並丟棄先前的搜索可見性,只返回最新的可見的搜索服務。 switchMap((term: string) => this.heroService.searchHeroes(term)), );
注意:使用switchMap(),每個有資格的鍵事件都可以觸發HttpClient.get()方法調用。即使在請求之間暫停了300毫秒,也可能有多個HTTP請求在運行,並且可能不會按照發送的順序返回。
switchMap()保留最初的請求順序,同時只返回最近的HTTP方法調用的observable。來自先前搜尋的結果被取消並被丟棄。
不過,取消先前的searchHeroes() Observable 並不會中止掛起的HTTP請求。不需要的結果在到達應用程序代碼之前就被丟棄了。
最後成品樣子如下:
完整範例檔請見: live example / download example.