使用React,Redux,redux-sage構建圖片庫(翻譯)

看到這篇文章build an image gallery using redux saga,以爲寫的不錯,長短也適中. 文後有註釋版的github代碼庫,請使用comment分枝. Flickr API可能須要有fQ的基本能力.可使用google的翻譯做爲參考,這篇文章google翻譯版的中文水平讓我吃了一驚. 翻譯已經完成.javascript

使用React,Redux和reudx-saga構建一個圖像瀏覽程序(翻譯)

Joel Hooks ,2016年3月php

構建一個圖片長廊

圖像長廊是一個簡單的程序,從Flicker API 加載圖片URLs,容許用戶查看圖片詳情。css

Screen Shot 2016-03-20 at 3.42.17 PM-2.png

後續咱們會使用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腳本配置:starttest.如今咱們想經過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中顯示一些圖片

首先咱們須要儘量快的得到一個能夠顯示的圖片素材.在項目文件夾中建立一個文件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

第一件須要作的事事初始化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).

  1. [x] state-這個數據表明應用的狀態(state).reducer函數使用這個狀態來構建一個reducer本身能夠管理的狀態.若是狀態沒有發生改變,reducer會返回輸入的狀態.
  2. [x] 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.

鏈接Gallery組件

藉助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屬性,當一個actioncase分支吻合之後,相應的單個reducer就會執行他的具體工做.

咱們的應用如今關聯到接收的動做.如今咱們須要把Redux-Store提供的state關聯到應用中.

默認的應用狀態(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

怎麼操做才能根據新選擇的圖片更新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組件中,咱們將會在組件的屬性中定義dispatchonClick函數體中調用他,如今咱們從便利角度考慮把他們放在一塊兒,可是二者功能是同樣的.一旦咱們點擊了縮略圖,他將會經過reducer更新大圖. 使用dispatch能夠很方便的建立通用actions,可是很快咱們會須要重用命名好的actions.爲了這樣作,可使用」action creators」.

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進行了包裝).

如今回顧一下,咱們作了幾件事:

  • 建立了一個reducer包含應用的默認初始狀態(initial state),而且監聽actions的執行.
  • 建立了一個store,把reducer具體化,提供一個分發器(dispatcher)能夠分發action.
  • 把咱們的Gallery組件關聯到store的state.
  • 把store的state映射爲屬性(property),傳遞給Gallery.
  • 映射一個動做建立器,Gallery能夠簡單的調用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

咱們將開始一個簡單的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

經過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請求.

首先來定義一下須要使用的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」.

從saga來更新應用

咱們能夠經過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?

使用actions來觸發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中添加loadImagesaction.

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就能夠了.

block(阻塞)和no-blocking(非阻塞)效應

如今咱們的引用工做的足夠好了,可是可能還有更多的問題須要考慮.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,takeEverytakeLastest(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);
  }
}
複製代碼

Sagas的測試

在應用中使用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();
});
複製代碼

下一個測試使咱們確信loadImagessaga在流程的下一個階段會被自動調用. 咱們須要一個 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();
});
複製代碼

測試loadImagessaga是同樣的,只須要把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一下這個例子的代碼.

若是你想擴充這個應用,能夠作一下幾個方面的工做.

  • 作一個幻燈顯示下一張要顯示的圖片
  • 容許使用者搜索Flickr圖片
  • 添加其餘提供圖片的API
  • 容許用戶選擇喜歡的API進行搜索.

咱們僅僅和生成器碰了一下面,可是即使如此,但願在聯合使用redux-saga library,Redux和React的時候給你一些幫助.

相關文章
相關標籤/搜索