第一節(jié):初識Angular-CLI
第二節(jié):登錄組件的構建
第三節(jié):建立一個待辦事項應用
第四節(jié):進化!模塊化你的應用
第五節(jié):多用戶版本的待辦事項應用
第六節(jié):使用第三方樣式庫及模塊優(yōu)化用
第七節(jié):給組件帶來活力
Rx--隱藏在Angular 2.x中利劍
Redux你的Angular 2應用
第八節(jié):查缺補漏大合集(上)
第九節(jié):查缺補漏大合集(下)
番外:Rx--隱藏在Angular 2.x中利劍
Rx(Reactive Extension -- 響應式擴展 http://reactivex.io )最近在各個領域都非常火。其實Rx這個貨是微軟在好多年前針對C#寫的一個開源類庫,但好多年都不溫不火,一直到Netflix針對Java平臺做出了RxJava版本后才在開源社區(qū)熱度飛速躥升。
這里還有個小故事,Netflix之所以做RxJava完全是一個偶然。個中緣由是由于Netflix的系統(tǒng)越做越復雜,大家都絞盡腦汁琢磨怎么才能從這些復雜邏輯的地獄中把系統(tǒng)拯救出來。一天,一個從微軟跳槽過來的員工和主管說,我們原來在微軟做的一個叫Rx的東東挺好的,可以非常簡單的處理這些邏輯。主管理都沒理,心想微軟那套東西肯定又臃腫又不好用,從來沒聽說過微軟有什么好的開源產品。但那位前微軟的哥們鍥而不舍,非常執(zhí)著,不斷和組內員工和主管游說,宣傳這個Rx思想有多牛X。終于有一天,大家受不了了,說,這么著吧,給你個機會,你給大家仔細講講這個Rx,我們討論看看到底適不適合。于是這哥們一頓噴,把大家都驚住了,微軟竟然有這么好的東西。但是這東西是.Net的,怎么辦呢,那就寫一個吧(此處略去高山仰止的3000字)。
八卦講完,進入正題,那么什么叫響應式編程呢?這里引用一下Wikipedia的解釋:
英文原文:In computing, reactive programming is a programming paradigm oriented around data flows and the propagation of change. This means that it should be possible to express static or dynamic data flows with ease in the programming languages used, and that the underlying execution model will automatically propagate changes through the data flow.
我的翻譯:在計算領域,響應式編程一種面向數據流和變化傳播的編程范式。這意味著可以在編程語言中很方便地表達靜態(tài)或動態(tài)的數據流,而相關的計算模型會自動將變化的值通過數據流進行傳播。
這都說的什么啊?沒關系,概念永遠是抽象的,我們來舉幾個例子。比如說在傳統(tǒng)的編程中 a=b+c
,表示將表達式的結果賦給a,而之后改變b或c 的值不會影響a。但在響應式編程中,a的值會隨著b或c的更新而更新。

那么用響應式編程方法寫出來就是這個樣子,可以看到隨著b和c的變化a也會隨之變化。

看出來一些不一樣的思維方式了嗎?響應式編程需要描述數據流,而不是單個點的數據變量,我們需要把數據的每個變化匯聚成一個數據流。如果說傳統(tǒng)編程方式是基于離散的點,那么響應式編程就是線。
上面的代碼雖然很短,但體現出Rx的一些特點
- Lamda表達式,對,就是那個看上去像箭頭的東西
=>
。你可以把它想象成一個數據流的指向,我們從箭頭左方取得數據流,在右方做一系列處理后或者輸出成另一個數據流或者做一些其他對于數據的操作。 - 操作符:這個例子中的
from
,zip
都是操作符。Rx中有太多的操作符,從大類上講分為:創(chuàng)建類操作符、變換類操作符、過濾類操作符、合并類操作符、錯誤處理類操作符、工具類操作符、條件型操作符、數學和聚集類操作符、連接型操作符等等。
Rx再體驗
還是從例子開始,我們逐漸的來熟悉Rx。
為了更直觀的看到Rx的效果,推薦大家去JSBin這個在線Javascript IDE http://jsbin.com 去實驗我們下面的練習。這個IDE非常方便,一共有5個功能窗口:HTML、CSS、Javascript、Console和Output

首先在HTML中引入Rx類庫,然后定義一個id為todo的文本輸入框:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://unpkg.com/@reactivex/rxjs@5.0.0-beta.7/dist/global/Rx.umd.js"></script>
</head>
<body>
<input id="todo" type="text"/>
</body>
</html>
在Javascript標簽中選擇 ES6/Babel
,因為這樣可以直接使用ES6的語法,在文本框中輸入以下javascript。在RxJS領域一般在Observable類型的變量后面加上$
標識這是一個“流變量”(由英文Stream得來,Observable就是一個Stream,所以用$
標識),不是必須的,但是屬于約定俗成。
let todo = document.getElementById('todo');
let input$ = Rx.Observable.fromEvent(todo, 'keyup');
input$.subscribe(input => console.log(input.target.value));
如果Console窗口默認沒有打開的話,請點擊 Console
標簽,然后選中右側的 Run with JS
旁邊的Auto-run js復選框。在Output窗口中應該可以看到一個文本輸入框,在這個輸入框中輸入任意你要試驗的字符,觀察Console

這幾行代碼很簡單:首先我們得到HTML中id為todo的輸入框對象,然后定義一個觀察者對象將todo這個輸入框的keyup事件轉換成一個數據流,最后訂閱這個數據流并在console中輸出我們接收到的input事件的值。我們從這個例子中可以觀察到幾個現象:
- 數據流:你每次在輸入框中輸入時都會有新的數據被推送過來。本例中,你會發(fā)現連續(xù)輸入“1,2,3,4”,在console的輸出是“1,12,123,1234”,也就是說每次keyup事件我們都得到了完整的輸入框中的值。而且這個數據流是無限的,只要我們不停止訂閱,它就會一直在那里待命。
- 我們觀察的是todo上發(fā)生的keyup這個事件,那如果我一直按著某個鍵不放會怎么樣呢?你的猜測是對的,一直按著的時候,數據流沒有更新,直到你抬起按鍵為止(你看到截圖里面有2條一模一樣的含有多個5的數據是因為我用的Surface Pro截圖時的快捷鍵也被截獲了,但由于是控制鍵所以文字內容沒有改變)

如果觀察的足夠仔細的話,你會發(fā)現console中輸出的值其實是 input.target.value
,我們觀察的對象其實是id為todo的這個對象上發(fā)生的keyup事件(Rx.Observable.fromEvent(todo, 'keyup')
)。那么其實在訂閱的代碼段中的input其實是keyup事件才對。好,我們看看到底是什么,將 console.log(input.target.value)
改寫成 console.log(input)
,看看會怎樣呢?是的,我們得到的確實是KeyboardEvent

不太過癮?那么我們再來做幾個小練習,首先將代碼改成下面的樣子,其實不用我講,你應該也可以猜得到,這是要過濾出 keyCode=32
的事件,keyCode是Ascii碼,那么這就是要把空格濾出來
let todo = document.getElementById('todo');
let input$ = Rx.Observable.fromEvent(todo, 'keyup');
input$
.filter(ev=>ev.keyCode===32)
.subscribe(ev=>console.log(ev.target.value));
結果我們看到了,按123456789都沒有反應,直到按了空格

你可能一直在奇怪,我們最終只對輸入框的值有興趣,能不能數據流只傳值過來呢?當然可以,使用map這個變換類操作符就可以完成這個轉換了
let todo = document.getElementById('todo');
let input$ = Rx.Observable.fromEvent(todo, 'keyup');
input$
.map(ev=>ev.target.value)
.subscribe(value=>console.log(value));
map這個操作符做的事情就是允許你對原數據流中的每一個元素應用一個函數,然后返回并形成一個新的數據流,這個數據流中的每一個元素都是原來的數據流中的元素應用函數后的值。比如下面的例子,對于原數據流中的每個數應用一個函數10*x,也就是擴大了10倍,形成一個新的數據流。

常見操作
最常見的兩個操作符我們上面已經了解了,我們繼續(xù)再來認識新的操作符。類似 .map(ev=>ev.target.value)
的場景太多了,以至于rxjs團隊搞出來一個專門的操作符來應對,這個操作符就是 pluck
。這個操作符專業(yè)從事從一系列嵌套的屬性種把值提取出來形成新的流。比如上面的例子可以改寫成下面的代碼,效果是一樣的。那么如果其中某個屬性為空怎么辦?這個操作符負責返回一個 undefined
作為值加入流中。
let todo = document.getElementById('todo');
let input$ = Rx.Observable.fromEvent(todo, 'keyup');
input$
.pluck('target', 'value')
.subscribe(value=>console.log(value));
下面我們稍微給我們的頁面加點料,除了輸入框再加一個按鈕
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://unpkg.com/@reactivex/rxjs@5.0.0-beta.7/dist/global/Rx.umd.js"></script>
</head>
<body>
<input id="todo" type="text"/>
<button id="addBtn">Add</button>
</body>
</html>
在Javascript中我們同樣方法得到按鈕的DOM對象以及聲明對此按鈕點擊事件的觀察者:
let addBtn = document.getElementById('addBtn');
let buttonClick$ = Rx.Observable
.fromEvent(addBtn, 'click')
.mapTo('clicked');
由于點擊事件沒有什么可見的值,所以我們利用一個操作符叫 mapTo
把對應的每次點擊轉換成字符 clicked
。其實它也是一個 map
的簡化操作。

合并類操作符
combineLatest操作符
既然現在我們已經有了兩個流,應該試驗一下合并類操作符了,先來試試 combineLatest
,我們合并了按鈕點擊事件的數據流和文本框輸入事件的數據流,并且返回一個對象,這個對象有兩個屬性,第一個是按鈕事件數據流的值,第二個是文本輸入事件數據流的值。也就是說應該是類似 { ev: 'clicked', input: '1'}
這樣的結構。
Rx.Observable.combineLatest(buttonClick$, input$, (ev, input)=>{
return {
ev: ev,
input: input
}
})
.subscribe(value => console.log(value))
那看看結果如何,在文本輸入框輸入1,沒反應,再輸入2,還是沒反應

那我們點擊一下按鈕試試,這回有結果了,但有點沒明白為什么是12,輸入的數據流應該是: 1,12,...
但那個1怎么丟了呢?

再來文本框輸入3,4看看,這回倒是都出來了

我們來解釋一下combineLatest的機制就會明白了,如下圖所示,上面的2條線是2個源數據流(我們分別叫它們源1和源2吧),經過combineLatest操作符后產生了最下面的數據流(我們稱它為結果流)。
當源1的數據流發(fā)射時,源2沒有數據,這時候結果流也不會有數據產生,當源2發(fā)射第一個數據(圖中A)后,combineLatest操作符做的處理是,把A和源1的最近產生的數據(圖中2)組合在一起,形成結果流的第一個數據(圖中2A)。當源2產生第二個數據(圖中B)時,源1這時沒有新的數據產生,那么還是用源1中最新的數據(圖中2)和源2中最新的數據(圖中B)組合。
也就是說 combineLatest
操作符其實是在組合2個源數據流中選擇最新的2個數據進行配對,如果其中一個源之前沒有任何數據產生,那么結果流也不會產生數據。

講到這里,有童鞋會問,原理是明白了,但什么樣的實際需求會需要這個操作符呢?其實有很多,我這里只舉一個小例子,現在健身這么熱,比如說我們做一個簡單的BMI計算器,BMI的計算公式是:體重(公斤)/(身高身高)(米米)。那么我們在頁面給出兩個輸入框和一個用于顯示結果的div:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://unpkg.com/@reactivex/rxjs@5.0.0-beta.7/dist/global/Rx.umd.js"></script>
</head>
<body>
Weight: <input type="number" id="weight"> kg
<br/>
Height: <input type="number" id="height"> cm
<br/>
Your BMI is <div id="bmi"></div>
</body>
</html>
那么在JS中,我們想要達成的結果是只有兩個輸入框都有值的時候才能開始計算BMI,這時你發(fā)現combineLatest的邏輯不要太順溜啊。
let weight = document.getElementById('weight');
let height = document.getElementById('height');
let bmi = document.getElementById('bmi');
let weight$ = Rx.Observable
.fromEvent(weight, 'input')
.pluck('target', 'value');
let height$ = Rx.Observable
.fromEvent(height, 'input')
.pluck('target', 'value');
let bmi$ = Rx.Observable
.combineLatest(weight$, height$, (w, h) => w/(h*h/100/100));
bmi$.subscribe(b => bmi.innerHTML=b);

zip操作符
除了 combineLatest
,Rxjs還提供了多個合并類的操作符,我們再試驗一個 zip
操作符。 zip
和 combineLatest
非常像,但重要的區(qū)別點在于 zip
嚴格的需要多個源數據流中的每一個的相同順序的元素配對。
比如說還是上面的例子,zip
要求源1的第一個數據和源2的第一個數據組成一對,產生結果流的第一個數據;源1的第二個數據和源2的第二個數據組成一對,產生結果流的第二個數據。而 combineLatest
不需要等待另一個源數據流產生數據,只要有一個產生,結果流就會產生。

zip
這個詞在英文中有拉鏈的意思,記住這個有助于我們理解這個操作符,就像拉鏈一樣,它需要拉鏈兩邊的齒一一對應。從效果角度上講,這個操作符有減緩發(fā)射速度的作用,因為它會等待合并序列中最慢的那個。
下面我們還是看個例子,在我寫第七章的使用Bing Image API變換背景時,我最開始的想法是取得圖片數組后,把這個數組中的元素每隔一段時間發(fā)送出去一個,這樣組件端就不用關心圖片變化的邏輯,只要服務發(fā)射一個地址,我就加載就行了。我就是用zip來實現的,我們在這個邏輯中有2個源數據流:基于一個數組生成的數據流以及一個時間間隔數據流。前者的發(fā)射速度非常快,后者則速度均勻,我們希望按后者的速度對齊前者,以達到每隔一段時間發(fā)射前者的數據的目的。
yieldByInterval(items, time) {
return Observable.from(items).zip(
Observable.interval(time),
(item, index) => item
);
}
為了更好的讓大家體會,我改寫一個純javascript版本,可以在JSBin上面直接跑的,它的本質邏輯和上面講的相同:
let greetings = ['Hello', 'How are you', 'How are you doing'];
let time = 3000;
let item$ = Rx.Observable.from(greetings);
let interval$ = Rx.Observable.interval(time);
Rx.Observable.zip(
item$,
interval$,
(item, index) => {
return {
item: item,
index: index
}
}
)
.subscribe(result =>
console.log(
'item: ' + result.item +
' index: ' + result.index +
' at ' + new Date()));
我們看到結果如下圖所示,每隔3000毫秒,數組中的歡迎文字被輸出一次。

這兩個操作符應該是Rx中最常用的2個合并類操作符了。其他的操作符大家可以去 http://reactivex.io/documentation/operators.html 查看,注意不是所有的操作符RxJS都有。而且RxJS 5.0 目前整體的趨勢是減少不必要的以及冗余的操作符,所以我們只介紹最常用的一些操作符。
創(chuàng)建類操作符
通常來講,Rx團隊不鼓勵新手自己從0開始創(chuàng)建Observable,因為狀態(tài)太復雜,會遺漏一些問題。Rx鼓勵的是通過已有的大量創(chuàng)建類轉換操作符來去建立Observable。我們其實之前已經見過一些了,包括 from
和 fromEvent
。
from操作符
from
可以支持從數組、類似數組的對象、Promise、iterable 對象或類似Observable的對象(其實這個主要指ES2015中的Observable)來創(chuàng)建一個Observable。
這個操作符應該是可以創(chuàng)建Observable的操作符中最常使用的一個,因為它幾乎可以把任何對象轉換成Observable。
var array = [10, 20, 30];
var result$ = Rx.Observable.from(array);
result$.subscribe(x => console.log(x));

fromEvent操作符
這個操作符是專門為事件轉換成Observable而制作的,非常強大且方便。對于前端來說,這個方法用于處理各種DOM中的事件再方便不過了。
var click$ = Rx.Observable.fromEvent(document, 'click');
click$.subscribe(x => console.log(x));

fromEventPattern
我們經常會遇到一些已有的代碼,這些代碼和類庫往往不受我們的控制,無法重構或代價太大。我們需要在這種情況下可以利用Rx的話,就需要大量的可以從原有的代碼中可以轉換的方法。addXXXHandler和removeXXXHandler就是大家以前經常使用的一種模式,那么在Rx中也提供了對應的方法可以轉換,那就是
function addClickHandler(handler) {
document.addEventListener('click', handler);
}
function removeClickHandler(handler) {
document.removeEventListener('click', handler);
}
var click$ = Rx.Observable.fromEventPattern(
addClickHandler,
removeClickHandler
);
click$.subscribe(x => console.log(x));

defer操作符
defer
是直到有訂閱者之后才創(chuàng)建Observable,而且它為每個訂閱者都會這樣做,也就是說其實每個訂閱者都是接收到自己的單獨數據流序列。

Rx.Observable.defer(()=>{
let result = doHeavyJob();
return result?'success':'failed';
})
.subscribe(x=>console.log(x))
function doHeavyJob(){
setTimeout(function() {console.log('doing something');}, 2000);
return true;
}

Interval
Rx提供內建的可以創(chuàng)建和計時器相關的Observable方法,第一個是Interval,它可以在指定時間間隔發(fā)送整數的自增長序列。

例如下面代碼,我們每隔500毫秒發(fā)送一個整數,這個數列是無窮的,我們取前三個好了:
let source = Rx.Observable
.interval(500 /* ms */)
.take(3);
let subscription = source.subscribe(
function (x) {
console.log('Next: ' + x);
},
function (err) {
console.log('Error: ' + err);
},
function () {
console.log('Completed');
});
那么輸出是

這里大家可能注意到我們沒有采用箭頭的方式,而是用傳統(tǒng)的寫法,寫了 function(x){...}
,哪種方式其實都可以,箭頭方式會更簡單。
另一個需要注意的地方是,在subscribe方法中我們多了2個參數:一個處理異常,一個處理完成。Rx認為所有的數據流會有三個狀態(tài):next,error和completed。這三個函數就是分別處理這三種狀態(tài)的,當然如果我們不寫某個狀態(tài)的處理,也就意味著我們認為此狀態(tài)不需要特別處理。而且有些序列是沒有completed狀態(tài)的,因為是無限序列。本例中,如果我們去掉 .take(3)
那么completed是永遠無法觸發(fā)的。
Timer
下面我們來看看Timer,一共有2種形式的Timer,一種是指定時間后返回一個序列中只有一個元素(值為0)的Observable。
//這里指定一開始的delay時間
//也可以輸入一個Date,比如“2016-12-31 20:00:00”
//這樣變成了在指定的時間觸發(fā)
let source = Rx.Observable.timer(2000);
let subscription = source.subscribe(
x => console.log('Next: ' + x),
err => console.log('Error: ' + err),
() => console.log('Completed'));

第二種Timer很類似于Interval。除了第一個參數是一開始的延遲時間,第二個參數是間隔時間,也就是說,在一開始的延遲時間后,每隔一段時間就會返回一個整數序列。這個和Interval基本一樣除了Timer可以指定什么時間開始(延遲時間)。
var source = Rx.Observable.timer(2000, 100)
.take(3);
var subscription = source.subscribe(
x => console.log('Next: ' + x),
err => console.log('Error: ' + err),
() => console.log('Completed'));

當然還有其他創(chuàng)建類的操作符,大家可以去 http://reactivex.io/documentation/operators 查閱自行試驗一下。
過濾類操作符
之前我們見過好幾個過濾類操作符:filter,distinct,take和debounce。
filter
Filter操作只允許數據流中滿足其predicate測試的元素發(fā)射出去,這個predicate函數接受3個參數:
- 原始數據流元素
- 索引,這個是指該元素在源數據流中的位置(從0開始)
- 源Observable對象
如下的代碼將0-5中偶數過濾出來:
let source = Rx.Observable.range(0, 5)
.filter(function (x, idx, obs) {
return x % 2 === 0;
});
let subscription = source.subscribe(
x => console.log('Next: ' + x),
err => console.log('Error: ' + err),
() => console.log('Completed'));

debounceTime
對于一些發(fā)射頻率比較高的數據流,我們有時會想給它安個“整流器”。比如在一個搜索框中,輸入一些字符后希望出現一些搜索建議,這是個非常好的功能,很多時候可以減少用戶的輸入。
但是由于這些搜索建議需要聯網完成數據的傳遞,如果太頻繁操作的話,對于用戶的數據流量和服務器的性能承載都是有副作用的。所以我們一般希望在用戶連續(xù)快速輸入時不去搜索,而是等待有相對較長的間隔時再去搜索。
下面的代碼從輸入上做了這樣的一個“整流器”,濾掉了間隔時間小于400毫米的輸入事件(輸入本身不受影響),只有用戶出現較明顯的停頓時才把輸入值發(fā)射出來。
let todo = document.getElementById('todo');
let input$ = Rx.Observable.fromEvent(todo, 'keyup');
input$
.debounceTime(400)
.subscribe(input => console.log(input.target.value));
快速輸入“12345”,在這種情況下得到的是一條數據

但如果不應用debounceTime,我們得到5條記錄

其他的過濾類操作符也很有趣,比如Distinct就是可以把重復的元素過濾掉,skip就可以跳過幾個元素等等,可以自行研究,這里就不一一舉例了。
Rx的操作符實在太多了,我只能列舉一些較常見的給大家介紹一下,其他的建議大家去官方文檔學習。
Angular2中的內建支持
Angular2中對于Rx的支持是怎么樣的呢?先試驗一下吧,簡單粗暴的一個組件模版頁面
<p>
{{clock}}
</p>
和在組件中定義一個簡單粗暴的成員變量
import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/interval';
@Component({
selector: 'app-playground',
templateUrl: './playground.component.html',
styleUrls: ['./playground.component.css']
})
export class PlaygroundComponent{
clock = Observable.interval(1000);
constructor() { }
}
搞定!打開瀏覽器,顯示了一個 [object Object]
,暈倒。

當然經過前面的學習,我們知道Observable是個異步數據流,我們可以把代碼改寫一下,在訂閱方法中去賦值就一切ok了。
import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/interval';
@Component({
selector: 'app-playground',
templateUrl: './playground.component.html',
styleUrls: ['./playground.component.css']
})
export class PlaygroundComponent{
clock: number;
constructor() {
Observable.interval(1000)
.subscribe(value => this.clock= value)
}
}

但是這樣做還是有一個問題,我們加入一個do操作符,在每次訂閱前去記錄就會發(fā)現一些問題。當我們離開頁面再回來,每次進入都會創(chuàng)建一個新的訂閱,,但原有的沒有釋放。
Observable.interval(1000)
.do(_ => console.log('observable created'))
.subscribe(value => this.clock= value);
觀察console中在‘observable created’之前的數字和頁面顯示的數字,大概是頁面每增加1,console的數字增加2,這說明我們后面運行著2個訂閱。

原因是我們沒有在頁面銷毀時取消訂閱,那么我們利用生命周期的onDestroy來完成這一步:
import { Component, OnDestroy } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/observable/interval';
@Component({
selector: 'app-playground',
templateUrl: './playground.component.html',
styleUrls: ['./playground.component.css']
})
export class PlaygroundComponent implements OnDestroy{
clock: number;
subscription: Subscription;
constructor() {
this.subscription = Observable.interval(1000)
.do(_ => console.log('observable created'))
.subscribe(value => this.clock= value);
}
ngOnDestroy(){
if(this.subscription !== undefined)
this.subscription.unsubscribe();
}
}
現在再來觀察,同樣進入并離開再進入頁面后,頁面每增加1,console也會增加1。

Async管道
現在看起來還是挺麻煩的,有沒有更簡單的方法呢?答案當然是肯定的:Angular2提供一個管道叫:async,有了這個管道,我們無需管理瑣碎的取消訂閱,以及訂閱了。
讓我們回到最開始的簡單粗暴版本,模版文件稍微改寫一下
<p>
{{ clock | async }}
</p>
這個 | async
是什么東東?async是Angular2提供的一種轉換器,叫管道(Pipe)。
每個應用開始的時候差不多都是一些簡單任務:獲取數據、轉換它們,然后把它們顯示給用戶。一旦取到數據,我們可以把它們原始值的結果直接顯示。 但這種做法很少能有好的用戶體驗。比如,幾乎每個人都更喜歡簡單的日期格式,幾月幾號星期幾,而不是原始字符串格式 —— Fri Apr 15 1988 00:00:00 GMT-0700 (Pacific Daylight Time)。通過管道我們可以把不友好的值轉換成友好的值顯示在頁面中。
Angular內置了一些管道,比如DatePipe、UpperCasePipe、LowerCasePipe、CurrencyPipe和PercentPipe。它們全都可以直接用在任何模板中。Async管道也是內置管道之一。
當然這樣在頁面寫完管道后,我們的組件版本也回歸了簡單粗暴版本:
import { Component, OnDestroy } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/interval';
@Component({
selector: 'app-playground',
templateUrl: './playground.component.html',
styleUrls: ['./playground.component.css']
})
export class PlaygroundComponent {
clock = Observable.interval(1000).do(_=>console.log('observable created'));
constructor() { }
}
現在打開瀏覽器,看一下頁面的效果

你做這個試驗時很可能會遭遇一個錯誤,說async pipe無法找到

這種情況一般是由于CommonModule沒有導入造成的,遇到這種錯誤,請導入CommonModule。
Rx版本的Todo
這一節(jié)我們通過改造我們的待辦事項應用來進一步體會Rx的威力。首先我們把TodoService中原來采用的Promise方式都替換成Observable的方式。
在進行改動之前,我們來重新分析一下邏輯:我們原有的實現方式中,組件中保留了一個todos數組的本地拷貝,服務器API邏輯在Service中完成。其實組件最好不關心邏輯,即使是本地拷貝的邏輯,也不應該放到組件中。組件本身的數據都是監(jiān)聽Service中的數據變化而得到的。
那么我們應該在Service中建立本地的內存“數據庫”,我們叫它 dataStore
吧。這個“數據庫”中只有一個“表”:todos。
//TodoService.ts
private dataStore: { // todos的內存“數據庫”
todos: Todo[]
};
為了讓組件可以監(jiān)聽到這個數據的變化,我們需要一個Observable,但是在Service中我們還需要寫入變化,這樣的話,我們選擇一個既是Observable又是Observer的對象,在Rx中,Subject就是這樣的對象:
//TodoService.ts
...
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
@Injectable()
export class TodoService {
...
private _todos: BehaviorSubject<Todo[]>;
constructor(private http: Http, @Inject('auth') private authService) {
this.dataStore = { todos: [] };
this._todos = new BehaviorSubject<Todo[]>([]);
}
...
get todos(){
return this._todos.asObservable();
}
...
我們使用了一個BehaviorSubject,它的一個特點是存儲了發(fā)射的最新的值,這樣無論什么訂閱者訂閱時都會得到“當前值”。我們之前通過ReplaySubject也實現過類似功能,但Replay是可以緩存多個值的。
我們在構造中分別初始化了 dataStore
和 _todos
,然后提供了一個get的屬性方法讓其他訂閱者可以訂閱todos的變化。在這個屬性方法中,我們把Subject轉成了Observable(通過.asObservable()
)。
那么我們如何寫入變化呢?拿增加一個代辦事項( addTodo(desc:string)
)的邏輯來看一下吧。
addTodo(desc:string){
let todoToAdd = {
id: UUID.UUID(),
desc: desc,
completed: false,
userId: this.userId
};
this.http
.post(this.api_url, JSON.stringify(todoToAdd), {headers: this.headers})
.map(res => res.json() as Todo)
.subscribe(todo => {
this.dataStore.todos = [...this.dataStore.todos, todo];
this._todos.next(Object.assign({}, this.dataStore).todos);
});
}
由于 this.http.post
返回的本身就是Observable,所以我們不再需要 .toPromise()
這個方法了。直接用 map
將response的數據流轉換成Todo的數據流,然后更新本地數據,然后使用Subject的 next
方法(this._todos.next
)把本地數據寫入數據流。這個next的含義就是讓推送一個新元素到數據流。
按照這種邏輯,我們把整個 TodoService
改造成下面的樣子。
import { Injectable, Inject } from '@angular/core';
import { Http, Headers } from '@angular/http';
import { UUID } from 'angular2-uuid';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Todo } from '../domain/entities';
@Injectable()
export class TodoService {
private api_url = 'http://localhost:3000/todos';
private headers = new Headers({'Content-Type': 'application/json'});
private userId: string;
private _todos: BehaviorSubject<Todo[]>;
private dataStore: { // todos的內存“數據庫”
todos: Todo[]
};
constructor(private http: Http, @Inject('auth') private authService) {
this.authService.getAuth()
.filter(auth => auth.user != null)
.subscribe(auth => this.userId = auth.user.id);
this.dataStore = { todos: [] };
this._todos = new BehaviorSubject<Todo[]>([]);
}
get todos(){
return this._todos.asObservable();
}
// POST /todos
addTodo(desc:string){
let todoToAdd = {
id: UUID.UUID(),
desc: desc,
completed: false,
userId: this.userId
};
this.http
.post(this.api_url, JSON.stringify(todoToAdd), {headers: this.headers})
.map(res => res.json() as Todo)
.subscribe(todo => {
this.dataStore.todos = [...this.dataStore.todos, todo];
this._todos.next(Object.assign({}, this.dataStore).todos);
});
}
// PATCH /todos/:id
toggleTodo(todo: Todo) {
const url = `${this.api_url}/${todo.id}`;
const i = this.dataStore.todos.indexOf(todo);
let updatedTodo = Object.assign({}, todo, {completed: !todo.completed});
return this.http
.patch(url, JSON.stringify({completed: !todo.completed}), {headers: this.headers})
.subscribe(_ => {
this.dataStore.todos = [
...this.dataStore.todos.slice(0,i),
updatedTodo,
...this.dataStore.todos.slice(i+1)
];
this._todos.next(Object.assign({}, this.dataStore).todos);
});
}
// DELETE /todos/:id
deleteTodo(todo: Todo){
const url = `${this.api_url}/${todo.id}`;
const i = this.dataStore.todos.indexOf(todo);
this.http
.delete(url, {headers: this.headers})
.subscribe(_ => {
this.dataStore.todos = [
...this.dataStore.todos.slice(0,i),
...this.dataStore.todos.slice(i+1)
];
this._todos.next(Object.assign({}, this.dataStore).todos);
});
}
// GET /todos
getTodos(){
this.http.get(`${this.api_url}?userId=${this.userId}`)
.map(res => res.json() as Todo[])
.do(t => console.log(t))
.subscribe(todos => this.updateStoreAndSubject(todos));
}
// GET /todos?completed=true/false
filterTodos(filter: string) {
switch(filter){
case 'ACTIVE':
this.http
.get(`${this.api_url}?completed=false&userId=${this.userId}`)
.map(res => res.json() as Todo[])
.subscribe(todos => this.updateStoreAndSubject(todos));
break;
case 'COMPLETED':
this.http
.get(`${this.api_url}?completed=true&userId=${this.userId}`)
.map(res => res.json() as Todo[])
.subscribe(todos => this.updateStoreAndSubject(todos));
break;
default:
this.getTodos();
}
}
toggleAll(){
this.dataStore.todos.forEach(todo => this.toggleTodo(todo));
}
clearCompleted(){
this.dataStore.todos
.filter(todo => todo.completed)
.forEach(todo => this.deleteTodo(todo));
}
private updateStoreAndSubject(todos) {
this.dataStore.todos = [...todos];
this._todos.next(Object.assign({}, this.dataStore).todos);
}
}
接下來我們看一下 src/app/todo/todo.component.ts
,由于大部分邏輯已經在 TodoService
中實現了,我們可以刪除客戶端的邏輯代碼:
import { Component, OnInit, Inject } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';
import { TodoService } from './todo.service';
import { Todo } from '../domain/entities';
import { Observable } from 'rxjs/Observable';
@Component({
templateUrl: './todo.component.html',
styleUrls: ['./todo.component.css']
})
export class TodoComponent implements OnInit {
todos : Observable<Todo[]>;
constructor(
@Inject('todoService') private service,
private route: ActivatedRoute,
private router: Router) {}
ngOnInit() {
this.route.params
.pluck('filter')
.subscribe(filter => {
this.service.filterTodos(filter);
this.todos = this.service.todos;
})
}
addTodo(desc: string) {
this.service.addTodo(desc);
}
toggleTodo(todo: Todo) {
this.service.toggleTodo(todo);
}
removeTodo(todo: Todo) {
this.service.deleteTodo(todo);
}
toggleAll(){
this.service.toggleAll();
}
clearCompleted(){
this.service.clearCompleted();
}
}
可以看到 addTodo
、 toggleTodo
、 removeTodo
、toggleAll
和 clearCompleted
基本上已經沒有業(yè)務邏輯代碼了,只是簡單調用service的方法而已。
還有一個比較明顯的變化是,我們接收路由參數的方式也變成了Rx的方式,之前我們提過,像Angular2這種深度嵌合Rx的平臺框架,幾乎處處都有Rx的影子。
當然,我們的組件中的todos變成了一個Observable,在構造時直接把Service的屬性方法todos賦值上去了。這樣改造后,我們只需改動模版的兩行代碼就大功告成了,那就是替換原有的="todos..."
為 = " todos | async"
。
<div>
<app-todo-header
placeholder="What do you want"
(onEnterUp)="addTodo($event)" >
</app-todo-header>
<app-todo-list
[todos]="todos | async"
(onToggleAll)="toggleAll()"
(onRemoveTodo)="removeTodo($event)"
(onToggleTodo)="toggleTodo($event)"
>
</app-todo-list>
<app-todo-footer
[itemCount]="(todos | async).length"
(onClear)="clearCompleted()">
</app-todo-footer>
</div>
啟動瀏覽器看看吧,一切功能正常,代碼更加簡潔,邏輯更加清楚。

小結
我們的Angular學習之旅從零開始到現在,完整的搭建了一個小應用。相信大家現在應該對Angular2有一個大概的認識了,而且也可以參與到正式的開發(fā)項目中去了。但Angular2作為一個完整框架,有很多細節(jié)我們是沒有提到的,大家可以到官方文檔 https://angular.cn/ 去查找和學習。
本屆代碼: https://github.com/wpcfan/awesome-tutorials/tree/master/angular2/ng2-tut
慕課網 Angular 視頻課上線: http://coding.imooc.com/class/123.html?mc_marking=1fdb7649e8a8143e8b81e221f9621c4a&mc_channel=banner
紙書出版了,比網上內容豐富充實了,歡迎大家訂購!
京東鏈接:https://item.m.jd.com/product/12059091.html?from=singlemessage&isappinstalled=0

第一節(jié):初識Angular-CLI
第二節(jié):登錄組件的構建
第三節(jié):建立一個待辦事項應用
第四節(jié):進化!模塊化你的應用
第五節(jié):多用戶版本的待辦事項應用
第六節(jié):使用第三方樣式庫及模塊優(yōu)化用
第七節(jié):給組件帶來活力
Rx--隱藏在Angular 2.x中利劍
Redux你的Angular 2應用
第八節(jié):查缺補漏大合集(上)
第九節(jié):查缺補漏大合集(下)