看到這篇文章build an image gallery using redux saga,覺得寫的不錯,長短也適中. 文后有注釋版的github代碼庫,請使用comment分枝. Flickr API可能需要有fQ的基本能力.可以使用google的翻譯作為參考,這篇文章google翻譯版的中文水平讓我吃了一驚.
翻譯已經(jīng)完成.
使用React,Redux和reudx-saga構(gòu)建一個圖像瀏覽程序(翻譯)
Joel Hooks ,2016年3月
構(gòu)建一個圖片長廊
圖像長廊是一個簡單的程序,從Flicker API 加載圖片URLs,允許用戶查看圖片詳情。
后續(xù)我們會使用React,Redux和redux-saga.React作為核心框架,優(yōu)勢是虛擬dom(virtual-dom)的實現(xiàn)。Redux在程序內(nèi)負責(zé)state的管理。最后,我們會使用redux-saga來執(zhí)行javascript的異步操作步驟。
我們會使用ES6(箭頭函數(shù),模塊,和模板字符串),所以我們首先需要做一些項目的配置工作。
項目配置和自動化
如果要開始一個React項目,須有有一系列的配置選項。對于一個簡單的項目,我想把配置選項盡可能縮減。考慮到瀏覽器的版本問題,使用Babel把ES6編譯為ES5。
首先使用npm init 創(chuàng)建一個package.json
文件
package.json
{
"name": "egghead-react-redux-image-gallery",
"version": "0.0.1",
"description": "Redux Saga beginner tutorial",
"main": "src/main.js",
"scripts": {
"test": "babel-node ./src/saga.spec.js | tap-spec",
"start": "budo ./src/main.js:build.js --dir ./src --verbose --live -- -t babelify"
},
"repository": {
"type": "git",
"url": "git+https://github.com/joelhooks/egghead-react-redux-image-gallery.git"
},
"author": "Joel Hooks <joelhooks@gmail.com>",
"license": "MIT",
"dependencies": {
"babel-polyfill": "6.3.14",
"react": "^0.14.3",
"react-dom": "^0.14.3",
"react-redux": "^4.4.1",
"redux": "^3.3.1",
"redux-saga": "^0.8.0"
},
"devDependencies": {
"babel-cli": "^6.1.18",
"babel-core": "6.4.0",
"babel-preset-es2015": "^6.1.18",
"babel-preset-react": "^6.1.18",
"babel-preset-stage-2": "^6.1.18",
"babelify": "^7.2.0",
"browserify": "^13.0.0",
"budo": "^8.0.4",
"tap-spec": "^4.1.1",
"tape": "^4.2.2"
}
}
有了package.json
, 可以在項目文件夾命令行運行 npm install
安裝程序需要的依賴項。
.babelrc
{
"presets": ["es2015", "react", "stage-2"]
}
這個文件告訴babel,我們將會使用ES2015(ES6),React以及ES2106的stage-2的一些特征。
package.json
有兩個標準的script腳本配置:start
和test
.現(xiàn)在我們想通過start腳本加載程序,start會使用src
目錄的一些文件,所以西藥先創(chuàng)建src
文件夾.在src
文件夾添加下面的一些文:
index.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>egghead: React Redux Image Gallery</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="title">

