看到這篇文章build an image gallery using redux saga,以爲寫的不錯,長短也適中. 文後有註釋版的github代碼庫,請使用comment分枝. Flickr API可能須要有fQ的基本能力.可使用google的翻譯做爲參考,這篇文章google翻譯版的中文水平讓我吃了一驚. 翻譯已經完成.javascript
Joel Hooks ,2016年3月php
圖像長廊是一個簡單的程序,從Flicker API 加載圖片URLs,容許用戶查看圖片詳情。css
後續咱們會使用React,Redux和redux-saga.React做爲核心框架,優點是虛擬dom(virtual-dom)的實現。Redux在程序內負責state的管理。最後,咱們會使用redux-saga來執行javascript的異步操做步驟。html
咱們會使用ES6(箭頭函數,模塊,和模板字符串),因此咱們首先須要作一些項目的配置工做。java
#####項目配置和自動化node
若是要開始一個React項目,須有有一系列的配置選項。對於一個簡單的項目,我想把配置選項儘量縮減。考慮到瀏覽器的版本問題,使用Babel把ES6編譯爲ES5。react
首先使用npm init 建立一個package.json
文件git
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
安裝程序須要的依賴項。github
.babelrc
{
"presets": ["es2015", "react", "stage-2"]
}
複製代碼
這個文件告訴babel,咱們將會使用ES2015(ES6),React以及ES2106的stage-2的一些特徵。
package.json
有兩個標準的script腳本配置:start
和test
.如今咱們想經過start腳本加載程序,start會使用src
目錄的一些文件,因此西藥先建立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">
![](http://cloud.egghead.io/2G021h3t2K10/download/egghead-logo-head-only.svg)
<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
是一個最基礎的React程序,他在index.html
的#root
元素中渲染一個h1
元素。建立這些文件之後,在項目文件夾中命令行運行npm start
。在瀏覽器打開http://10.11.12.1:9966
.就能夠看到index.html
中渲染的頁面
如今咱們來構建基礎的Gallery
React 組件
首先咱們須要儘量快的得到一個能夠顯示的圖片素材.在項目文件夾中建立一個文件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>
)
}
}
複製代碼
咱們直接在組件中硬編碼了一個提供數據的數組,讓項目儘快的工做起來.Gallery組件
繼承Component組件
,在構造函數中建立一些組件的出事狀態.最後咱們利用一些樣式標記渲染一下文件。image-scroller
元素遍歷(map
方法)圖片數組,生成摘要小圖片。
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')
);
複製代碼
到如今,咱們使用硬編碼的圖片URLs(經過fickrImages)數組,第一張圖片做爲selectedImage
.這些屬性在Gallery
組件的構造函數缺省配置中,經過初始狀態(initial)來設定.
接下來在組件中添加一個和組件進行交互操做的方法,方法具體內容是操作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
屬性調用這個方法.image
做爲第二個參數傳遞,元素自身做爲第一個參數傳遞.bind方法傳遞javascript函數調用上下文對象是很是便捷。
看起來不錯!如今咱們有了一些交互操做的方法,有點「APP」的意思了。截止目前,咱們已經讓app運行起來了,接下來要考慮怎麼加載遠程數據。最容易加載遠程數據的地方是一個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
生命週期方法觸發Flickr圖片數據的獲取。
在React
組件運行的不一樣時間點,組件會調用不一樣的生命週期函數。在這段代碼中,當組件被渲染到DOM
中的時間點,componentDidMount
函數就會被調用。須要注意的是:Gallery
組件只有一次渲染到DOM
的機會,因此這個函數能夠提供一些初始化圖片.考慮到在APP的整個生命週期中,有更多的動態組件的加載和卸載,這可能會形成一些多餘的調用和沒法考慮到的結果。
咱們使用瀏覽器接口(browser API)的fetch
方法執行請求.Fetch返回一個promise對象解析response
對象.調用response.json()
方法,返回另外一個promise對象,這就是咱們實際須要的json
格式的數據.遍歷這個對象之後就能夠獲取圖片的url地址.
坦白講,這個應用目前還很簡單.咱們還須要在這裏花費更多的時間,還有一些基礎的需求須要完成.或許咱們應該在promise處理流程中添加錯誤處理方法,若是圖片數據獲取成功也須要一些處理邏輯.在這個地方,你須要發揮一些想象力考慮一下更多的邏輯.在生產實踐中簡單的需求是不多見的.很快,應用中就會添加更多的需求。認證,滾動櫥窗,加載不一樣圖片庫的能力和圖片的設置等等.僅僅這些還遠遠不夠.
咱們已經使用React
構建了一個加載圖片庫的程序。接下來咱們須要考慮到隨着程序功能的添加,到底須要哪些基礎的模式.首先考慮到的一個問題就是要把應用的狀態(state)控制從Gallery
組件中分離出來.
咱們經過引入Redux
來完成應用的狀態管理工做。
Redux
來管理狀態在你的應用中只要使用了setState
方法都會讓一個組件從無狀態變爲有狀態的組件.糟糕的是這個方法會致使應用中出現一些使人困惑的代碼,這些代碼會在應用中處處蔓延。
Flux
構架來減輕這個問題.Flux
把邏輯(logic)和狀態(state)遷移到Store
中.應用中的動做(Actions
)被Dispatch
的時候,Stores
會作相應的更新.Stores
的更新會觸發View
根據新狀態的渲染.
那麼咱們爲何要捨棄Flux
?他居然仍是「官方」構建的. 好吧!Redux
是基於Flux
構架的,可是他有一些獨特的優點.下面是Dan Abramov(Redux建立者)的一些話:
Redux和Flux沒有什麼不一樣.整體來說他們是相同的構架,可是Redux經過功能組合把Flux使用回調註冊的複雜點給屏蔽掉了. 兩個構架從更本上講沒有什麼不一樣,可是我發現Redux使一些在Flux比較難實現的邏輯更容易實現.
Redux文檔很是棒. 若是你尚未讀過代碼的卡通教程或者Dan的系列文章.趕快去看看吧!
第一件須要作的事事初始化Redux
,讓他在咱們的程序中運行起來.如今不須要作安裝工做,剛開始運行npm install
的時候已經安裝好了依賴項,咱們須要作一些導入和配置工做. reducer函數是Redux的大腦. 每當應用分發(或派遣,dispatch)一個操做(action)的時候,reducer
函數會接受操做(action)而且依據這個動做(action)建立reducer
本身的state
.由於reducers
是純函數,他們能夠組合到一塊兒,建立應用的一個完整state
.讓咱們在src
中建立一個簡單的reducer:
reducer.js
export default function images(state, action) {
console.log(state, action)
return state;
}
複製代碼
一個reducer函數接受兩個參數(arguments).
state
-這個數據表明應用的狀態(state).reducer函數使用這個狀態來構建一個reducer本身能夠管理的狀態.若是狀態沒有發生改變,reducer會返回輸入的狀態.action
-這是觸發reducer的事件.Actions經過store派發(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
庫中導入createStore
組件.creatStore
用來建立Redux的store.大多數狀況下,咱們不會和store直接交互,store在Redux中作幕後管理工做.
也須要導入剛纔建立的reducer函數,以便於他能夠被髮送到store. 咱們將經過createStore(reducer)
操做,利用reducer來配置應用的store.這個示例僅僅只有一個reducer,可是createStore
能夠接收多個reducer做爲參數.稍後咱們會看到這一點.
最後咱們導入高度集成化的組件Provider
,這個組件用來包裝Gallery
,以便於咱們在應用中使用Redux.咱們須要把剛剛建立的store傳遞給Provider
.你也能夠不使用Provider
,實際上Redux能夠不須要React.可是咱們將會使用Provider
,由於他很是便於使用.
這張圖可能有點古怪,可是展現了Redux的一個有意思的地方.全部的reducers接收在應用中的所有actions(動做或操做).在這個例子中咱們能夠看到Redux本身派發的一個action
.
藉助Redux,咱們將使用」connected」和「un-connected」組件.一個connected
組件被連線到store.connected
組件使控制動做事件(controls action event)和store協做起來.一般,一個connected
組件有子組件,子組件具備單純的接收輸入和渲染功能,當數據更新時執行調用.這個子組件就是unconnected組件.
提示:當Rect和Redux配合是工做的很是好,可是Redux不是非要和React在一塊兒才能工做.沒有React,Redux其實能夠和其餘框架配合使用.
在應用中須要關聯React組件
和Redux Store
的時候,react-redux
提供了便捷的包裝器.咱們把react-redux添加進Gallery
中 ,從而使Gallery
成爲首要的關聯組件.
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
導入connect
函數,能夠在導出組件的時候把他變爲連接組件(connected component).請注意,connect()(Gallery)
代碼把Gallery
組件放在第二個形參中,這是由於connect()
返回一個函數,這個函數接受一個React組件做爲參數(argument).調用connect()
函數時須要配置項.後面咱們將會傳遞配置咱們應用的actions和state參數. 咱們也把connect
做爲默認配置處處模塊.這一點很是重要!如今當咱們import Gallery
的時候,就不是一個單純的React組件了,而是一個和Redux關聯的組件了.
若是你觀察咱們添加進構造器的console.log
的輸出,就能夠看到Gallery
組件的屬性如今包括了一個dispatch
函數.這個地方是connect
爲咱們的應用修改的,這個改動賦予了組件把本身的動做對象(action objects)派發
到reducers
的能力.
export class Gallery extends Component {
constructor(props) {
super(props);
+ this.props.dispatch({type: 'TEST'});
this.state = {
images: []
}
}
[...]
複製代碼
咱們能夠在組件的構造器中調用派發功能.你能夠在開發者的終端中看到來自reducer的日誌聲明.看到聲明表示咱們已經派發了第一個action!.Actions是一個單一的javascript對象,必需有type
屬性.Actions能夠擁有任意數量和種類的其餘屬性.可是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代碼塊
過濾有關的消息,Switch
語句使用actions的type屬性,當一個action
和case
分支吻合之後,相應的單個reducer就會執行他的具體工做.
咱們的應用如今關聯到接收的動做.如今咱們須要把Redux
-Store
提供的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;
}
複製代碼
咱們建立一個defaultState
對象,這個對象返回一個空數組做爲images的屬性.咱們把images
函數的參數state
設置爲默認.若是在test分支中輸出日誌,將會看到state不是undefined(空數組不是undefined)!reducer須要返回應用的當前state.這點很重要!如今咱們沒有作任何改變,因此僅僅返回state.注意咱們在case
中添加了default分支,reducer必需要返回一個state.
在Gallery
組件中,咱們也能夠把state作必定的映射(map)之後再鏈接到應用.
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
組件的底部代碼,你會注意到,咱們建立了一個mapStateToProps
函數,接收一個state
做爲參數,返回一個對象,把state.images
映射爲images
屬性.mapStateToProps
作爲參數傳遞給connect
. 正如名字暗示的同樣,mapStateToProps
函數接收當前應用的state,而後把state轉變爲組件的屬性(propertys).若是在構造器中輸出props,將會看到images數組是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數組,你將能夠看到一些圖片從新出如今gallery中!如今當用戶點擊縮略圖的時候,咱們能夠反饋選擇動做,返回對應的大圖.
怎麼操做才能根據新選擇的圖片更新state? 須要配置reducer監聽IMAGE_SELECTED
動做,藉助action攜帶的信息(payload,有的文章翻譯爲載荷,載荷怎麼理解?手機載荷就是聲音,短信和流量數據。若是是卡車就是拉的貨物,若是是客車就乘載的乘客,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;
}
}
複製代碼
如今reducer已經準備接收IMAGE_SELECTED
action了.在IMAGE_SELECTED
分支選項內,咱們在展開(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
函數體中調用他,如今咱們從便利角度考慮把他們放在一塊兒,可是二者功能是同樣的.一旦咱們點擊了縮略圖,他將會經過reducer更新大圖. 使用dispatch能夠很方便的建立通用actions,可是很快咱們會須要重用命名好的actions.爲了這樣作,可使用」action creators」.
Action creators函數返回配置好的action對象.咱們在action.js
中添加第一個action creator.
action.js
export const IMAGE_SELECTED = 'IMAGE_SELECTED';
export function selectImage(image) {
return {
type: IMAGE_SELECTED,
image
}
}
複製代碼
這個方法通過export之後,能夠直接在任何須要建立selectImage
action地方導入!selectImage
是純函數,只能返回數據.他接收一個image做爲參數,把image添加到action對象中,並返回.
注意:咱們正在返回一個單純的javascript object,可是
image
的屬性可能很古怪,若是你之前沒有碰到這樣的樣式.從ES6的角度出發,若是你給一個對象傳遞一個相似這樣的屬性,隱含的意思是把image:'任何image包含的值'
添加到最終返回的對象.超級好用!
import * as GalleryActions from './actions.js';
[...]
onClick={() => dispatch(GalleryActions.selectImage(image))}
複製代碼
this isn’t much than just using dispatch
though.
幸運的是,這個模式很廣泛,Redux在bindActionCreators
函數裏提供了一個更好的辦法來完成這個功能.
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)
複製代碼
咱們已經添加了mapActionCreatorsToProps
函數,他接收dispatch
函數做爲參數.返回bindActionCreators
的調用結果,GalleryActions
做爲bindActionCreators
的參數.如今若是你輸出屬性日誌,就看不到dispatch
做爲參數,selectImage
直接可使用了.(這裏至關於對dispatch和action進行了包裝).
如今回顧一下,咱們作了幾件事:
selectImage(image)
,分發動做,應用狀態將會更新.那麼,咱們怎麼才能使用這些模式從遠程資源加載數據呢?
這個過程將會很是有趣!
你可能在參加函數式編程的時候據說過」反作用」(side effects)這個名詞,side effects是發生在應用的範圍以外的東西.在咱們溫馨的肥皂泡裏,side effect根本不是問題,可是當咱們要到達一個遠程資源,肥皂泡就被穿透了.有些事情咱們就控制不了了,咱們必須接受這個事實.(根據這段話,side effect 翻譯爲意想不到的事情,出乎意料的不受控制的事情更好)
在Redux裏,reducer沒有Side effects.這意味着reducers不處理咱們應用中的異步活動.咱們不能使用reducers加載遠程數據,由於reducers是純函數,沒有side effects.
Redux很棒,若是你的應用裏沒有任何異步活動,你能夠停下來,不用再往下看了. 若是你建立的應用比較大,可能你會從服務端加載數據,這時,固然要使用異步方式.
注意: Redux其中一個最酷的地方是他很是小巧.他試圖解決有限範圍內的問題.大多數的應用須要解決不少問題!萬幸,Reduc提供中間件概念,中間件存在於action->reducer->store的三角關係中,經過中間件的方式,能夠導入諸如遠程數據異步加載相似的功能.
其中一個方法是使用thunks
對象,在Redux中有 redux-thunk 中間件.Thunks很是厲害,可是可能會致使actions的序列很複雜,測試起來也是很大的挑戰.
考慮到咱們的 圖片瀏覽程序.當應用加載是,須要作:
這些事件都要在用戶點擊應用裏的任何元素以前完成! 咱們該怎麼作呢? redux-saga就是爲此而誕生,爲咱們的應用提供絕佳的服務.
redux-sage
redux-sage能夠在Redux應用中操做異步actions.他提供中間件和趁手的方法使構建複雜的異步操做流程垂手可得.
一個saga是一個Generator(生成器),Generator函數是ES2015新添加的特性.多是你第一次遇到Generator函數,這樣你會以爲有點古怪,能夠參考(ruanyifeng文章).不要苦惱,若是你對此仍然很抓耳撓腮.使用redux-sage你不須要javascript異步編程的博士學位.
由於使用了generators的緣故,咱們能建立一個順序執行的命令序列,用來描述複雜的異步操做流程(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實例,而後配置他鏈接到咱們的應用.在src
建立一個文件 saga.js
export function* sayHello() {
console.log('hello');
}
複製代碼
咱們的saga是一個簡單的generator函數.函數後面的*
做爲標誌,他也被叫作」super star」.
如今在main.js
文件中導入新函數,而且執行他.
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」永遠不會出現. 這是由於sayHello
是一個generator!Generator 不會當即執行.若是你把代碼該爲sayHello().next();
你的「hello」就出現了.不用擔憂,咱們不會老是調用next
.正如Redux,redux-saga用來消除應用開發中的痛苦.
配置 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導入了applyMiddleware
函數.從redux-saga導入createSagaMiddleware
函數.當咱們建立store的時候,咱們須要經過中間件提供Redux須要的功能.在這個實例中,咱們會調用applyMiddleware
函數,這個函數返回createSagaMiddleware(sayHello)
的結果.在幕後,redux-saga加載sayHello
函數,儀式性的調用next
函數.
應該能夠在終端中看到提示消息了. 如今讓咱們構建加載圖片的saga
咱們將刪除出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')
);
複製代碼
如今saga已經加載,在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對象.咱們將調用fetchImages
,可是如今咱們要使用yield
關鍵字.經過黑暗藝術和巫術,generators理解Promise對象,正如終端輸出的日誌顯示,咱們已經收穫了一個圖片URLs的數組.看看loadImages
的代碼,他看起來像是典型的同步操做代碼.yield
關鍵字是祕製調味醬,讓咱們的代碼用同步格式執行異步操做活動.
首先來定義一下須要使用的api.他沒有什麼特殊的地方,實際上他和早先加載Flickr images的代碼是相同的.咱們建立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`
);
})
})
};
複製代碼
嚴格意義上來講,不須要這麼作,可是這會帶來必定的好處.咱們處在應用的邊緣(boundaries of our application,意思是說在這裏的代碼多是不少和遠程服務器交互的代碼,可能邏輯會很複雜),事情都有點亂.經過封裝和遠程API交互的邏輯,咱們的代碼將會很整潔,很容易更新.若是須要抹掉圖片服務也會出奇的簡單.
咱們的saga.js
看起來是這個樣子:
import {fetchImages} from './flickr';
export function* loadImages() {
const images = yield fetchImages();
console.log(images)
}
複製代碼
咱們仍然須要在saga外獲取數據,而且進入應用的state(使用異步獲取的遠程數據更新state).爲了處理這個問題,咱們將使用」effects」.
咱們能夠經過dispatch
或者store做爲參數來調用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':
+ return {...state, images: action.images};
default:
return state;
}
}
複製代碼
咱們添加了新的分支,並從defaultState
中刪除了硬編碼的URLs數據.IMAGES_LOADED
分支如今返回一個更新的state,包含action的image數據. 下一步咱們更新saga:
import {fetchImages} from './flickr';
+import {put} from 'redux-saga/effects';
export function* loadImages() {
const images = yield fetchImages();
+ yield put({type: 'IMAGES_LOADED', images})
}
複製代碼
導入put
之後,咱們在loadImages
添加另一行.他yield
put
函數調用的返回結果.在幕後,redux-saga 分發這些動做,reducer接收到了消息! 怎樣才能使用特定類型的action來觸發一個saga?
Sagas變得愈來愈有用,由於咱們有能力使用redux actions來觸發工做流.當咱們這樣作,saga會在咱們的應用中表現出更大的能力.首先咱們建立一個新的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來保持一直激活和等待調用狀態.在循環的內部,咱們生成(yield)一個redux-sage調用方法:take
.Take方法監放任何類型的actions,他也會使saga接受下一個yield.在上面的例子中咱們調用了一個方法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
之後,應用再也不加載圖片,咱們須要在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
+ }
+}
複製代碼
由於咱們已經綁定了action creators(Action建立器),咱們只須要在Gallery
組件中調用這個action就能夠了.
如今咱們的引用工做的足夠好了,可是可能還有更多的問題須要考慮.watchForLoadImages
saga包含 block effects.那麼這究竟是什麼意思呢?這意味着在工做流中咱們只能執行一次LOAD_IMAGES
!在諸如咱們如今構建的小型應用同樣,這一點不太明顯,實際上咱們也僅僅加載了一次圖片集. 實際上,廣泛的作法是使用fork
effect 代替 yield
來加載圖片 .
export function* watchForLoadImages() {
while(true) {
yield take('LOAD_IMAGES');
- yield loadImages();
+ yield fork(loadImages); //be sure to import it!
}
}
複製代碼
使用fork
助手(helper)函數,watchForLoadImages
就變成了非阻塞saga了,不再用考慮他是否是之前掉用過.redux-sagas 提供兩個helpers,takeEvery
和takeLastest
(takeEvery監聽屢次action,不考慮是否是同一種action type,takeLatest只處理同一種action type的最後一次調用). ####選擇默認的圖片 Sagas按照隊列來執行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函數調用,action type是IMAGE_SELECTED
.發送咱們選擇的圖片(在這個例子中,發送的僅僅是圖片的url的字符串).
若是在saga循環內部出現錯誤,咱們要考慮提醒應用作出合理的迴應.全部流程包裝到try/catch語句塊裏就能夠實現,捕獲錯誤之後put
一個提示信息做爲IMAGE_LOAD_FAILURE
action的內容.
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);
}
}
複製代碼
在應用中使用Redux,測試變得至關的舒服. 看看咱們的鵝蛋頭系列課程,能夠了解到不少React的測試技術. 使用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();
});
複製代碼
添加全部須要的組件,如今咱們添加一個測試.這個測試接收一個名稱和一個函數做爲形參.在測試的函數體內部代碼塊,咱們建立了一個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
的調用,這個調用使生成器從暫停中恢復,獲得值.下一個值單單是一個false
.我想看到他失敗,最後一個參數描述了測試期待的行爲. 在項目文件夾中命令行運行npm test
看看結果:
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();
});
複製代碼
測試結果和預期的同樣失敗,結果有點意思.實際的結論是{TAKE:'LOAD_IMAGES'}
,這是咱們調用take('LOAD_IMAGES')
受到的結果.實際上,咱們的saga’能夠yield一個對象來代替調用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();
});
複製代碼
咱們簡單的調用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,
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在流程的下一個階段會被自動調用. 咱們須要一個 false來檢查結果. 更新一下saga代碼,yield一個loadImages
saga:
export function* watchForLoadImages() {
while(true) {
yield take('LOAD_IMAGES');
+ yield loadImages();
- yield fork(loadImages); //be sure to import it!
}
}
複製代碼
如今運行測試,將會看到下面結果:
✖ watchForLoadImages should call loadImages after LOAD_IMAGES action is received
---------------------------------------------------------------------------------
operator: deepEqual
expected: |-
false
actual: |-
{ _invoke: [Function: invoke] }
複製代碼
哼!{ _invoke: [Function: invoke] }
絕對不是咱們yield take想要的結果. 有問題.幸運的是redux-saga可使用諸如fork
同樣的effects
來解決這個問題.fork
,take
和其餘的effect方法返容易知足測試要求的簡單對象.這些effects返回的對象是一個指導redux-saga進行任務執行的集合.這一點對於測試來講很是的優雅,由於咱們不用擔憂相似遠程服務請求的反作用.有了redux-saga,咱們把注意點放到請求執行的命令上. 下面讓咱們更新一下saga,再一次使用fork
.
export function* watchForLoadImages() {
while(true) {
yield take('LOAD_IMAGES');
- yield loadImages();
+ yield fork(loadImages);
}
}
複製代碼
這裏使用yield fork(loadImages)
直接代替loadImages
.須要注意的是咱們尚未執行loadImages
,而是做爲參數傳遞給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] } }
複製代碼
結果獲得了一個單純對象而不是一個函數調用.函數在瀏覽器端也同時加載了,可是咱們如今能夠輕鬆的在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
.這個斷言測試使用異常捕獲代替生成器函數的next方法.另外一個很是酷的地方是:能夠傳值.注意看代碼,咱們建立了images
常量,而且傳遞到next函數.saga能夠在接下來的任務序列中使用傳遞的值. 太棒了,這種方法是測試異步編程的程序員求之不得的技術.
你能夠fork一下這個例子的代碼.
若是你想擴充這個應用,能夠作一下幾個方面的工做.
咱們僅僅和生成器碰了一下面,可是即使如此,但願在聯合使用redux-saga library,Redux和React的時候給你一些幫助.