Redux 入门教程(二):中间件与异步操作
上一篇文章,介绍了 Redux 的基本做法:用户发出 Action,Reducer 函数算出新的 State,View 重新渲染。
但是,一个关键问题没有解决:异步操作怎么办?Action 发出以后,Reducer 立即算出 State,这叫做同步;Action 发出以后,过一段时间再执行 Reducer,这就是异步。
怎么才能 Reducer 在异步操作结束后自动执行呢?这就要用到新的工具:中间件(middleware)。
一、中间件的概念
为了理解中间件,让我们站在框架作者的角度思考问题:如果要添加功能,你会在哪个环节添加?
(1)Reducer:纯函数,只承担计算 State 的功能,不合适承担其他功能,也承担不了,因为理论上,纯函数不能进行读写操作。
(2)View:与 State 一一对应,可以看作 State 的视觉层,也不合适承担其他功能。
(3)Action:存放数据的对象,即消息的载体,只能被别人操作,自己不能进行任何操作。
想来想去,只有发送 Action 的这个步骤,即store.dispatch()
方法,可以添加功能。举例来说,要添加日志功能,把 Action 和 State 打印出来,可以对store.dispatch
进行如下改造。
let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action);
next(action);
console.log('next state', store.getState());
}
上面代码中,对store.dispatch
进行了重定义,在发送 Action 前后添加了打印功能。这就是中间件的雏形。
中间件就是一个函数,对store.dispatch
方法进行了改造,在发出 Action 和执行 Reducer 这两步之间,添加了其他功能。
二、中间件的用法
本教程不涉及如何编写中间件,因为常用的中间件都有现成的,只要引用别人写好的模块即可。比如,上一节的日志中间件,就有现成的redux-logger模块。这里只介绍怎么使用中间件。
import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
const logger = createLogger(); const store = createStore(
reducer,
applyMiddleware(logger)
);
上面代码中,redux-logger
提供一个生成器createLogger
,可以生成日志中间件logger
。然后,将它放在applyMiddleware
方法之中,传入createStore
方法,就完成了store.dispatch()
的功能增强。
这里有两点需要注意:
(1)createStore
方法可以接受整个应用的初始状态作为参数,那样的话,applyMiddleware
就是第三个参数了。
const store = createStore(
reducer,
initial_state,
applyMiddleware(logger)
);
(2)中间件的次序有讲究。
const store = createStore(
reducer,
applyMiddleware(thunk, promise, logger)
);
上面代码中,applyMiddleware
方法的三个参数,就是三个中间件。有的中间件有次序要求,使用前要查一下文档。比如,logger
就一定要放在最后,否则输出结果会不正确。
三、applyMiddlewares()
看到这里,你可能会问,applyMiddlewares
这个方法到底是干什么的?
它是 Redux 的原生方法,作用是将所有中间件组成一个数组,依次执行。下面是它的源码。
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
var store = createStore(reducer, preloadedState, enhancer);
var dispatch = store.dispatch;
var chain = []; var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch); return {...store, dispatch}
}
}
上面代码中,所有中间件被放进了一个数组chain
,然后嵌套执行,最后执行store.dispatch
。可以看到,中间件内部(middlewareAPI
)可以拿到getState
和dispatch
这两个方法。
四、异步操作的基本思路
理解了中间件以后,就可以处理异步操作了。
同步操作只要发出一种 Action 即可,异步操作的差别是它要发出三种 Action。
- 操作发起时的 Action
- 操作成功时的 Action
- 操作失败时的 Action
以向服务器取出数据为例,三种 Action 可以有两种不同的写法。
// 写法一:名称相同,参数不同
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } } // 写法二:名称不同
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
除了 Action 种类不同,异步操作的 State 也要进行改造,反映不同的操作状态。下面是 State 的一个例子。
let state = {
// ...
isFetching: true,
didInvalidate: true,
lastUpdated: 'xxxxxxx'
};
上面代码中,State 的属性isFetching
表示是否在抓取数据。didInvalidate
表示数据是否过时,lastUpdated
表示上一次更新时间。
现在,整个异步操作的思路就很清楚了。
- 操作开始时,送出一个 Action,触发 State 更新为"正在操作"状态,View 重新渲染
- 操作结束后,再送出一个 Action,触发 State 更新为"操作结束"状态,View 再一次重新渲染
五、redux-thunk 中间件
异步操作至少要送出两个 Action:用户触发第一个 Action,这个跟同步操作一样,没有问题;如何才能在操作结束时,系统自动送出第二个 Action 呢?
奥妙就在 Action Creator 之中。
class AsyncApp extends Component {
componentDidMount() {
const { dispatch, selectedPost } = this.props
dispatch(fetchPosts(selectedPost))
} // ...
上面代码是一个异步组件的例子。加载成功后(componentDidMount
方法),它送出了(dispatch
方法)一个 Action,向服务器要求数据 fetchPosts(selectedSubreddit)
。这里的fetchPosts
就是 Action Creator。
下面就是fetchPosts
的代码,关键之处就在里面。
const fetchPosts = postTitle => (dispatch, getState) => {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(postTitle, json)));
};
}; // 使用方法一
store.dispatch(fetchPosts('reactjs'));
// 使用方法二
store.dispatch(fetchPosts('reactjs')).then(() =>
console.log(store.getState())
);
上面代码中,fetchPosts
是一个Action Creator(动作生成器),返回一个函数。这个函数执行后,先发出一个Action(requestPosts(postTitle)
),然后进行异步操作。拿到结果后,先将结果转成 JSON 格式,然后再发出一个 Action( receivePosts(postTitle, json)
)。
上面代码中,有几个地方需要注意。
(1)
fetchPosts
返回了一个函数,而普通的 Action Creator 默认返回一个对象。(2)返回的函数的参数是
dispatch
和getState
这两个 Redux 方法,普通的 Action Creator 的参数是 Action 的内容。(3)在返回的函数之中,先发出一个 Action(
requestPosts(postTitle)
),表示操作开始。(4)异步操作结束之后,再发出一个 Action(
receivePosts(postTitle, json)
),表示操作结束。
这样的处理,就解决了自动发送第二个 Action 的问题。但是,又带来了一个新的问题,Action 是由store.dispatch
方法发送的。而store.dispatch
方法正常情况下,参数只能是对象,不能是函数。
这时,就要使用中间件redux-thunk
。
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers'; // Note: this API requires redux@>=3.1.0
const store = createStore(
reducer,
applyMiddleware(thunk)
);
上面代码使用redux-thunk
中间件,改造store.dispatch
,使得后者可以接受函数作为参数。
因此,异步操作的第一种解决方案就是,写出一个返回函数的 Action Creator,然后使用redux-thunk
中间件改造store.dispatch
。
六、redux-promise 中间件
既然 Action Creator 可以返回函数,当然也可以返回其他值。另一种异步操作的解决方案,就是让 Action Creator 返回一个 Promise 对象。
这就需要使用redux-promise
中间件。
import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import reducer from './reducers'; const store = createStore(
reducer,
applyMiddleware(promiseMiddleware)
);
这个中间件使得store.dispatch
方法可以接受 Promise 对象作为参数。这时,Action Creator 有两种写法。写法一,返回值是一个 Promise 对象。
const fetchPosts =
(dispatch, postTitle) => new Promise(function (resolve, reject) {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => {
type: 'FETCH_POSTS',
payload: response.json()
});
});
写法二,Action 对象的payload
属性是一个 Promise 对象。这需要从redux-actions
模块引入createAction
方法,并且写法也要变成下面这样。
import { createAction } from 'redux-actions'; class AsyncApp extends Component {
componentDidMount() {
const { dispatch, selectedPost } = this.props
// 发出同步 Action
dispatch(requestPosts(selectedPost));
// 发出异步 Action
dispatch(createAction(
'FETCH_POSTS',
fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
));
}
上面代码中,第二个dispatch
方法发出的是异步 Action,只有等到操作结束,这个 Action 才会实际发出。注意,createAction
的第二个参数必须是一个 Promise 对象。
看一下redux-promise
的源码,就会明白它内部是怎么操作的。
export default function promiseMiddleware({ dispatch }) {
return next => action => {
if (!isFSA(action)) {
return isPromise(action)
? action.then(dispatch)
: next(action);
} return isPromise(action.payload)
? action.payload.then(
result => dispatch({ ...action, payload: result }),
error => {
dispatch({ ...action, payload: error, error: true });
return Promise.reject(error);
}
)
: next(action);
};
}
从上面代码可以看出,如果 Action 本身是一个 Promise,它 resolve 以后的值应该是一个 Action 对象,会被dispatch
方法送出(action.then(dispatch)
),但 reject 以后不会有任何动作;如果 Action 对象的payload
属性是一个 Promise 对象,那么无论 resolve 和 reject,dispatch
方法都会发出 Action。
中间件和异步操作,就介绍到这里。
Redux 入门教程(二):中间件与异步操作的更多相关文章
- Redux 入门教程
Redux 入门教程(三):React-Redux 的用法(53@2016.09.21) Redux 入门教程(二):中间件与异步操作(32@2016.09.20) Redux 入门教程(一):基本用 ...
- RabbitMQ入门教程(二):简介和基本概念
原文:RabbitMQ入门教程(二):简介和基本概念 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn ...
- 无废话ExtJs 入门教程二十一[继承:Extend]
无废话ExtJs 入门教程二十一[继承:Extend] extjs技术交流,欢迎加群(201926085) 在开发中,我们在使用视图组件时,经常要设置宽度,高度,标题等属性.而这些属性可以通过“继承” ...
- 无废话ExtJs 入门教程二十[数据交互:AJAX]
无废话ExtJs 入门教程二十[数据交互:AJAX] extjs技术交流,欢迎加群(521711109) 1.代码如下: 1 <!DOCTYPE html PUBLIC "-//W3C ...
- 无废话ExtJs 入门教程二[Hello World]
无废话ExtJs 入门教程二[Hello World] extjs技术交流,欢迎加群(201926085) 我们在学校里学习任何一门语言都是从"Hello World"开始,这里我 ...
- mongodb入门教程二
title: mongodb入门教程二 date: 2016-04-07 10:33:02 tags: --- 上一篇文章说了mongodb最基本的东西,这边博文就在深入一点,说一下mongo的一些高 ...
- Fastify 系列教程二 (中间件、钩子函数和装饰器)
Fastify 系列教程: Fastify 系列教程一 (路由和日志) Fastify 系列教程二 (中间件.钩子函数和装饰器) 中间件 Fastify 提供了与 Express 和 Restify ...
- SpringBoot入门教程(二)CentOS部署SpringBoot项目从0到1
在之前的博文<详解intellij idea搭建SpringBoot>介绍了idea搭建SpringBoot的详细过程, 并在<CentOS安装Tomcat>中介绍了Tomca ...
- PySide——Python图形化界面入门教程(二)
PySide——Python图形化界面入门教程(二) ——交互Widget和布局容器 ——Interactive Widgets and Layout Containers 翻译自:http://py ...
- Elasticsearch入门教程(二):Elasticsearch核心概念
原文:Elasticsearch入门教程(二):Elasticsearch核心概念 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:ht ...
随机推荐
- java抓取网页或者文件的邮箱号码
抓文件的 package reg; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.i ...
- css图片垂直水平居中及放大(实现水平垂直居中的效果有哪些方法?)
实现水平垂直居中方法有很多种: 一.万能法: 1.已知高度宽度元素的水平垂直居中,利用绝对定位和负边距实现. <style type="text/css"> .wrap ...
- Spring入门详细教程(四)
前言 本篇紧接着spring入门详细教程(三),建议阅读本篇前,先阅读第一篇,第二篇以及第三篇.链接如下: Spring入门详细教程(一) https://www.cnblogs.com/jichi/ ...
- 自动化测试基础篇--Selenium判断元素是够存在
摘自https://www.cnblogs.com/sanzangTst/p/8376101.html selenium+python处于学习阶段,功能实现之后开始整理之前写的代码,突然发现一个功能没 ...
- ArcGIS Server10.2 集群部署注意事项
不接触Server很久了,最近一个省级项目需要提交一个部署方案,由于是省级系统,数据.服务数量都较大,需要考虑采用Server集群的方式来实现.在网上搜罗了以下Server集群的资料,按照步骤一步步来 ...
- 处理程序“SimpleHandlerFactory-Integrated”在其模块列表中有一个错误模块“ManagedPipelineHandler” 先装 .Net 后装 IIS
以管理员身份打开 cmd 运行 cd C:\Windows\Microsoft.NET\Framework\v4.0.30319 运行 aspnet_regiis.exe -i 重新注册 原因是先 ...
- Linux学习历程——Centos 7 cat命令
一.命令介绍 我们使用man cat命令,可以看到cat命令的用途是连接文件或标准输入并打印,简单来说cat命令是用来查看纯文本文件(通常为内容较少的文件),与重定向符号配合使用,可以实现创建文件与合 ...
- Docker详细介绍安装与镜像制作和拉取
一.Docker是什么? 产生背景: 开发和运维之间因为环境不同和导致的矛盾(不同的操作系统.软件环境.应用配置等)DevOps 代码.系统.环境.配置等封装成镜像Image--->运维: 集群 ...
- March 06th, 2018 Week 10th Tuesday
Hope for the best, but prepare for the worst. 抱最好的愿望,做最坏的打算. To hope for the best and prepare for th ...
- [JXOI2018]游戏
嘟嘟嘟 九条可怜竟然有这种良心题,似乎稍稍刷新了我对九条可怜的认识. 首先假设我们求出了所有必须要筛出来的数m,那么\(t(p)\)就只受最后一个数的位置影响. 所以我们枚举最后一个数的位置,然后用组 ...