<h3>Egghead Image Gallery</h3>
</div>
<div id="root"></div>
<script type="text/javascript" src="build.js"></script>
</body>
</html>
main.js
import "babel-polyfill"
import React from 'react'
import ReactDOM from 'react-dom'
ReactDOM.render(
<h1>Hello React!</h1>,
document.getElementById('root')
);
style.css
body {
font-family: Helvetica, Arial, Sans-Serif, sans-serif;
background: white;
}
.title {
display: flex;
padding: 2px;
}
.egghead {
width: 30px;
padding: 5px;
}
.image-gallery {
width: 300px;
display: flex;
flex-direction: column;
border: 1px solid darkgray;
}
.gallery-image {
height: 250px;
display: flex;
align-items: center;
justify-content: center;
}
.gallery-image img {
width: 100%;
max-height: 250px;
}
.image-scroller {
display: flex;
justify-content: space-around;
overflow: auto;
overflow-y: hidden;
}
.image-scroller img {
width: 50px;
height: 50px;
padding: 1px;
border: 1px solid black;
}
index.html
文件加載style.css
文件提供一些基本的布局樣式,同時也加載build.js
文件,這是一個生成出來的文件.main.js
是一個最基礎(chǔ)的React程序,他在index.html
的#root
元素中渲染一個h1
元素。創(chuàng)建這些文件以后,在項目文件夾中命令行運行npm start
。在瀏覽器打開http://10.11.12.1:9966
.就可以看到index.html
中渲染的頁面
現(xiàn)在我們來構(gòu)建基礎(chǔ)的Gallery
React 組件
在Gallery中顯示一些圖片
首先我們需要盡可能快的獲得一個可以顯示的圖片素材.在項目文件夾中創(chuàng)建一個文件Gallery.js
Gallery.js
import React, {Component} from 'react'
const flickrImages = [
"https://farm2.staticflickr.com/1553/25266806624_fdd55cecbc.jpg",
"https://farm2.staticflickr.com/1581/25283151224_50f8da511e.jpg",
"https://farm2.staticflickr.com/1653/25265109363_f204ea7b54.jpg",
"https://farm2.staticflickr.com/1571/25911417225_a74c8041b0.jpg",
"https://farm2.staticflickr.com/1450/25888412766_44745cbca3.jpg"
];
export default class Gallery extends Component {
constructor(props) {
super(props);
this.state = {
images: flickrImages,
selectedImage: flickrImages[0]
}
}
render() {
const {images, selectedImage} = this.state;
return (
<div className="image-gallery">
<div className="gallery-image">
<div>
<img src={selectedImage} />
</div>
</div>
<div className="image-scroller">
{images.map((image, index) => (
<div key={index}>
<img src={image}/>
</div>
))}
</div>
</div>
)
}
}
我們直接在組件中硬編碼了一個提供數(shù)據(jù)的數(shù)組,讓項目盡快的工作起來.Gallery組件
繼承Component組件
,在構(gòu)造函數(shù)中創(chuàng)建一些組件的出事狀態(tài).最后我們利用一些樣式標記渲染一下文件。image-scroller
元素遍歷(map
方法)圖片數(shù)組,生成摘要小圖片。
import "babel-polyfill"
import React from 'react'
import ReactDOM from 'react-dom'
+ import Gallery from './Gallery'
ReactDOM.render(
- <h1>Hello React!</h1>,
+ <Gallery />,
document.getElementById('root')
);
到現(xiàn)在,我們使用硬編碼的圖片URLs(通過fickrImages)數(shù)組,第一張圖片作為selectedImage
.這些屬性在Gallery
組件的構(gòu)造函數(shù)缺省配置中,通過初始狀態(tài)(initial)來設(shè)定.
接下來在組件中添加一個和組件進行交互操作的方法,方法具體內(nèi)容是操做setSate
.
Gallery.js
export default class Gallery extends Component {
constructor(props) {
super(props);
this.state = {
images: flickrImages,
selectedImage: flickrImages[0]
}
}
+ handleThumbClick(selectedImage) {
+ this.setState({
+ selectedImage
+ })
+ }
render() {
const {images, selectedImage} = this.state;
return (
<div className="image-gallery">
<div className="gallery-image">
<div>
<img src={selectedImage} />
</div>
</div>
<div className="image-scroller">
{images.map((image, index) => (
- <div key={index}>
+ <div key={index} onClick={this.handleThumbClick.bind(this,image)}>
<img src={image}/>
</div>
))}
</div>
</div>
)
}
}
在Gallery組件
添加handleThumbClick
方法,任何元素都可用通過onClick
屬性調(diào)用這個方法.image
作為第二個參數(shù)傳遞,元素自身作為第一個參數(shù)傳遞.bind方法傳遞javascript函數(shù)調(diào)用上下文對象是非常便捷。
看起來不錯!現(xiàn)在我們有了一些交互操作的方法,有點“APP”的意思了。截止目前,我們已經(jīng)讓app運行起來了,接下來要考慮怎么加載遠程數(shù)據(jù)。最容易加載遠程數(shù)據(jù)的地方是一個React組件
生命周期方法,我們使用componentDidMount
方法,通過他從Flikr API
請求并加載一些圖片.
Gallery.js
export default class Gallery extends Component {
constructor(props) {
super(props);
this.state = {
images: flickrImages,
selectedImage: flickrImages[0]
}
}
+ componentDidMount() {
+ const API_KEY = 'a46a979f39c49975dbdd23b378e6d3d5';
+ const API_ENDPOINT = `https://api.flickr.com/services/rest/?method=flickr.interestingness.+getList&api_key=${API_KEY}&format=json&nojsoncallback=1&per_page=5`;+
+
+ fetch(API_ENDPOINT).then((response) => {
+ return response.json().then((json) => {
+ const images = json.photos.photo.map(({farm, server, id, secret}) => {
+ return `https://farm${farm}.staticflickr.com/${server}/${id}_${secret}.jpg`
+ });
+
+ this.setState({images, selectedImage: images[0]});
+ })
+ })
+ }
[...]
我們在Gallery
類中添加了一個新的方法,通過React的componentDidMount
生命周期方法觸發(fā)Flickr圖片數(shù)據(jù)的獲取。
在React
組件運行的不同時間點,組件會調(diào)用不同的生命周期函數(shù)。在這段代碼中,當(dāng)組件被渲染到DOM
中的時間點,componentDidMount
函數(shù)就會被調(diào)用。需要注意的是:Gallery
組件只有一次渲染到DOM
的機會,所以這個函數(shù)可以提供一些初始化圖片.考慮到在APP的整個生命周期中,有更多的動態(tài)組件的加載和卸載,這可能會造成一些多余的調(diào)用和無法考慮到的結(jié)果。
我們使用瀏覽器接口(browser API)的fetch
方法執(zhí)行請求.Fetch返回一個promise對象解析response
對象.調(diào)用response.json()
方法,返回另一個promise對象,這就是我們實際需要的json
格式的數(shù)據(jù).遍歷這個對象以后就可以獲取圖片的url地址.
坦白講,這個應(yīng)用目前還很簡單.我們還需要在這里花費更多的時間,還有一些基礎(chǔ)的需求需要完成.或許我們應(yīng)該在promise處理流程中添加錯誤處理方法,如果圖片數(shù)據(jù)獲取成功也需要一些處理邏輯.在這個地方,你需要發(fā)揮一些想象力考慮一下更多的邏輯.在生產(chǎn)實踐中簡單的需求是很少見的.很快,應(yīng)用中就會添加更多的需求。認證,滾動櫥窗,加載不同圖片庫的能力和圖片的設(shè)置等等.僅僅這些還遠遠不夠.
我們已經(jīng)使用React
構(gòu)建了一個加載圖片庫的程序。接下來我們需要考慮到隨著程序功能的添加,到底需要哪些基礎(chǔ)的模式.首先考慮到的一個問題就是要把應(yīng)用的狀態(tài)(state)控制從Gallery
組件中分離出來.
我們通過引入Redux
來完成應(yīng)用的狀態(tài)管理工作。
使用Redux
來管理狀態(tài)
在你的應(yīng)用中只要使用了setState
方法都會讓一個組件從無狀態(tài)變?yōu)橛袪顟B(tài)的組件.糟糕的是這個方法會導(dǎo)致應(yīng)用中出現(xiàn)一些令人困惑的代碼,這些代碼會在應(yīng)用中到處蔓延。
Flux
構(gòu)架來減輕這個問題.Flux
把邏輯(logic)和狀態(tài)(state)遷移到Store
中.應(yīng)用中的動作(Actions
)被Dispatch
的時候,Stores
會做相應(yīng)的更新.Stores
的更新會觸發(fā)View
根據(jù)新狀態(tài)的渲染.
那么我們?yōu)槭裁匆釛?code>Flux?他竟然還是“官方”構(gòu)建的.
好吧!Redux
是基于Flux
構(gòu)架的,但是他有一些獨特的優(yōu)勢.下面是Dan Abramov(Redux創(chuàng)建者)的一些話:
Redux和Flux沒有什么不同.總體來講他們是相同的構(gòu)架,但是Redux通過功能組合把Flux使用回調(diào)注冊的復(fù)雜點給屏蔽掉了.
兩個構(gòu)架從更本上講沒有什么不同,但是我發(fā)現(xiàn)Redux使一些在Flux比較難實現(xiàn)的邏輯更容易實現(xiàn).
Redux文檔非常棒.
如果你還沒有讀過代碼的卡通教程或者Dan的系列文章.趕快去看看吧!
啟動Redux
第一件需要做的事事初始化Redux
,讓他在我們的程序中運行起來.現(xiàn)在不需要做安裝工作,剛開始運行npm install
的時候已經(jīng)安裝好了依賴項,我們需要做一些導(dǎo)入和配置工作.
reducer函數(shù)是Redux的大腦. 每當(dāng)應(yīng)用分發(fā)(或派遣,dispatch)一個操作(action)的時候,reducer
函數(shù)會接受操作(action)并且依據(jù)這個動作(action)創(chuàng)建reducer
自己的state
.因為reducers
是純函數(shù),他們可以組合到一起,創(chuàng)建應(yīng)用的一個完整state
.讓我們在src
中創(chuàng)建一個簡單的reducer:
reducer.js
export default function images(state, action) {
console.log(state, action)
return state;
}
一個reducer函數(shù)接受兩個參數(shù)(arguments).
- [x]
state
-這個數(shù)據(jù)代表應(yīng)用的狀態(tài)(state).reducer函數(shù)使用這個狀態(tài)來構(gòu)建一個reducer自己可以管理的狀態(tài).如果狀態(tài)沒有發(fā)生改變,reducer會返回輸入的狀態(tài). - [x]
action
-這是觸發(fā)reducer的事件.Actions通過store派發(fā)(dispatch),由reducer處理.action需要一個type
屬性來告訴reducer怎么處理state.
目前,images
reuducer在終端中打印出日志記錄,表明工作流程是正常的,可以做接下來的工作了.為了使用reducer,需要在main.js
中做一些配置工作:
main.js
import "babel-polyfill";
import React from 'react';
import ReactDOM from 'react-dom';
import Gallery from './Gallery';
+ import { createStore } from 'redux'
+ import reducer from './reducer'
+ const store = createStore(reducer);
+ import {Provider} from 'react-redux';
ReactDOM.render(
+ <Provider store={store}>
<Gallery />
+ </Provider>,
document.getElementById('root')
);
}
我們從Redux
庫中導(dǎo)入createStore
組件.creatStore
用來創(chuàng)建Redux的store.大多數(shù)情況下,我們不會和store直接交互,store在Redux中做幕后管理工作.
也需要導(dǎo)入剛才創(chuàng)建的reducer函數(shù),以便于他可以被發(fā)送到store.
我們將通過createStore(reducer)
操作,利用reducer來配置應(yīng)用的store.這個示例僅僅只有一個reducer,但是createStore
可以接收多個reducer作為參數(shù).稍后我們會看到這一點.
最后我們導(dǎo)入高度集成化的組件Provider
,這個組件用來包裝Gallery
,以便于我們在應(yīng)用中使用Redux.我們需要把剛剛創(chuàng)建的store傳遞給Provider
.你也可以不使用Provider
,實際上Redux可以不需要React.但是我們將會使用Provider
,因為他非常便于使用.
這張圖可能有點古怪,但是展示了Redux的一個有意思的地方.所有的reducers接收在應(yīng)用中的全部actions(動作或操作).在這個例子中我們可以看到Redux自己派發(fā)的一個action
.
連接Gallery組件
借助Redux,我們將使用”connected”和“un-connected”組件.一個connected
組件被連線到store.connected
組件使控制動作事件(controls action event)和store協(xié)作起來.通常,一個connected
組件有子組件,子組件具有單純的接收輸入和渲染功能,當(dāng)數(shù)據(jù)更新時執(zhí)行調(diào)用.這個子組件就是unconnected組件.
提示:當(dāng)Rect和Redux配合是工作的非常好,但是Redux不是非要和React在一起才能工作.沒有React,Redux其實可以和其他框架配合使用.
在應(yīng)用中需要關(guān)聯(lián)React組件
和Redux Store
的時候,react-redux
提供了便捷的包裝器.我們把react-redux添加進Gallery
中
,從而使Gallery
成為首要的關(guān)聯(lián)組件.
Gallery.js
import React, {Component} from 'react'
+import {connect} from 'react-redux';
-export default class Gallery extends Component {
+export class Gallery extends Component {
constructor(props) {
super(props);
+ console.log(props);
this.state = {
images: []
}
}
componentDidMount() {
const API_KEY = 'a46a979f39c49975dbdd23b378e6d3d5';
const API_ENDPOINT = `https://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=${API_KEY}&format=json&nojsoncallback=1&per_page=5`;
fetch(API_ENDPOINT).then((response) => {
return response.json().then((json) => {
const images = json.photos.photo.map(({farm, server, id, secret}) => {
return `https://farm${farm}.staticflickr.com/${server}/${id}_${secret}.jpg`
});
this.setState({images, selectedImage: images[0]});
})
})
}
handleThumbClick(selectedImage) {
this.setState({
selectedImage
})
}
render() {
const {images, selectedImage} = this.state;
return (
<div className="image-gallery">
<div className="gallery-image">
<div>
<img src={selectedImage} />
</div>
</div>
<div className="image-scroller">
{images.map((image, index) => (
<div key={index} onClick={this.handleThumbClick.bind(this,image)}>
<img src={image}/>
</div>
))}
</div>
</div>
)
}
}
+export default connect()(Gallery)
從react-redux
導(dǎo)入connect
函數(shù),可以在導(dǎo)出組件的時候把他變?yōu)殒溄咏M件(connected component).請注意,connect()(Gallery)
代碼把Gallery
組件放在第二個形參中,這是因為connect()
返回一個函數(shù),這個函數(shù)接受一個React組件作為參數(shù)(argument).調(diào)用connect()
函數(shù)時需要配置項.后面我們將會傳遞配置我們應(yīng)用的actions和state參數(shù).
我們也把connect
作為默認配置到處模塊.這一點非常重要!現(xiàn)在當(dāng)我們import Gallery
的時候,就不是一個單純的React組件了,而是一個和Redux關(guān)聯(lián)的組件了.
如果你觀察我們添加進構(gòu)造器的console.log
的輸出,就可以看到Gallery
組件的屬性現(xiàn)在包括了一個dispatch
函數(shù).這個地方是connect
為我們的應(yīng)用修改的,這個改動賦予了組件把自己的動作對象(action objects)派發(fā)
到reducers
的能力.
export class Gallery extends Component {
constructor(props) {
super(props);
+ this.props.dispatch({type: 'TEST'});
this.state = {
images: []
}
}
[...]
我們可以在組件的構(gòu)造器中調(diào)用派發(fā)功能.你可以在開發(fā)者的終端中看到來自reducer的日志聲明.看到聲明表示我們已經(jīng)派發(fā)了第一個action!.Actions是一個單一的javascript對象,必需有type
屬性.Actions可以擁有任意數(shù)量和種類的其他屬性.但是type
可以讓reducers理解這些動作到底是做什么用的(意譯,意思是只有擁有type屬性,reducers才知道對state做什么樣的修改).
export default function images(state, action) {
- console.log(state, action)
+ switch(action.type) {
+ case 'TEST':
+ console.log('THIS IS ONLY A TEST')
+ }
return state;
}
總的reducers使用switch代碼塊
過濾有關(guān)的消息,Switch
語句使用actions的type屬性,當(dāng)一個action
和case
分支吻合以后,相應(yīng)的單個reducer就會執(zhí)行他的具體工作.
我們的應(yīng)用現(xiàn)在關(guān)聯(lián)到接收的動作.現(xiàn)在我們需要把Redux
-Store
提供的state
關(guān)聯(lián)到應(yīng)用中.
默認的應(yīng)用狀態(tài)(state)
reducer.js
const defaultState = {
images: []
}
export default function images(state = defaultState, action) {
switch(action.type) {
case 'TEST':
- console.log('THIS IS ONLY A TEST')
+ console.log(state, action)
+ return state;
+ default:
+ return state;
}
- return state;
}
我們創(chuàng)建一個defaultState
對象,這個對象返回一個空數(shù)組作為images的屬性.我們把images
函數(shù)的參數(shù)state
設(shè)置為默認.如果在test分支中輸出日志,將會看到state不是undefined(空數(shù)組不是undefined)!reducer需要返回應(yīng)用的當(dāng)前state.這點很重要!現(xiàn)在我們沒有做任何改變,所以僅僅返回state.注意我們在case
中添加了default分支,reducer必須要返回一個state.
在Gallery
組件中,我們也可以把state做一定的映射(map)以后再連接到應(yīng)用.
import React, {Component} from 'react'
import {connect} from 'react-redux';
export class Gallery extends Component {
constructor(props) {
super(props);
this.props.dispatch({type: 'TEST'});
+ console.log(props);
- this.state = {
- images: []
- }
}
- componentDidMount() {
- const API_KEY = 'a46a979f39c49975dbdd23b378e6d3d5';
- const API_ENDPOINT = `https://api.flickr.com/services/rest/?method=flickr.interestingness.-getList&api_key=${API_KEY}&format=json&nojsoncallback=1&per_page=5`;-
-
- fetch(API_ENDPOINT).then((response) => {
- return response.json().then((json) => {
- const images = json.photos.photo.map(({farm, server, id, secret}) => {
- return `https://farm${farm}.staticflickr.com/${server}/${id}_${secret}.jpg`
- });
-
- this.setState({images, selectedImage: images[0]});
- })
- })
- }
- handleThumbClick(selectedImage) {
- this.setState({
- selectedImage
- })
- }
render() {
- const {images, selectedImage} = this.state;
+ const {images, selectedImage} = this.props;
return (
<div className="image-gallery">
<div className="gallery-image">
<div>
<img src={selectedImage} />
</div>
</div>
<div className="image-scroller">
{images.map((image, index) => (
- <div key={index} onClick={this.handleThumbClick.bind(this,image)}>
+ <div key={index}>
<img src={image}/>
</div>
))}
</div>
</div>
)
}
}
+function mapStateToProps(state) {
+ return {
+ images: state.images
+ selectedImage: state.selectedImage
+ }
+}
-export default connect()(Gallery)
+export default connect(mapStateToProps)(Gallery)
我們將移除連接組件中的所有圖片加載和交互邏輯代碼,如果你注意看Gallery
組件的底部代碼,你會注意到,我們創(chuàng)建了一個mapStateToProps
函數(shù),接收一個state
作為參數(shù),返回一個對象,把state.images
映射為images
屬性.mapStateToProps
做為參數(shù)傳遞給connect
.
正如名字暗示的一樣,mapStateToProps
函數(shù)接收當(dāng)前應(yīng)用的state,然后把state轉(zhuǎn)變?yōu)榻M件的屬性(propertys).如果在構(gòu)造器中輸出props,將會看到images數(shù)組是reducer
返回的默認state.
const defaultState = {
- images: []
+ images: [
+ "https://farm2.staticflickr.com/1553/25266806624_fdd55cecbc.jpg",
+ "https://farm2.staticflickr.com/1581/25283151224_50f8da511e.jpg",
+ "https://farm2.staticflickr.com/1653/25265109363_f204ea7b54.jpg",
+ "https://farm2.staticflickr.com/1571/25911417225_a74c8041b0.jpg",
+ "https://farm2.staticflickr.com/1450/25888412766_44745cbca3.jpg"
+ ],
+ selectedImage: "https://farm2.staticflickr.com/1553/25266806624_fdd55cecbc.jpg"
}
export default function images(state = defaultState, action) {
switch(action.type) {
case 'TEST':
console.log(state, action)
return state;
default:
return state;
}
}
如果在defaultState
中更新images數(shù)組,你將可以看到一些圖片重新出現(xiàn)在gallery中!現(xiàn)在當(dāng)用戶點擊縮略圖的時候,我們可以反饋選擇動作,返回對應(yīng)的大圖.
更新state
怎么操作才能根據(jù)新選擇的圖片更新state?
需要配置reducer監(jiān)聽IMAGE_SELECTED
動作,借助action攜帶的信息(payload,有的文章翻譯為載荷,載荷怎么理解?手機載荷就是聲音,短信和流量數(shù)據(jù)。如果是卡車就是拉的貨物,如果是客車就乘載的乘客,action的載荷就是要讓reducer明白你要干什么,需要什么)來更新state.
const defaultState = {
images: [
"https://farm2.staticflickr.com/1553/25266806624_fdd55cecbc.jpg",
"https://farm2.staticflickr.com/1581/25283151224_50f8da511e.jpg",
"https://farm2.staticflickr.com/1653/25265109363_f204ea7b54.jpg",
"https://farm2.staticflickr.com/1571/25911417225_a74c8041b0.jpg",
"https://farm2.staticflickr.com/1450/25888412766_44745cbca3.jpg"
],
selectedImage: "https://farm2.staticflickr.com/1553/25266806624_fdd55cecbc.jpg"
}
export default function images(state = defaultState, action) {
switch(action.type) {
- case 'TEST':
case 'IMAGE_SELECTED':
- return state;
+ return {...state, selectedImage: action.image};
default:
return state;
}
}
```
___
現(xiàn)在reducer已經(jīng)準備接收`IMAGE_SELECTED` action了.在`IMAGE_SELECTED`分支選項內(nèi),我們在展開(spreading,ES6的對象操作方法),并重寫`selectedImage`屬性后,返回一個新state對象.了解更多的`...state`對象操作可以看`ruanyifeng`的書.
import React, {Component} from 'react'
import {connect} from 'react-redux';
export class Gallery extends Component {
- constructor(props) {
- super(props);
- this.props.dispatch({type: 'TEST'});
- console.log(props);
- }
render() { - const {images, selectedImage} = this.props;
- const {images, selectedImage, dispatch} = this.props;
return (
<div className="image-gallery">
<div className="gallery-image">
<div>
<img src={selectedImage} />
</div>
</div>
<div className="image-scroller">
{images.map((image, index) => (
<div key={index}>
-
)<div key={index} onClick={() => dispatch({type:'IMAGE_SELECTED', image})}> <img src={image}/> </div> ))} </div> </div>
}
}
function mapStateToProps(state) {
return {
images: state.images,
selectedImage: state.selectedImage
}
}
export default connect(mapStateToProps)(Gallery)
___
在`Gallery`組件中,我們將會在組件的屬性中定義`dispatch`在`onClick`函數(shù)體中調(diào)用他,現(xiàn)在我們從便利角度考慮把他們放在一起,但是兩者功能是一樣的.一旦我們點擊了縮略圖,他將會通過reducer更新大圖.
使用dispatch可以很方便的創(chuàng)建通用actions,但是很快我們會需要重用命名好的actions.為了這樣做,可以使用”action creators”.
####Action Creators
Action creators函數(shù)返回配置好的action對象.我們在`action.js`中添加第一個action creator.
action.js
export const IMAGE_SELECTED = 'IMAGE_SELECTED';
export function selectImage(image) {
return {
type: IMAGE_SELECTED,
image
}
}
___
這個方法經(jīng)過export以后,可以直接在任何需要創(chuàng)建`selectImage` action地方導(dǎo)入!`selectImage`是純函數(shù),只能返回數(shù)據(jù).他接收一個image作為參數(shù),把image添加到action對象中,并返回.
>注意:我們正在返回一個單純的javascript object,但是`image`的屬性可能很古怪,如果你以前沒有碰到這樣的樣式.從ES6的角度出發(fā),如果你給一個對象傳遞一個類似這樣的屬性,隱含的意思是把`image:'任何image包含的值'`添加到最終返回的對象.超級好用!
import * as GalleryActions from './actions.js';
[...]
onClick={() => dispatch(GalleryActions.selectImage(image))}
___
this isn’t much than just using `dispatchti` though.
幸運的是,這個模式很普遍,Redux在`bindActionCreators`函數(shù)里提供了一個更好的辦法來完成這個功能.
import React, {Component} from 'react'
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import * as GalleryActions from './actions.js';
export class Gallery extends Component {
constructor(props) {
super(props);
this.props.dispatch({type: 'TEST'});
console.log(props);
}
handleThumbClick(selectedImage) {
this.setState({
selectedImage
})
}
render() {
- const {images, selectedImage, dispatch} = this.props;
- const {images, selectedImage, selectImage} = this.props;
return (
<div className="image-gallery">
<div className="gallery-image">
<div>
<img src={selectedImage} />
</div>
</div>
<div className="image-scroller">
{images.map((image, index) => (
<div key={index} onClick={() => dispatch({type:'IMAGE_SELECTED', image})}>
-
)<div key={index} onClick={() => selectImage(image)}> <img src={image}/> </div> ))} </div> </div>
}
}
function mapStateToProps(state) {
return {
images: state.images,
selectedImage: state.selectedImage
}
}
+function mapActionCreatorsToProps(dispatch) {
- return bindActionCreators(GalleryActions, dispatch);
+}
-export default connect(mapStateToProps)(Gallery)
+export default connect(mapStateToProps, mapActionCreatorsToProps)(Gallery)
___
我們已經(jīng)添加了`mapActionCreatorsToProps`函數(shù),他接收`dispatch`函數(shù)作為參數(shù).返回`bindActionCreators`的調(diào)用結(jié)果,`GalleryActions`作為`bindActionCreators`的參數(shù).現(xiàn)在如果你輸出屬性日志,就看不到`dispatch`作為參數(shù),`selectImage`直接可以使用了.(這里相當(dāng)于對dispatch和action進行了包裝).
現(xiàn)在回顧一下,我們做了幾件事:
- 創(chuàng)建了一個reducer包含應(yīng)用的默認初始狀態(tài)(initial state),并且監(jiān)聽actions的執(zhí)行.
- 創(chuàng)建了一個store,把reducer具體化,提供一個分發(fā)器(dispatcher)可以分發(fā)action.
- 把我們的Gallery組件關(guān)聯(lián)到store的state.
- 把store的state映射為屬性(property),傳遞給Gallery.
- 映射一個動作創(chuàng)建器,Gallery可以簡單的調(diào)用`selectImage(image)`,分發(fā)動作,應(yīng)用狀態(tài)將會更新.
那么,我們怎么才能使用這些模式從遠程資源加載數(shù)據(jù)呢?
這個過程將會非常有趣!
####異步活動?
---
你可能在參加函數(shù)式編程的時候聽說過”副作用”(side effects)這個名詞,side effects是發(fā)生在應(yīng)用的范圍之外的東西.在我們舒適的肥皂泡里,side effect根本不是問題,但是當(dāng)我們要到達一個遠程資源,肥皂泡就被穿透了.有些事情我們就控制不了了,我們必須接受這個事實.(根據(jù)這段話,side effect 翻譯為意想不到的事情,出乎意料的不受控制的事情更好)
在Redux里,reducer沒有Side effects.這意味著reducers不處理我們應(yīng)用中的異步活動.我們不能使用reducers加載遠程數(shù)據(jù),因為reducers是純函數(shù),沒有side effects.
Redux很棒,如果你的應(yīng)用里沒有任何異步活動,你可以停下來,不用再往下看了.
如果你創(chuàng)建的應(yīng)用比較大,可能你會從服務(wù)端加載數(shù)據(jù),這時,當(dāng)然要使用異步方式.
>**注意**: Redux其中一個最酷的地方是他非常小巧.他試圖解決有限范圍內(nèi)的問題.大多數(shù)的應(yīng)用需要解決很多問題!萬幸,Reduc提供中間件概念,中間件存在于action->reducer->store的三角關(guān)系中,通過中間件的方式,可以導(dǎo)入諸如遠程數(shù)據(jù)異步加載類似的功能.
其中一個方法是使用`thunks`對象,在Redux中有 redux-thunk 中間件.Thunks非常厲害,但是可能會導(dǎo)致actions的序列很復(fù)雜,測試起來也是很大的挑戰(zhàn).
考慮到我們的 圖片瀏覽程序.當(dāng)應(yīng)用加載是,需要做:
- 從服務(wù)器請求圖片數(shù)組
- 當(dāng)圖片加載完畢,顯示提示消息
- 當(dāng)遠程數(shù)據(jù)返回以后,選擇初始圖片顯示
- 處理可能出現(xiàn)的錯誤
這些事件都要在用戶點擊應(yīng)用里的任何元素之前完成!
我們該怎么做呢?
redux-saga就是為此而誕生,為我們的應(yīng)用提供絕佳的服務(wù).
redux-sage
___
redux-sage可以在Redux應(yīng)用中操作異步actions.他提供中間件和趁手的方法使構(gòu)建復(fù)雜的異步操作流程輕而易舉.
一個saga是一個Generator(生成器),Generator函數(shù)是ES2015新添加的特性.可能是你第一次遇到Generator函數(shù),這樣你會覺得有點古怪,可以參考(ruanyifeng文章).不要苦惱,如果你對此仍然很抓耳撓腮.使用redux-sage你不需要javascript異步編程的博士學(xué)位.
因為使用了generators的緣故,我們能創(chuàng)建一個順序執(zhí)行的命令序列,用來描述復(fù)雜的異步操作流程(workflows).整個圖片的加載流程序列如下:
export function* loadImages() {
try {
const images = yield call(fetchImages);
yield put({type: 'IMAGES_LOADED', images})
yield put({type: 'IMAGE_SELECTED', image: images[0]})
} catch(error) {
yield put({type: 'IMAGE_LOAD_FAILURE', error})
}
}
export function* watchForLoadImages() {
while(true) {
yield take('LOAD_IMAGES');
yield call(loadImages);
}
}
___
####第一個saga
我們將開始一個簡單的saga實例,然后配置他連接到我們的應(yīng)用.在`src`創(chuàng)建一個文件
`saga.js`
export function* sayHello() {
console.log('hello');
}
___
我們的saga是一個簡單的generator函數(shù).函數(shù)后面的`*`作為標志,他也被叫做”super star”.
現(xiàn)在在`main.js`文件中導(dǎo)入新函數(shù),并且執(zhí)行他.
import "babel-polyfill";
import React from 'react';
import ReactDOM from 'react-dom';
import Gallery from './Gallery';
import { createStore } from 'redux'
import {Provider} from 'react-redux';
import reducer from './reducer'
+import {sayHello} from './sagas';
+sayHello();
const store = createStore(reducer);
ReactDOM.render(
<Provider store={store}>
<Gallery />
</Provider>,
document.getElementById('root')
);
___
不管你盯住終端多長時間,“hello”永遠不會出現(xiàn).
這是因為`sayHello`是一個generator!Generator 不會立即執(zhí)行.如果你把代碼該為`sayHello().next();`你的“hello”就出現(xiàn)了.不用擔(dān)心,我們不會總是調(diào)用`next`.正如Redux,redux-saga用來消除應(yīng)用開發(fā)中的痛苦.
配置 redux-sage
___
import "babel-polyfill";
import React from 'react';
import ReactDOM from 'react-dom';
import Gallery from './Gallery';
-import { createStore } from 'redux'
+import { createStore, applyMiddleware } from 'redux'
+import createSagaMiddleware from 'redux-saga'
import {Provider} from 'react-redux';
import reducer from './reducer'
import {sayHello} from './sagas';
-sayHello()
-const store = createStore(reducer);
+const store = createStore(
- reducer,
- applyMiddleware(createSagaMiddleware(sayHello))
+);
ReactDOM.render(
<Provider store={store}>
<Gallery />
</Provider>,
document.getElementById('root')
);
___
我們已從Redux導(dǎo)入了`applyMiddleware`函數(shù).從redux-saga導(dǎo)入`createSagaMiddleware`函數(shù).當(dāng)我們創(chuàng)建store的時候,我們需要通過中間件提供Redux需要的功能.在這個實例中,我們會調(diào)用`applyMiddleware`函數(shù),這個函數(shù)返回`createSagaMiddleware(sayHello)`的結(jié)果.在幕后,redux-saga加載`sayHello`函數(shù),儀式性的調(diào)用`next`函數(shù).
應(yīng)該可以在終端中看到提示消息了.
現(xiàn)在讓我們構(gòu)建加載圖片的saga
####通過Saga加載圖片數(shù)據(jù)
___
我們將刪除出sayHello saga,使用`loadImages` saga
-export function* sayHello() {
- console.log('hello');
-}
+export function* loadImages() {
- console.log('load some images please')
+}
___
不要忘了更新`main.js`
import "babel-polyfill";
import React from 'react';
import ReactDOM from 'react-dom';
import Gallery from './Gallery';
import { createStore, applyMiddleware } from 'redux'
import {Provider} from 'react-redux';
import createSagaMiddleware from 'redux-saga'
import reducer from './reducer'
-import {sayHello} from './sagas';
+import {loadImages} from './sagas';
const store = createStore(
reducer,
- applyMiddleware(createSagaMiddleware(sayHello))
- applyMiddleware(createSagaMiddleware(loadImages))
);
ReactDOM.render(
<Provider store={store}>
<Gallery />
</Provider>,
document.getElementById('root')
);
___
現(xiàn)在saga已經(jīng)加載,在`saga.js`中添加`fetchImages`方法
const API_KEY = 'a46a979f39c49975dbdd23b378e6d3d5';
const API_ENDPOINT = https://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=${API_KEY}&format=json&nojsoncallback=1&per_page=5
;
const fetchImages = () => {
return fetch(API_ENDPOINT).then(function (response) {
return response.json().then(function (json) {
return json.photos.photo.map(
({farm, server, id, secret}) => https://farm${farm}.staticflickr.com/${server}/${id}_${secret}.jpg
);
})
})
};
export function* loadImages() {
const images = yield fetchImages();
console.log(images)
}
___
`fetchImages`方法返回一個promise對象.我們將調(diào)用`fetchImages`,但是現(xiàn)在我們要使用`yield`關(guān)鍵字.通過黑暗藝術(shù)和巫術(shù),generators理解Promise對象,正如終端輸出的日志顯示,我們已經(jīng)收獲了一個圖片URLs的數(shù)組.看看`loadImages`的代碼,他看起來像是典型的同步操作代碼.`yield`關(guān)鍵字是秘制調(diào)味醬,讓我們的代碼用同步格式執(zhí)行異步操作活動.
___
####封裝我們的異步API請求.
首先來定義一下需要使用的api.他沒有什么特殊的地方,實際上他和早先加載Flickr images的代碼是相同的.我們創(chuàng)建`flickr.js`文件
const API_KEY = 'a46a979f39c49975dbdd23b378e6d3d5';
const API_ENDPOINT = https://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=${API_KEY}&format=json&nojsoncallback=1&per_page=5
;
export const fetchImages = () => {
return fetch(API_ENDPOINT).then(function (response) {
return response.json().then(function (json) {
return json.photos.photo.map(
({farm, server, id, secret}) => https://farm${farm}.staticflickr.com/${server}/${id}_${secret}.jpg
);
})
})
};
___
嚴格意義上來說,不需要這么做,但是這會帶來一定的好處.我們處在應(yīng)用的邊緣(boundaries of our application,意思是說在這里的代碼可能是很多和遠程服務(wù)器交互的代碼,可能邏輯會很復(fù)雜),事情都有點亂.通過封裝和遠程API交互的邏輯,我們的代碼將會很整潔,很容易更新.如果需要抹掉圖片服務(wù)也會出奇的簡單.
我們的`saga.js`看起來是這個樣子:
import {fetchImages} from './flickr';
export function* loadImages() {
const images = yield fetchImages();
console.log(images)
}
___
我們?nèi)匀恍枰趕aga外獲取數(shù)據(jù),并且進入應(yīng)用的state(使用異步獲取的遠程數(shù)據(jù)更新state).為了處理這個問題,我們將使用”effects”.
####從saga來更新應(yīng)用
我們可以通過`dispatch`或者store作為參數(shù)來調(diào)用saga,但是這個方法時間一長就會給人造成些許的困擾.我們選擇采用redux-saga提供的`put`方法.
首先我們更新`reducer.js`操作一個新的action類型`IMAGES_LOADED`.
const defaultState = {
- images: []
}
export default function images(state = defaultState, action) {
switch(action.type) {
case 'IMAGE_SELECTED':
return {...state, selectedImage: action.image};
- case 'IMAGES_LOADED':
-
default:return {...state, images: action.images};
return state;
}
}
import {fetchImages} from './flickr';___ 我們添加了新的分支,并從`defaultState`中刪除了硬編碼的URLs數(shù)據(jù).`IMAGES_LOADED`分支現(xiàn)在返回一個更新的state,包含action的image數(shù)據(jù). 下一步我們更新saga:
+import {put} from 'redux-saga/effects';
export function* loadImages() {
const images = yield fetchImages();
- yield put({type: 'IMAGES_LOADED', images})
}
___
導(dǎo)入`put`以后,我們在`loadImages`添加另外一行.他`yield` `put`函數(shù)調(diào)用的返回結(jié)果.在幕后,redux-saga 分發(fā)這些動作,reducer接收到了消息!
怎樣才能使用特定類型的action來觸發(fā)一個saga?
####使用actions來觸發(fā)saga工作流
___
Sagas變得越來越有用,因為我們有能力使用redux actions來觸發(fā)工作流.當(dāng)我們這樣做,saga會在我們的應(yīng)用中表現(xiàn)出更大的能力.首先我們創(chuàng)建一個新的saga.`watchForLoadImages`.
import {fetchImages} from './flickr';
-import {put} from 'redux-saga/effects';
+import {put, take} from 'redux-saga/effects';
export function* loadImages() {
const images = yield fetchImages();
yield put({type: 'IMAGES_LOADED', images})
}
+export function* watchForLoadImages() {
- while(true) {
- yield take('LOAD_IMAGES');
- yield loadImages();
- }
+}
新的saga使用的是while來保持一直激活和等待調(diào)用狀態(tài).在循環(huán)的內(nèi)部,我們生成(yield)一個redux-sage調(diào)用方法:take
.Take方法監(jiān)聽任何類型的actions,他也會使saga接受下一個yield.在上面的例子中我們調(diào)用了一個方法loadImages
,初始化圖片加載.
import "babel-polyfill";
import React from 'react';
import ReactDOM from 'react-dom';
import Gallery from './Gallery';
import { createStore, applyMiddleware } from 'redux'
import {Provider} from 'react-redux';
import createSagaMiddleware from 'redux-saga'
import reducer from './reducer'
-import {loadImages} from './sagas';
+import {loadImages} from './watchForLoadImages';
const store = createStore(
reducer,
- applyMiddleware(createSagaMiddleware(loadImages))
+ applyMiddleware(createSagaMiddleware(watchForLoadImages))
);
ReactDOM.render(
<Provider store={store}>
<Gallery />
</Provider>,
document.getElementById('root')
);
更新了main.js
以后,應(yīng)用不再加載圖片,我們需要在action creators中添加loadImages
的action
.
export const IMAGE_SELECTED = 'IMAGE_SELECTED';
+const LOAD_IMAGES = 'LOAD_IMAGES';
export function selectImage(image) {
return {
type: IMAGE_SELECTED,
image
}
}
+export function loadImages() {
+ return {
+ type: LOAD_IMAGES
+ }
+}
因為我們已經(jīng)綁定了action creators(Action創(chuàng)建器),我們只需要在Gallery
組件中調(diào)用這個action就可以了.
block(阻塞)和no-blocking(非阻塞)效應(yīng)
現(xiàn)在我們的引用工作的足夠好了,但是可能還有更多的問題需要考慮.watchForLoadImages
saga包含 block effects.那么這到底是什么意思呢?這意味著在工作流中我們只能執(zhí)行一次LOAD_IMAGES
!在諸如我們現(xiàn)在構(gòu)建的小型應(yīng)用一樣,這一點不太明顯,實際上我們也僅僅加載了一次圖片集.
實際上,普遍的做法是使用fork
effect 代替 yield
來加載圖片
.
export function* watchForLoadImages() {
while(true) {
yield take('LOAD_IMAGES');
- yield loadImages();
+ yield fork(loadImages); //be sure to import it!
}
}
使用fork
助手(helper)函數(shù),watchForLoadImages
就變成了非阻塞saga了,再也不用考慮他是不是以前掉用過.redux-sagas 提供兩個helpers,takeEvery
和takeLastest
(takeEvery監(jiān)聽多次action,不考慮是不是同一種action type,takeLatest只處理同一種action type的最后一次調(diào)用).
選擇默認的圖片
Sagas按照隊列來執(zhí)行acitons,所以添加更多的saga也很容易.
import {fetchImages} from './flickr';
import {put, take, fork} from 'redux-saga/effects';
export function* loadImages() {
const images = yield fetchImages();
yield put({type: 'IMAGES_LOADED', images})
+ yield put({type: 'IMAGE_SELECTED', image: images[0]})
}
export function* watchForLoadImages() {
while(true) {
yield take('LOAD_IMAGES');
yield fork(loadImages);
}
}
在 loadImages
工作流上,我們可以yield put函數(shù)調(diào)用,action type是IMAGE_SELECTED
.發(fā)送我們選擇的圖片(在這個例子中,發(fā)送的僅僅是圖片的url的字符串).
錯誤處理
如果在saga循環(huán)內(nèi)部出現(xiàn)錯誤,我們要考慮提醒應(yīng)用做出合理的回應(yīng).所有流程包裝到try/catch語句塊里就可以實現(xiàn),捕獲錯誤以后put
一個提示信息作為IMAGE_LOAD_FAILURE
action的內(nèi)容.
import {fetchImages} from './flickr';
import {put, take, fork} from 'redux-saga/effects';
export function* loadImages() {
+ try {
const images = yield fetchImages();
yield put({type: 'IMAGES_LOADED', images})
yield put({type: 'IMAGE_SELECTED', image: images[0]})
+ } catch(error) {
+ yield put({type: 'IMAGE_LOAD_FAILURE', error})
+ }
}
export function* watchForLoadImages() {
while(true) {
yield take('LOAD_IMAGES');
yield fork(loadImages);
}
}
Sagas的測試
在應(yīng)用中使用Redux,測試變得相當(dāng)?shù)氖娣? 看看我們的鵝蛋頭系列課程,可以了解到很多React的測試技術(shù).
使用Redux-saga在棒的一個方面就是異步代碼測試很容易.測試javascript異步代碼真是一件苦差事.有了saga,我們不需要跳出引用的核心代碼.Saga把javascript的痛點都抹掉了.是不是意味著我們要寫更多的測試?對的.
我們會使用tape
組件,首先做一些配置工作.
import test from 'tape';
import {put, take} from 'redux-saga/effects'
import {watchForLoadImages, loadImages} from './sagas';
import {fetchImages} from './flickr';
test('watchForLoadImages', assert => {
const generator = watchForLoadImages();
assert.end();
});
添加所有需要的組件,現(xiàn)在我們添加一個測試.這個測試接收一個名稱和一個函數(shù)作為形參.在測試的函數(shù)體內(nèi)部代碼塊,我們創(chuàng)建了一個saga生成器代碼實例.在這個實例里面我們尅是測試saga的每一個動作.
import test from 'tape';
import {put, take} from 'redux-saga/effects'
import {watchForLoadImages, loadImages} from './sagas';
import {fetchImages} from './flickr';
test('watchForLoadImages', assert => {
const generator = watchForLoadImages();
+ assert.deepEqual(
+ generator.next().value,
+ false,
+ 'watchForLoadImages should be waiting for LOAD_IMAGES action'
+ );
assert.end();
});
assert.deepEqual
方法接收兩個值,檢查一下他們是不是深度相同(js對象的概念).第一行代碼是generator.next().value
的調(diào)用,這個調(diào)用使生成器從暫停中恢復(fù),得到值.下一個值單單是一個false
.我想看到他失敗,最后一個參數(shù)描述了測試期待的行為.
在項目文件夾中命令行運行npm test
看看結(jié)果:
import test from 'tape';
import {put, take} from 'redux-saga/effects'
import {watchForLoadImages, loadImages} from './sagas';
import {fetchImages} from './flickr';
test('watchForLoadImages', assert => {
const generator = watchForLoadImages();
+ assert.deepEqual(
+ generator.next().value,
+ false,
+ 'watchForLoadImages should be waiting for LOAD_IMAGES action'
+ );
assert.end();
});
測試結(jié)果和預(yù)期的一樣失敗,結(jié)果有點意思.實際的結(jié)論是{TAKE:'LOAD_IMAGES'}
,這是我們調(diào)用take('LOAD_IMAGES')
受到的結(jié)果.實際上,我們的saga’可以yield一個對象來代替調(diào)用take
.但是take
添加了一些代碼,讓我們少敲些代碼.
import test from 'tape';
import {put, take} from 'redux-saga/effects'
import {watchForLoadImages, loadImages} from './sagas';
import {fetchImages} from './flickr';
test('watchForLoadImages', assert => {
const generator = watchForLoadImages();
assert.deepEqual(
generator.next().value,
- false
+ take('LOAD_IMAGES'),
'watchForLoadImages should be waiting for LOAD_IMAGES action'
);
assert.end();
});
我們簡單的調(diào)用take
函數(shù),就可以得到期待的結(jié)果了.
import test from 'tape';
import {put, take} from 'redux-saga/effects'
import {watchForLoadImages, loadImages} from './sagas';
import {fetchImages} from './flickr';
test('watchForLoadImages', assert => {
const generator = watchForLoadImages();
assert.deepEqual(
generator.next().value,
take('LOAD_IMAGES'),
'watchForLoadImages should be waiting for LOAD_IMAGES action'
);
+ assert.deepEqual(
+ gen.next().value,
+ false,
+ 'watchForLoadImages should call loadImages after LOAD_IMAGES action is received'
+ );
assert.end();
});
下一個測試使我們確信loadImages
saga在流程的下一個階段會被自動調(diào)用.
我們需要一個 false來檢查結(jié)果.
更新一下saga代碼,yield一個loadImages
saga:
export function* watchForLoadImages() {
while(true) {
yield take('LOAD_IMAGES');
+ yield loadImages();
- yield fork(loadImages); //be sure to import it!
}
}
現(xiàn)在運行測試,將會看到下面結(jié)果:
? watchForLoadImages should call loadImages after LOAD_IMAGES action is received
---------------------------------------------------------------------------------
operator: deepEqual
expected: |-
false
actual: |-
{ _invoke: [Function: invoke] }
哼!{ _invoke: [Function: invoke] }
絕對不是我們yield take想要的結(jié)果.
有問題.幸運的是redux-saga可以使用諸如fork
一樣的effects
來解決這個問題.fork
,take
和其他的effect方法返容易滿足測試要求的簡單對象.這些effects返回的對象是一個指導(dǎo)redux-saga進行任務(wù)執(zhí)行的集合.這一點對于測試來說非常的優(yōu)雅,因為我們不用擔(dān)心類似遠程服務(wù)請求的副作用.有了redux-saga,我們把注意點放到請求執(zhí)行的命令上.
下面讓我們更新一下saga,再一次使用fork
.
export function* watchForLoadImages() {
while(true) {
yield take('LOAD_IMAGES');
- yield loadImages();
+ yield fork(loadImages);
}
}
這里使用yield fork(loadImages)
直接代替loadImages
.需要注意的是我們還沒有執(zhí)行loadImages
,而是作為參數(shù)傳遞給fork
.
再次運行npm test
.
? watchForLoadImages should call loadImages after LOAD_IMAGES action is received
---------------------------------------------------------------------------------
operator: deepEqual
expected: |-
false
actual: |-
{ FORK: { args: [], context: null, fn: [Function: loadImages] } }
結(jié)果得到了一個單純對象而不是一個函數(shù)調(diào)用.函數(shù)在瀏覽器端也同時加載了,但是我們現(xiàn)在可以輕松的在saga 工作流里測試這個步驟.
import test from 'tape';
import {put, take} from 'redux-saga/effects'
import {watchForLoadImages, loadImages} from './sagas';
import {fetchImages} from './flickr';
test('watchForLoadImages', assert => {
const generator = watchForLoadImages();
assert.deepEqual(
generator.next().value,
take('LOAD_IMAGES'),
'watchForLoadImages should be waiting for LOAD_IMAGES action'
);
assert.deepEqual(
generator.next().value,
- false,
+ yield fork(loadImages),
'watchForLoadImages should call loadImages after LOAD_IMAGES action is received'
);
assert.end();
});
測試loadImages
saga是一樣的,只需要把yield fetchImages
更新為yield fork(fetchImages)
.
test('loadImages', assert => {
const gen = loadImages();
assert.deepEqual(
gen.next().value,
call(fetchImages),
'loadImages should call the fetchImages api'
);
const images = [0];
assert.deepEqual(
gen.next(images).value,
put({type: 'IMAGES_LOADED', images}),
'loadImages should dispatch an IMAGES_LOADED action with the images'
);
assert.deepEqual(
gen.next(images).value,
put({type: 'IMAGE_SELECTED', image: images[0]}),
'loadImages should dispatch an IMAGE_SELECTED action with the first image'
);
const error = 'error';
assert.deepEqual(
gen.throw(error).value,
put({type: 'IMAGE_LOAD_FAILURE', error}),
'loadImages should dispatch an IMAGE_LOAD_FAILURE if an error is thrown'
);
assert.end();
});
特別注意最后一個assert
.這個斷言測試使用異常捕獲代替生成器函數(shù)的next方法.另一個非常酷的地方是:可以傳值.注意看代碼,我們創(chuàng)建了images
常量,并且傳遞到next函數(shù).saga可以在接下來的任務(wù)序列中使用傳遞的值.
太棒了,這種方法是測試異步編程的程序員夢寐以求的技術(shù).
接下來做什么?
你可以fork一下這個例子的代碼.
如果你想擴充這個應(yīng)用,可以做一下幾個方面的工作.
- 做一個幻燈顯示下一張要顯示的圖片
- 允許使用者搜索Flickr圖片
- 添加其他提供圖片的API
- 允許用戶選擇喜歡的API進行搜索.
我們僅僅和生成器碰了一下面,但是即便如此,希望在聯(lián)合使用redux-saga library,Redux和React的時候給你一些幫助.