看到这篇文章build an image gallery using redux saga,觉得写的不错,长短也适中. 文后有注释版的github代码库,请使用comment分枝. Flickr API可能需要有fQ的基本能力.可以使用google的翻译作为参考,这篇文章google翻译版的中文水平让我吃了一惊.
翻译已经完成.
使用React,Redux和reudx-saga构建一个图像浏览程序(翻译)
Joel Hooks ,2016年3月
构建一个图片长廊
图像长廊是一个简单的程序,从Flicker API 加载图片URLs,允许用户查看图片详情。
后续我们会使用React,Redux和redux-saga.React作为核心框架,优势是虚拟dom(virtual-dom)的实现。Redux在程序内负责state的管理。最后,我们会使用redux-saga来执行javascript的异步操作步骤。
我们会使用ES6(箭头函数,模块,和模板字符串),所以我们首先需要做一些项目的配置工作。
#####项目配置和自动化
如果要开始一个React项目,须有有一系列的配置选项。对于一个简单的项目,我想把配置选项尽可能缩减。考虑到浏览器的版本问题,使用Babel把ES6编译为ES5。
首先使用npm init 创建一个package.json
文件
package.json
1 | { |
有了package.json
, 可以在项目文件夹命令行运行 npm install
安装程序需要的依赖项。
.babelrc
1 | { |
这个文件告诉babel,我们将会使用ES2015(ES6),React以及ES2106的stage-2的一些特征。
package.json
有两个标准的script脚本配置:start
和test
.现在我们想通过start脚本加载程序,start会使用src
目录的一些文件,所以西药先创建src
文件夹.在src
文件夹添加下面的一些文:
index.html
1 |
|
main.js
1 | import "babel-polyfill" |
style.css
1 | body { |
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
1 | import React, {Component} from 'react' |
我们直接在组件中硬编码了一个提供数据的数组,让项目尽快的工作起来.Gallery组件
继承Component组件
,在构造函数中创建一些组件的出事状态.最后我们利用一些样式标记渲染一下文件。image-scroller
元素遍历(map
方法)图片数组,生成摘要小图片。
1 | import "babel-polyfill" |
到现在,我们使用硬编码的图片URLs(通过fickrImages)数组,第一张图片作为selectedImage
.这些属性在Gallery
组件的构造函数缺省配置中,通过初始状态(initial)来设定.
接下来在组件中添加一个和组件进行交互操作的方法,方法具体内容是操做setSate
.
Gallery.js
1 | export default class Gallery extends Component { |
在Gallery组件
添加handleThumbClick
方法,任何元素都可用通过onClick
属性调用这个方法.image
作为第二个参数传递,元素自身作为第一个参数传递.bind方法传递javascript函数调用上下文对象是非常便捷。
看起来不错!现在我们有了一些交互操作的方法,有点“APP”的意思了。截止目前,我们已经让app运行起来了,接下来要考虑怎么加载远程数据。最容易加载远程数据的地方是一个React组件
生命周期方法,我们使用componentDidMount
方法,通过他从Flikr API
请求并加载一些图片.
Gallery.js
1 | export default class Gallery extends Component { |
我们在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
1 | export default function images(state, action) { |
一个reducer函数接受两个参数(arguments).
- [x]
state
-这个数据代表应用的状态(state).reducer函数使用这个状态来构建一个reducer自己可以管理的状态.如果状态没有发生改变,reducer会返回输入的状态. - [x]
action
-这是触发reducer的事件.Actions通过store派发(dispatch),由reducer处理.action需要一个type
属性来告诉reducer怎么处理state.
目前,images
reuducer在终端中打印出日志记录,表明工作流程是正常的,可以做接下来的工作了.为了使用reducer,需要在main.js
中做一些配置工作:
main.js
1 | import "babel-polyfill"; |
我们从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
1 | import React, {Component} from 'react' |
从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
的能力.
1 | export class Gallery extends Component { |
我们可以在组件的构造器中调用派发功能.你可以在开发者的终端中看到来自reducer的日志声明.看到声明表示我们已经派发了第一个action!.Actions是一个单一的javascript对象,必需有type
属性.Actions可以拥有任意数量和种类的其他属性.但是type
可以让reducers理解这些动作到底是做什么用的(意译,意思是只有拥有type属性,reducers才知道对state做什么样的修改).
1 | export default function images(state, action) { |
总的reducers使用switch代码块
过滤有关的消息,Switch
语句使用actions的type属性,当一个action
和case
分支吻合以后,相应的单个reducer就会执行他的具体工作.
我们的应用现在关联到接收的动作.现在我们需要把Redux
-Store
提供的state
关联到应用中.
默认的应用状态(state)
reducer.js
1 | const defaultState = { |
我们创建一个defaultState
对象,这个对象返回一个空数组作为images的属性.我们把images
函数的参数state
设置为默认.如果在test分支中输出日志,将会看到state不是undefined(空数组不是undefined)!reducer需要返回应用的当前state.这点很重要!现在我们没有做任何改变,所以仅仅返回state.注意我们在case
中添加了default分支,reducer必须要返回一个state.
在Gallery
组件中,我们也可以把state做一定的映射(map)以后再连接到应用.
1 | import React, {Component} from 'react' |
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;
}
}
1 |
|
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;
}
}1
2
现在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> <img src={selectedImage} /> </div>
{images.map((image, index) => (
- dispatch({type:’IMAGE_SELECTED’, image})}>
))}
)
}
}
function mapStateToProps(state) {
return {
images: state.images,
selectedImage: state.selectedImage
}
}
export default connect(mapStateToProps)(Gallery)
1 |
|
export const IMAGE_SELECTED = ‘IMAGE_SELECTED’;
export function selectImage(image) {
return {
type: IMAGE_SELECTED,
image
}
}
1 |
|
import * as GalleryActions from ‘./actions.js’;
[…]
onClick={() => dispatch(GalleryActions.selectImage(image))}
1 |
|
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> <img src={selectedImage} /> </div>
{images.map((image, index) => (
- dispatch({type:’IMAGE_SELECTED’, image})}>
- selectImage(image)}>
))}
)
}
}
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)
1 |
|
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);
}
}
1 |
|
export function* sayHello() {
console.log(‘hello’);
}
1 | ___ |
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(
document.getElementById(‘root’)
);
1 |
|
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(
document.getElementById(‘root’)
);
1 |
|
-export function* sayHello() {
- console.log(‘hello’);
-}
+export function* loadImages() {
- console.log(‘load some images please’)
+}1
2
不要忘了更新`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(
document.getElementById(‘root’)
);
1 |
|
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)
}1
2
3
4
5
6
7
8
`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
);
})
})
};1
2
3
4
5
6
严格意义上来说,不需要这么做,但是这会带来一定的好处.我们处在应用的边缘(boundaries of our application,意思是说在这里的代码可能是很多和远程服务器交互的代码,可能逻辑会很复杂),事情都有点乱.通过封装和远程API交互的逻辑,我们的代码将会很整洁,很容易更新.如果需要抹掉图片服务也会出奇的简单.
我们的`saga.js`看起来是这个样子:
import {fetchImages} from ‘./flickr’;
export function* loadImages() {
const images = yield fetchImages();
console.log(images)
}
1 |
|
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;
}
}1
2
3
4
我们添加了新的分支,并从`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})
}1
2
3
4
5
6
7
导入`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();
- }
+}1
2
新的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(
document.getElementById(‘root’)
);
1 |
|
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
- }
+}1
2
3
4
5
6
7
因为我们已经绑定了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!
}
}1
2
3
4
5
使用`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);
}
}
1 |
|
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);
}
}
1 | #### Sagas的测试 |
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();
});
1 |
|
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();
});1
2
3
`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();
});1
2
测试结果和预期的一样失败,结果有点意思.实际的结论是`{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();
});1
2
我们简单的调用`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();
});1
2
3
4
下一个测试使我们确信`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!
}
}1
2
现在运行测试,将会看到下面结果:
✖ watchForLoadImages should call loadImages after LOAD_IMAGES action is received
operator: deepEqual
expected: |-
false
actual: |-
{ _invoke: [Function: invoke] }
1 |
|
export function* watchForLoadImages() {
while(true) {
yield take(‘LOAD_IMAGES’);
- yield loadImages();
yield fork(loadImages);
}
}1
2
3
4
这里使用`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] } }
1 |
|
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();
});1
2
测试`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一下这个例子的代码.
如果你想扩充这个应用,可以做一下几个方面的工作.
- 做一个幻灯显示下一张要显示的图片
- 允许使用者搜索Flickr图片
- 添加其他提供图片的API
- 允许用户选择喜欢的API进行搜索.
我们仅仅和生成器碰了一下面,但是即便如此,希望在联合使用redux-saga library,Redux和React的时候给你一些帮助.