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

看到這篇文章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,允許用戶查看圖片詳情。

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

后續(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腳本配置:starttest.現(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">
 ![](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是一個最基礎(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).

  1. [x] state-這個數(shù)據(jù)代表應(yīng)用的狀態(tài)(state).reducer函數(shù)使用這個狀態(tài)來構(gòu)建一個reducer自己可以管理的狀態(tài).如果狀態(tài)沒有發(fā)生改變,reducer會返回輸入的狀態(tài).
  2. [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)一個actioncase分支吻合以后,相應(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':
  •  return {...state, images: action.images};
    
    default:
    return state;
    }
    }
    
    ___
    我們添加了新的分支,并從`defaultState`中刪除了硬編碼的URLs數(shù)據(jù).`IMAGES_LOADED`分支現(xiàn)在返回一個更新的state,包含action的image數(shù)據(jù).
    下一步我們更新saga:
    
    import {fetchImages} from './flickr';
    +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中添加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
+  }
+}

因為我們已經(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,takeEverytakeLastest(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();
});

下一個測試使我們確信loadImagessaga在流程的下一個階段會被自動調(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();
});

測試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.這個斷言測試使用異常捕獲代替生成器函數(shù)的next方法.另一個非常酷的地方是:可以傳值.注意看代碼,我們創(chuàng)建了images常量,并且傳遞到next函數(shù).saga可以在接下來的任務(wù)序列中使用傳遞的值.
太棒了,這種方法是測試異步編程的程序員夢寐以求的技術(shù).

接下來做什么?

你可以fork一下這個例子的代碼.

如果你想擴充這個應(yīng)用,可以做一下幾個方面的工作.

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

我們僅僅和生成器碰了一下面,但是即便如此,希望在聯(lián)合使用redux-saga library,Redux和React的時候給你一些幫助.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內(nèi)容