第一節:初識Angular-CLI
第二節:登陸組件的構建
第三節:創建一個待辦事項應用
第四節:進化!模塊化你的應用
第五節:多用戶版本的待辦事項應用
第六節:使用第三方樣式庫及模塊優化用
第七節:給組件帶來活力
Rx--隱藏在 Angular 中的利劍
Redux你的 Angular 應用
第八節:查缺補漏大合集(上)
第九節:查缺補漏大合集(下)javascript
Rx(Reactive Extension -- 響應式擴展 reactivex.io )最近在各個領域都很是火。其實Rx這個貨是微軟在好多年前針對C#寫的一個開源類庫,但好多年都不溫不火,一直到Netflix針對Java平臺作出了RxJava版本後纔在開源社區熱度飛速躥升。css
這裏還有個小故事,Netflix之因此作RxJava徹底是一個偶然。箇中原因是因爲Netflix的系統越作越複雜,你們都絞盡腦汁琢磨怎麼才能從這些複雜邏輯的地獄中把系統拯救出來。一天,一個從微軟跳槽過來的員工和主管說,咱們原來在微軟作的一個叫Rx的東東挺好的,能夠很是簡單的處理這些邏輯。主管理都沒理,心想微軟那套東西確定又臃腫又很差用,歷來沒據說過微軟有什麼好的開源產品。但那位前微軟的哥們持之以恆,很是執着,不斷和組內員工和主管遊說,宣傳這個Rx思想有多牛X。終於有一天,你們受不了了,說,這麼着吧,給你個機會,你給你們仔細講講這個Rx,咱們討論看看到底適不適合。因而這哥們一頓噴,把你們都驚住了,微軟居然有這麼好的東西。可是這東西是.Net的,怎麼辦呢,那就寫一個吧(此處略去高山仰止的3000字)。html
八卦講完,進入正題,那麼什麼叫響應式編程呢?這裏引用一下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.java
個人翻譯:在計算領域,響應式編程一種面向數據流和變化傳播的編程範式。這意味着能夠在編程語言中很方便地表達靜態或動態的數據流,而相關的計算模型會自動將變化的值經過數據流進行傳播。react
這都說的什麼啊?不要緊,概念永遠是抽象的,咱們來舉幾個例子。好比說在傳統的編程中 a=b+c
,表示將表達式的結果賦給a,而以後改變b或c 的值不會影響a。但在響應式編程中,a的值會隨着b或c的更新而更新。git
那麼用響應式編程方法寫出來就是這個樣子,能夠看到隨着b和c的變化a也會隨之變化。github
看出來一些不同的思惟方式了嗎?響應式編程須要描述數據流,而不是單個點的數據變量,咱們須要把數據的每一個變化匯聚成一個數據流。若是說傳統編程方式是基於離散的點,那麼響應式編程就是線。數據庫
上面的代碼雖然很短,但體現出Rx的一些特色express
=>
。你能夠把它想象成一個數據流的指向,咱們從箭頭左方取得數據流,在右方作一系列處理後或者輸出成另外一個數據流或者作一些其餘對於數據的操做。from
, zip
都是操做符。Rx中有太多的操做符,從大類上講分爲:建立類操做符、變換類操做符、過濾類操做符、合併類操做符、錯誤處理類操做符、工具類操做符、條件型操做符、數學和彙集類操做符、鏈接型操做符等等。仍是從例子開始,咱們逐漸的來熟悉Rx。
爲了更直觀的看到Rx的效果,推薦你們去JSBin這個在線Javascript IDE 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事件的值。咱們從這個例子中能夠觀察到幾個現象:
若是觀察的足夠仔細的話,你會發現console中輸出的值實際上是 input.target.value
,咱們觀察的對象實際上是id爲todo的這個對象上發生的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倍,造成一個新的數據流。
最多見的兩個操做符咱們上面已經瞭解了,咱們繼續再來認識新的操做符。相似 .map(ev=>ev.target.value)
的場景太多了,以致於rxjs團隊搞出來一個專門的操做符來應對,這個操做符就是 pluck
。這個操做符專業從事從一系列嵌套的屬性種把值提取出來造成新的流。好比上面的例子能夠改寫成下面的代碼,效果是同樣的。那麼若是其中某個屬性爲空怎麼辦?這個操做符負責返回一個 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
,咱們合併了按鈕點擊事件的數據流和文本框輸入事件的數據流,而且返回一個對象,這個對象有兩個屬性,第一個是按鈕事件數據流的值,第二個是文本輸入事件數據流的值。也就是說應該是相似 { 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的數據流發射時,源2沒有數據,這時候結果流也不會有數據產生,當源2發射第一個數據(圖中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,這時你發現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);複製代碼
除了 combineLatest
,Rxjs還提供了多個合併類的操做符,咱們再試驗一個 zip
操做符。 zip
和 combineLatest
很是像,但重要的區別點在於 zip
嚴格的須要多個源數據流中的每個的相同順序的元素配對。
好比說仍是上面的例子,zip
要求源1的第一個數據和源2的第一個數據組成一對,產生結果流的第一個數據;源1的第二個數據和源2的第二個數據組成一對,產生結果流的第二個數據。而 combineLatest
不須要等待另外一個源數據流產生數據,只要有一個產生,結果流就會產生。
zip
這個詞在英文中有拉鍊的意思,記住這個有助於咱們理解這個操做符,就像拉鍊同樣,它須要拉鍊兩邊的齒一一對應。從效果角度上講,這個操做符有減緩發射速度的做用,由於它會等待合併序列中最慢的那個。
下面咱們仍是看個例子,在我寫第七章的使用Bing Image API變換背景時,我最開始的想法是取得圖片數組後,把這個數組中的元素每隔一段時間發送出去一個,這樣組件端就不用關心圖片變化的邏輯,只要服務發射一個地址,我就加載就好了。我就是用zip來實現的,咱們在這個邏輯中有2個源數據流:基於一個數組生成的數據流以及一個時間間隔數據流。前者的發射速度很是快,後者則速度均勻,咱們但願按後者的速度對齊前者,以達到每隔一段時間發射前者的數據的目的。
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個合併類操做符了。其餘的操做符你們能夠去 reactivex.io/documentati… 查看,注意不是全部的操做符RxJS都有。並且RxJS 5.0 目前總體的趨勢是減小沒必要要的以及冗餘的操做符,因此咱們只介紹最經常使用的一些操做符。
一般來說,Rx團隊不鼓勵新手本身從0開始建立Observable,由於狀態太複雜,會遺漏一些問題。Rx鼓勵的是經過已有的大量建立類轉換操做符來去創建Observable。咱們其實以前已經見過一些了,包括 from
和 fromEvent
。
from
能夠支持從數組、相似數組的對象、Promise、iterable 對象或相似Observable的對象(其實這個主要指ES2015中的Observable)來建立一個Observable。
這個操做符應該是能夠建立Observable的操做符中最常使用的一個,由於它幾乎能夠把任何對象轉換成Observable。
var array = [10, 20, 30];
var result$ = Rx.Observable.from(array);
result$.subscribe(x => console.log(x));複製代碼
這個操做符是專門爲事件轉換成Observable而製做的,很是強大且方便。對於前端來講,這個方法用於處理各類DOM中的事件再方便不過了。
var click$ = Rx.Observable.fromEvent(document, 'click');
click$.subscribe(x => console.log(x));複製代碼
咱們常常會遇到一些已有的代碼,這些代碼和類庫每每不受咱們的控制,沒法重構或代價太大。咱們須要在這種狀況下能夠利用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
是直到有訂閱者以後才建立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;
}複製代碼
Rx提供內建的能夠建立和計時器相關的Observable方法,第一個是Interval,它能夠在指定時間間隔發送整數的自增加序列。
例以下面代碼,咱們每隔500毫秒發送一個整數,這個數列是無窮的,咱們取前三個好了:
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');
});複製代碼
那麼輸出是
這裏你們可能注意到咱們沒有采用箭頭的方式,而是用傳統的寫法,寫了 function(x){...}
,哪一種方式其實均可以,箭頭方式會更簡單。
另外一個須要注意的地方是,在subscribe方法中咱們多了2個參數:一個處理異常,一個處理完成。Rx認爲全部的數據流會有三個狀態:next,error和completed。這三個函數就是分別處理這三種狀態的,固然若是咱們不寫某個狀態的處理,也就意味着咱們認爲此狀態不須要特別處理。並且有些序列是沒有completed狀態的,由於是無限序列。本例中,若是咱們去掉 .take(3)
那麼completed是永遠沒法觸發的。
下面咱們來看看Timer,一共有2種形式的Timer,一種是指定時間後返回一個序列中只有一個元素(值爲0)的Observable。
//這裏指定一開始的delay時間
//也能夠輸入一個Date,好比「2016-12-31 20:00:00」
//這樣變成了在指定的時間觸發
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'));複製代碼
固然還有其餘建立類的操做符,你們能夠去 reactivex.io/documentati… 查閱自行試驗一下。
以前咱們見過好幾個過濾類操做符:filter,distinct,take和debounce。
Filter操做只容許數據流中知足其predicate測試的元素髮射出去,這個predicate函數接受3個參數:
以下的代碼將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'));複製代碼
對於一些發射頻率比較高的數據流,咱們有時會想給它安個「整流器」。好比在一個搜索框中,輸入一些字符後但願出現一些搜索建議,這是個很是好的功能,不少時候能夠減小用戶的輸入。
可是因爲這些搜索建議須要聯網完成數據的傳遞,若是太頻繁操做的話,對於用戶的數據流量和服務器的性能承載都是有反作用的。因此咱們通常但願在用戶連續快速輸入時不去搜索,而是等待有相對較長的間隔時再去搜索。
下面的代碼從輸入上作了這樣的一個「整流器」,濾掉了間隔時間小於400毫米的輸入事件(輸入自己不受影響),只有用戶出現較明顯的停頓時才把輸入值發射出來。
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中對於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操做符,在每次訂閱前去記錄就會發現一些問題。當咱們離開頁面再回來,每次進入都會建立一個新的訂閱,,但原有的沒有釋放。
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。
如今看起來仍是挺麻煩的,有沒有更簡單的方法呢?答案固然是確定的: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的威力。首先咱們把TodoService中原來採用的Promise方式都替換成Observable的方式。
在進行改動以前,咱們來從新分析一下邏輯:咱們原有的實現方式中,組件中保留了一個todos數組的本地拷貝,服務器API邏輯在Service中完成。其實組件最好不關心邏輯,即便是本地拷貝的邏輯,也不該該放到組件中。組件自己的數據都是監聽Service中的數據變化而獲得的。
那麼咱們應該在Service中創建本地的內存「數據庫」,咱們叫它 dataStore
吧。這個「數據庫」中只有一個「表」:todos。
//TodoService.ts
private dataStore: { // todos的內存「數據庫」
todos: Todo[]
};複製代碼
爲了讓組件能夠監聽到這個數據的變化,咱們須要一個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,它的一個特色是存儲了發射的最新的值,這樣不管什麼訂閱者訂閱時都會獲得「當前值」。咱們以前經過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
基本上已經沒有業務邏輯代碼了,只是簡單調用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有一個大概的認識了,並且也能夠參與到正式的開發項目中去了。但Angular2做爲一個完整框架,有不少細節咱們是沒有提到的,你們能夠到官方文檔 angular.cn/ 去查找和學習。
本屆代碼: github.com/wpcfan/awes…
紙書出版了,比網上內容豐富充實了,歡迎你們訂購!
京東連接:item.m.jd.com/product/120…
第一節:Angular 2.0 從0到1 (一)
第二節:Angular 2.0 從0到1 (二)
第三節:Angular 2.0 從0到1 (三)
第四節:Angular 2.0 從0到1 (四)
第五節:Angular 2.0 從0到1 (五)
第六節:Angular 2.0 從0到1 (六)
第七節:Angular 2.0 從0到1 (七)
第八節:Angular 2.0 從0到1 (八)