第一節:初識Angular-CLI
第二節:登陸組件的構建
第三節:創建一個待辦事項應用
第四節:進化!模塊化你的應用
第五節:多用戶版本的待辦事項應用
第六節:使用第三方樣式庫及模塊優化用
第七節:給組件帶來活力
Rx--隱藏在 Angular 中的利劍
Redux你的 Angular 應用
第八節:查缺補漏大合集(上)
第九節:查缺補漏大合集(下)javascript
在 Rx -- 隱藏在 Angular 中的利劍 一文中咱們已經初步的瞭解了 Rx 和 Rx 在 Angular 的應用。 今天咱們一塊兒經過一個具體的例子來理解響應式編程設計的思路。最後會看看剛剛發佈的 Angular 4 的新特性給響應式編程帶來了什麼新鮮的元素。css
我給出的答案很簡單:響應式編程可讓你把程序邏輯想的很清楚。爲何這麼說呢?讓咱們先來看一個小例子,好比咱們有這樣一個需求,在生日的控件以前添加一個年齡的選擇,用以輔助生日的輸入。雖然很變態,其實直接輸入趕腳比這種方式快啊,但真的有客戶提出過這種需求,無論怎樣咱們來看一下好了。html
首先分析一下需求:java
若是按傳統方式編程的話,咱們可能須要在年齡和年齡單位的兩個處理輸入改變的 event handler
去對數據進行處理,具體咱們就不展開了。咱們來看一下用響應式編程如何處理這個邏輯。react
理解 Rx 的關鍵是要把任何變化想象成數據流,數據流分爲幾種:編程
這麼說好像比較抽象,那麼仍是回到例子來看這個問題。就這個需求來看的話,年齡和年齡單位這兩個數據要一塊兒來考慮,數組
上圖中(因爲太懶,後面的合併虛線就沒有畫了),上面兩個流爲原始數據流,一個是年齡的數據流,每次更改年齡數時,這個數據流就產生一個數據:好比一開始初始值爲 33,咱們刪掉個位數的 3,這時因爲其變化,產生第二個值 3 (原十位的3),而後咱們添加了5,新值變成35,所以流中的第三個數據是35,以此類推。另外一個數據流反映了年齡單位的變化,按照「歲-月-歲-天」的次序產生新的數據。一我的的最終的年齡是經過年齡值和年齡單位聯合肯定的,這也就是說咱們須要對這兩個流作合併計算。app
那麼選擇什麼樣的合併方式呢?其實咱們須要的是任何一個流的值變化的時候,新的合併流都應該有一個對應數據,這個數據包括剛剛變化的那個值和另外一個流中最新的值。好比:若是年齡數據從 33 刪掉個位變成 3,此時咱們沒有改變年齡單位,合併流中的新數據應該是 3歲
。接下來咱們改變單位爲 月
,那這時候年齡數據的最新值仍然是 3 ,因此新流的數據應爲 3月
等等以此類推。dom
這樣的一種合併方式在 Rx 中專門有一個操做符來處理,那就是 combineLatest
。若是咱們使用 age$
表明年齡數據流(那個 $
表明 Stream -- 流的意思,約定俗成的寫法,不強制要求),用 ageUnit$
表明年齡單位數據流的話,咱們能夠寫出以下的合併邏輯,爲了簡化問題,咱們這裏合併後都使用 天
做爲單位:異步
// 這裏前面兩個參數都是參與合併的數據流,第三個是個處理函數
// 這個處理函數接受兩個流中的最新數據,而後通過運算輸出新值
this.computed$ = Observable.combineLatest(age$, ageUnit$, (a, u)=>{
// 非法數字就都按初始值處理,這裏就簡單粗暴了
if(a === undefined || a <= 0 ) return initialAge;
// 所有轉化爲天數
switch (parseInt(u)) {
case AgeUnit.Day.valueOf():
return a;
case AgeUnit.Month.valueOf():
return a * 30;
case AgeUnit.Year.valueOf():
default:
// 別問我閏年大小月啥的,只是個例子而已
return a * 365;
}
})複製代碼
合併以後呢,因爲咱們最終須要向生日那個輸入框中寫入一個日期,而咱們合併以後的流給出的是按天數計算的年齡,因此這裏顯然須要一個轉換。
在 Rx 中這種數據的轉換再容易不過了,最經常使用的一個就是 map
轉換操做符,接着上面的代碼繼續來一個 map
函數,這裏使用了 momentjs
的按當前日期減去剛剛的以天數爲單位的年齡值,就獲得一個大概估算的出生日期。
.map(a => {
const date = moment().subtract(a, 'days').format('YYYY-MM-DD');
return date;
});複製代碼
可是到這裏,你會發現咱們尚未定義兩個原始數據流呢,別急,留到後面是爲了引出 Angular 對於 Rx 的良好支持。
Angular 的表單處理很是強大,有模版驅動的表單和響應式表單兩類,兩種表單各有千秋,在不一樣場合能夠分別使用,甚至混合使用,但這裏就不展開了。咱們這裏使用了響應式表單,也很是簡單,就是一個 form
裏面 3 個控件,這裏我採用了官方的 Material 控件,若是你以爲不爽,能夠直接用基礎的 HTML 控件搭配樣式便可。
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<md-input-container align="end">
<input mdInput formControlName="age" type="number" placeholder="年齡" max="200" min="1" />
</md-input-container>
<md-button-toggle-group formControlName="ageUnit">
<md-button-toggle value="0" >歲</md-button-toggle>
<md-button-toggle value="1" >月</md-button-toggle>
<md-button-toggle value="2" >天</md-button-toggle>
</md-button-toggle-group>
<md-input-container>
<input mdInput formControlName="dateOfBirth" type="date" placeholder="出生日期" max="2100-12-31" min="1900-01-01" [value]="computed$ | async" />
<md-hint align="start">YYYY/MM/DD格式輸入</md-hint>
</md-input-container>
</form>複製代碼
Angular 中處理響應式表單只有 3 個步驟:
formControlName="blablabla"
form
標籤中添加 [formGroup]="xxx"
指令,這個 xxx
就是你在組件中聲明的 FormGroup
類型的成員變量:好比下面代碼中的 form: FormGroup;
FormBuilder
後(好比下面代碼中的 constructor(private fb: FormBuilder) { }
),用 FormBuilder
構造表單控件數組並賦值給剛纔的類型爲 FormGroup
的成員變量。import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, FormControl, Validators } from '@angular/forms';
import { AgeUnit } from '../../domain/entities.interface';
import * as moment from 'moment/moment';
@Component({
selector: 'app-reactive',
templateUrl: './reactive.component.html',
styleUrls: ['./reactive.component.scss']
})
export class ReactiveComponent implements OnInit {
form: FormGroup;
computed$: Observable<string>;
ageSub: Subscription;
dateOfBirth$: Observable<string>;
dateOfBirthSub: Subscription;
constructor(private fb: FormBuilder) { }
ngOnInit() {
this.form = this.fb.group({
age: ['', Validators.required],
ageUnit: ['', Validators.required],
dateOfBirth: ['', Validators.compose([Validators.required, this.validateDate])]
});
const initialAge = 33;
const initialAgeUnit = AgeUnit.Year;
this.form.controls['age'].setValue(initialAge);
this.form.controls['ageUnit'].setValue(initialAgeUnit);
}
validateDate(c: FormControl): {[key: string]: any}{
const result = moment(c.value).isValid
&& moment(c.value).isBefore()
&& moment(c.value).year()> 1900;
return {
"valid": result
}
}
onSubmit() {
if(!this.form.valid) return;
}
}複製代碼
如今這個表單就創建好了,但你可能會問,這也沒看出來響應式啊,別急,接下來咱們就要看看它的響應式支持了。咱們再回到一開始的小題目,咱們的兩個原始數據流:age$
和 ageUnit$
怎麼構建?這兩個數據流實際上是來自於兩個控件的值的變化,而響應式表單獲取值的變化是很是簡單的就一行:
this.form.controls['age'].valueChanges複製代碼
上面這行代碼的意思是從表單的控件數組中取得 formControlName
爲 age
的這個控件而後監聽其值的變化。這個 valueChanges
返回的其實就是一個 Observable
,見下面的 TypeScript 定義:
/** * Emits an event every time the value of the control changes, in * the UI or programmatically. */
readonly valueChanges: Observable<any>;複製代碼
既然咱們獲得了這個原始數據流,剩下的工做就比較簡單了。但咱們可能須要對這個原始數據流再作點處理。首先,咱們並不但願每次改這個值都去監聽,由於輸入是一個連續事件,每一次按鍵都監聽是不太划算的。這就須要一個濾波器的處理 .debounceTime(500)
,咱們不去處理 500 毫秒內的變化,而是等待其輸入停頓時再發送數據。第二,若是用戶採用了拷貝粘貼的方式,咱們但願一樣的數據不重複發送,因此濾掉相同的數據。最後,咱們採用 startWith
給這個流一個初始值,這是因爲若是一開始咱們什麼都不作,兩個流就都沒有數據;或者只改變其中一個,另外一個因爲一直沒有變就不會產生數據,這樣的話,合併流也不會有數據。
// 省略其它引入
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
// 省略其它部分
const age$ = this.form.controls['age'].valueChanges
.debounceTime(500)
.distinctUntilChanged()
.startWith(initialAge);
const ageUnit$ = this.form.controls['ageUnit'].valueChanges
.distinctUntilChanged()
.startWith(initialAgeUnit);複製代碼
到目前爲止,咱們尚未進行對 Observable 的訂閱,若是不訂閱的話,寫的再漂亮的語句也不會執行的。按常規套路來說,咱們得聲明 Subscription 對象,由於 Observable 是一直監聽的,即便頁面銷燬,它也還在,這會形成內存泄漏。因此,咱們須要再頁面銷燬(ngOnDestroy
中)的適合取消訂閱。 須要訂閱的 Observable
少的時候還好,一旦多起來,處理時也挺麻煩,像下面的代碼那樣。
// 省略其它引入
import { Subscription } from 'rxjs/Subscription';
// 省略其它部分
ageSub: Subscription;
// 省略其它部分
this.ageSub = this.computed$.subscribe(date => this.form.controls['dateOfBirth'].setValue(date));
// 省略其它部分
onNgDestroy(){
if(this.ageSub !== undefined || !this.ageSub.closed)
this.ageSub.unsubscribe();
}複製代碼
所幸的是,Angular 提供了對於響應式編程很是友好的設計,咱們徹底能夠不在代碼中作訂閱或取消訂閱的動做。那麼問題來了,不訂閱的話,值怎麼得到呢?答案是 Async 管道。Async 會在組件初始化時自動的訂閱以及在組件銷燬時自動取消訂閱,太爽了。所以,咱們能夠刪掉上面的代碼了,而後在組件模版中給生日的那個 input
添加一個指令 [value]="computed$ | async"
,這就是說該 input
的 value 就是 computed$
訂閱後的值,那麼 | async
是說 computed$
是一個 Observable,請對他採用異步處理,即初始化時自動的訂閱以及在組件銷燬時自動取消訂閱。
<input mdInput formControlName="dateOfBirth" // 省略其它屬性 [value]="computed$ | async" />複製代碼
上面的例子,我不知道你們發現沒有,固然 Rx 提供了好多方便的操做符。但更重要的是,寫 Rx 的時候,咱們須要對流程理解的足夠清晰,或者說 Rx 逼着咱們對流程反覆梳理。其實有的時候,寫 Rx 不必定很快,但一旦業務梳理清楚了,接下來就是幾行代碼的事情。若是你有時候以爲用現有的 Rx 操做符寫不出,那多半是你的對需求中涉及的數據流的關係沒有弄清楚。
Angular 4 中的 ngIf
如今能夠攜帶 else
了,若是你曾經使用過 Angular 就知道,原來咱們是得寫兩個 ngIf
來完成相似的功能的。這個 else
能夠攜帶一個模版的引用。好比下面例子中:若是用戶登陸成功顯示用戶名,不然顯示登陸連接。
<span *ngIf="auth$ else login">
<a routerLink="/profile">{{(auth$|async).user.name}}</a>
<a routerLink="/blablabla">{{(auth$|async).visits}}</a>
</span>
<ng-template #login>
<a routerLink="/login">登陸</a>
</ng-template>複製代碼
另外一個改進是 ngIf
中如今能夠將評估表達式的結果賦值給一個變量,好處是什麼呢?可讓你少寫不少 (auth$|async)
<span *ngIf="auth$ | async as auth else login">
<a routerLink="/profile">{{auth.user.name}}</a>
<a routerLink="/blablabla">{{auth.visits}}</a>
</span>
<ng-template #login>
<a routerLink="/login">登陸</a>
</ng-template>複製代碼
很久沒寫 Angular 了,但願後面會有時間多謝一些。另外,個人 《Angular 從零到一》出版了,歡迎你們圍觀、訂購、提出寶貴意見。