【響應式程式設計的思維藝術】 (5)Angular中Rxjs的應用示例
本文是 【Rxjs 響應式程式設計-第四章 構建完整的Web應用程式】 這篇文章的學習筆記。
示例程式碼託管在: http://www.github.com/dashnowords/blogs
部落格園地址: 《大史住在大前端》原創博文目錄
華為雲社群地址: 【你要的前端打怪升級指南】
一. 劃重點
-
RxJS-DOM
原文示例中使用這個庫進行DOM操作,筆者看了一下 github倉庫 ,400多星,而且相關的資料很少,所以建議理解思路即可,至於生產環境的使用還是三思吧。開發中
Rxjs
幾乎預設是和Angular
技術棧繫結在一起的,筆者最近正在使用ionic3
進行開發,本篇將對基本使用方法進行演示。 -
冷熱Observable
javascript
-
涉及的運算子
bufferWithTime(time:number)
-每隔指定時間將流中的資料以陣列形式推送出去。pluck(prop:string)
- 操作符,提取物件屬性值,是一個柯里化後的函式,只接受一個引數。
二. Angular應用中的Http請求
Angular
應用中基本HTTP請求的方式:
import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { MessageService } from './message.service';//某個自定義的服務 import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; @Injectable({ providedIn: 'root' }) export class HeroService { private localhost = 'http://localhost:3001'; private all_hero_api = this.localhost + '/hero/all';//查詢所有英雄 private query_hero_api = this.localhost + '/hero/query';//查詢指定英雄 constructor(private http:HttpClient) { } /*一般get請求*/ getHeroes(): Observable<HttpResponse<Hero[]>>{ return this.http.get<Hero[]>(this.all_hero_api,{observe:'response'}); } /*帶引數的get請求*/ getHero(id: number): Observable<HttpResponse<Hero>>{ let params = new HttpParams(); params.set('id', id+''); return this.http.get<Hero>(this.query_hero_api,{params:params,observe:'response'}); } /*帶請求體的post請求,any可以自定義響應體格式*/ createHero(newhero: object): Observable<HttpResponse<any>>{ return this.http.post<HttpResponse<any>>(this.create_hero_api,{data:newhero},{observe:'response'}); } }
在 express
中寫一些用於測試的虛擬資料:
var express = require('express'); var router = express.Router(); /* GET home page. */ router.get('/all', function(req, res, next) { let heroes = [{ index:1, name:'Thor', hero:'God of Thunder' },{ index:2, name:'Tony', hero:'Iron Man' },{ index:3, name:'Natasha', hero:'Black Widow' }] res.send({ data:heroes, result:true }) }); /* GET home page. */ router.get('/query', function(req, res, next) { console.log(req.query); let hero= { index:4, name:'Steve', hero:'Captain America' } res.send({ data:hero, result:true }) }); /* GET home page. */ router.post('/create', function(req, res, next) { console.log(req.body); let newhero = { index:5, name:req.body.name, hero:'New Hero' } res.send({ data:newhero, result:true }) }); module.exports = router;
在元件中呼叫上面定義的方法:
sendGet(){ this.heroService.getHeroes().subscribe(resp=>{ console.log('響應資訊:',resp); console.log('響應體:',resp.body['data']); }) } sendQuery(){ this.heroService.getHero(1).subscribe(resp=>{ console.log('響應資訊:',resp); console.log('響應體:',resp.body['data']); }) } sendPost(){ this.heroService.createHero({name:'Dash'}).subscribe(resp=>{ console.log('響應資訊:',resp); console.log('響應體:',resp.body['data']); }) }
控制檯列印的資訊可以看到後臺的虛擬資料已經被請求到了:
三. 使用Rxjs構建Http請求結果的處理管道
3.1 基本示例
儘管看起來 Http
請求的返回結果是一個可觀測物件,但是它卻沒有 map
方法,當需要對 http
請求返回的可觀測物件進行操作時,可以使用 pipe
操作符來實現:
import { Observable, of, from} from 'rxjs'; import { map , tap, filter, flatMap }from 'rxjs/operators'; /*構建一個模擬的結果處理管道 *map操作來獲取資料 *tap實現日誌 *flatMap實現結果自動遍歷 *filter實現結果過濾 */ getHeroes$(): Observable<HttpResponse<Hero[]>>{ return this.http.get<Hero[]>(this.all_hero_api,{observe:'response'}) .pipe( map(resp=>resp.body['data']), tap(this.log), flatMap((data)=>{return from(data)}), filter((data)=>data['index'] > 1) ); }
很熟悉吧?經過處理管道後,一次響應中的結果資料被轉換為逐個發出的資料,並過濾掉了不符合條件的項:
3.2 常見的操作符
Angular
中文網列舉了最常用的一些操作符, RxJS官方文件 有非常詳細的示例及說明,且均配有形象的大理石圖,建議先整體瀏覽一下有個印象,有需要的讀者可以每天熟悉幾個,很快就能上手,運算子的使用稍顯抽象,且不同運算子的組合使用在流程控制和資料處理方面的用法靈活多變,也是有很多 套路 的,開發經驗需要慢慢積累。
四. 冷熱Observable的兩種典型場景
原文中提到的冷熱Observable的差別可以參考這篇文章 【RxJS:冷熱模式的比較】 ,概念本身並不難理解。
4.1 shareReplay與請求快取
開發中常會遇到這樣一種場景,某些集合型的常量,完全是可以複用的,通常開發者會將其進行快取至某個全域性單例中,接著在優化階段,通過增加一個 if
判斷在請求之前先檢查快取再決定是否需要請求, Rxjs
提供了一種更優雅的實現。
先回顧一下上面的 http
請求程式碼:
getHeroes(): Observable<HttpResponse<Hero[]>>{ return this.http.get<Hero[]>(this.all_hero_api,{observe:'response'}); }
http
請求預設返回一個 冷Observable
,每當返回的流被訂閱時就會觸發一個新的 http
請求, Rxjs
中通過 shareReplay( )
操作符將一個可觀測物件轉換為 熱Observable
(注意: shareReplay( )
不是唯一一種可以加熱 Observable
的方法),這樣在第一次被訂閱時,網路請求被髮出並進行了快取,之後再有其他訂閱者加入時,就會得到之前快取的資料,運算子的名稱已經很清晰了,【share-共享】,【replay-重播】,是不是形象又好記。對上面的流進行一下轉換:
getHeroes$(): Observable<HttpResponse<Hero[]>>{ return this.http.get<Hero[]>(this.all_hero_api,{observe:'response'}) .pipe( map(resp=>resp.body['data']), tap(this.log), flatMap((data)=>{return from(data)}), filter((data)=>data['index'] > 1), shareReplay() // 轉換管道的最後將這個流轉換為一個熱Observable ) }
在呼叫的地方編寫呼叫程式碼:
sendGet(){ let obs = this.heroService.getHeroes$(); //第一次被訂閱 obs.subscribe(resp=>{ console.log('響應資訊:',resp); }); //第二次被訂閱 setTimeout(()=>{ obs.subscribe((resp)=>{ console.log('延遲後的響應資訊',resp); }) },2000) }
通過結果可以看出,第二次訂閱沒有觸發網路請求,但是也得到了資料:
網路請求只發送了一次(之前的會發送兩次):
4.2 share與非同步管道
這種場景筆者並沒有進行生產實踐,一是因為這種模式需要將資料的變換處理全部通過 pipe( )
管道來進行,筆者自己的函數語言程式設計功底可能還不足以應付,二來總覺得很多示例的使用場景很牽強,所以僅作基本功能介紹,後續有實戰心得後再修訂補充。 Angular
中提供了一種叫做 非同步管道
的模板語法,可以直接在 *ngFor
的微語法中使用可觀測物件:
<ul> <li *ngFor="let contact of contacts | async">{{contact.name}}</li> </ul> <ul> <li *ngFor="let contact of contacts2 | async">{{contact.name}}</li> </ul>
示例:
this.contacts = http.get('contacts.json') .map(response => response.json().items) .share(); setTimeout(() => this.contacts2 = this.contacts, 500);
五. 一點建議
一定要好好讀官方文件。