Redux与它的中间件:redux-thunk,redux-actions,redux-promise,redux-sage
序言
这里要讲的就是一个redux在react中的应用问题,讲一讲redux,react-redux,redux-thunk,redux-actions,redux-promise,redux-sage这些包的作用和他们解决的问题。
因为不想把篇幅拉得太长,所以没有太多源码分析和语法讲解,能怎么简单就怎么简单。
redux
先看看百度百科上面redux的一张图:
这是redux在github上的介绍:redux用于js程序,是一个可预测的状态容器。
在这里我们首先要明白的是什么叫可预测?什么叫状态容器?
什么叫状态?实际上就是变量,对话框显示或隐藏的变量,一杯奶茶多少钱的变量。
那么这个状态容器,实际上就是一个存放这些变量的变量。
你创建了一个全局变量叫store,然后将代码中控制各个状态的变量存放在里面,那么现在store就叫做状态容器。
什么叫可预测?
你在操作这个store的时候,总是用store.price的方式来设置值,这种操作数据的方式很原始,对于复杂的系统而言永远都不知道程序在运行的过程中发生了什么。
那么现在我们都通过发送一个action去做修改,而store在接收到action后会使用reducer对action传递的数据做处理,最后应用到store中。
相对于store.price的方式来修改者,这种方式无疑更麻烦,但是这种方式的好处就是,每一个action里面都可以写日志,可以记录各种状态的变动,这就是可预测。
所以如果你的程序很简单,你完全没有必要去用redux。
看看redux的示例代码:
actiontypes.js:
export const change_btn_text = 'change_btn_text';
actions.js:
import * as t from './actiontypes'; export const changebtntext = (text) => { return { type: t.change_btn_text, payload: text }; };
reducers.js:
import * as t from './actiontypes'; const initialstate = { btntext: '我是按钮', }; const pagemainreducer = (state = initialstate, action) => { switch (action.type) { case t.change_btn_text: return { ...state, btntext: action.payload }; default: return state; } }; export default pagemainreducer;
index.js
import { createstore } from 'redux'; import reducer from './reducers'; import { changebtntext } from './actions'; const store = createstore(reducer); // 开始监听,每次state更新,那么就会打印出当前状态 const unsubscribe = store.subscribe(() => { console.info(store.getstate()); }); // 发送消息 store.dispatch(changebtntext('点击了按钮')); // 停止监听state的更新 unsubscribe();
这里就不解释什么语法作用了,网上这样的资料太多了。
redux与react的结合:react-redux
redux是一个可预测的状态容器,跟react这种构建ui的库是两个相互独立的东西。
redux要应用到react中,很明显action,reducer,dispatch这几个阶段并不需要改变,唯一需要考虑的是redux中的状态需要如何传递给react组件。
很简单,只需要每次要更新数据时运用store.getstate获取到当前状态,并将这些数据传递给组件即可。
那么问题来了,如何让每个组件都获取到store呢?
当然是将store作为一个值传递给根组件,然后store就会一级一级往下传,使得每个组件都能获取到store的值。
但是这样太繁琐了,难道每个组件需要写一个传递store的逻辑?为了解决这个问题,那么得用到react的context玩法,通过在根组件上将store放在根组件的context中,然后在子组件中通过context获取到store。
react-redux的主要思路也是如此,通过嵌套组件provider将store放到context中,通过connect这个高阶组件,来隐藏取store的操作,这样我们就不需要每次去操作context写一大堆代码那么麻烦了。
然后我们再来基于之前的redux示例代码给出react-redux的使用演示代码,其中action和reduce部分不变,先增加一个组件pagemain:
const pagemain = (props) => { return ( <div> <button onclick={() => { props.changetext('按钮被点击了'); }} > {props.btntext} </button> </div> ); }; // 映射store.getstate()的数据到pagemain const mapstatetoprops = (state) => { return { btntext: state.pagemain.btntext, }; }; // 映射使用了store.dispatch的函数到pagemain const mapdispatchtoprops = (dispatch) => { return { changetext: (text) => { dispatch(changebtntext(text)); } }; }; // 这个地方也可以简写,react-redux会自动做处理 const mapdispatchtoprops = { changetext: changebtntext }; export default connect(mapstatetoprops, mapdispatchtoprops)(pagemain);
注意上面的state.pagemain.btntext,这个pagemain是我用redux的combinereducers将多个reducer合并后给的原先的reducer一个命名。
它的代码如下:
import { combinereducers } from 'redux'; import pagemain from './components/pagemain/reducers'; const reducer = combinereducers({ pagemain }); export default reducer;
然后修改index.js:
import react from 'react'; import { createstore } from 'redux'; import { provider } from 'react-redux'; import reactdom from 'react-dom'; import reducer from './reducers'; import pagemain from './components/pagemain'; const store = createstore(reducer); const app = () => ( <provider store={store}> <pagemain /> </provider> ); reactdom.render(<app />, document.getelementbyid('app'));
redux的中间件
之前我们讲到redux是个可预测的状态容器,这个可预测在于对数据的每一次修改都可以进行相应的处理和记录。
假如现在我们需要在每次修改数据时,记录修改的内容,我们可以在每一个dispatch前面加上一个console.info记录修改的内容。
但是这样太繁琐了,所以我们可以直接修改store.dispatch:
let next = store.dispatch store.dispatch = (action)=> { console.info('修改内容为:', action) next(action) }
redux中也有同样的功能,那就是applymiddleware。直译过来就是“应用中间件”,它的作用就是改造dispatch函数,跟上面的玩法基本雷同。
来一段演示代码:
import { createstore, applymiddleware } from 'redux'; import reducer from './reducers'; const store = createstore(reducer, applymiddleware(curstore => next => action => { console.info(curstore.getstate(), action); return next(action); }));
看起来挺奇怪的玩法,但是理解起来并不难。通过这种返回函数的方法,使得applymiddleware内部以及我们使用时可以处理store和action,并且这里next的应用就是为了使用多个中间件而存在的。
而通常我们没有必要自己写中间件,比如日志的记录就已经有了成熟的中间件:redux-logger,这里给一个简单的例子:
import { applymiddleware, createstore } from 'redux'; import createlogger from 'redux-logger'; import reducer from './reducers'; const logger = createlogger(); const store = createstore( reducer, applymiddleware(logger) );
这样就可以记录所有action及其发送前后的state的日志,我们可以了解到代码实际运行时到底发生了什么。
redux-thunk:处理异步action
在上面的代码中,我们点击按钮后,直接修改了按钮的文本,这个文本是个固定的值。
actions.js:
import * as t from './actiontypes'; export const changebtntext = (text) => { return { type: t.change_btn_text, payload: text }; };
但是在我们实际生产的过程中,很多情况都是需要去请求服务端拿到数据再修改的,这个过程是一个异步的过程。又或者需要settimeout去做一些事情。
我们可以去修改这一部分如下:
const mapdispatchtoprops = (dispatch) => { return { changetext: (text) => { dispatch(changebtntext('正在加载中')); axios.get('http://test.com').then(() => { dispatch(changebtntext('加载完毕')); }).catch(() => { dispatch(changebtntext('加载有误')); }); } }; };
实际上,我们每天不知道要处理多少这样的代码。
但是问题来了,异步操作相比同步操作多了一个很多确定因素,比如我们展示正在加载中时,可能要先要做异步操作a,而请求后台的过程却非常快,导致加载完毕先出现,而这时候操作a才做完,然后再展示加载中。
所以上面的这个玩法并不能满足这种情况。
这个时候我们需要去通过store.getstate获取当前状态,从而判断到底是展示正在加载中还是展示加载完毕。
这个过程就不能放在mapdispatchtoprops中了,而需要放在中间件中,因为中间件中可以拿到store。
首先创造store的时候需要应用react-thunk,也就是
import { createstore, applymiddleware } from 'redux'; import thunk from 'redux-thunk'; import reducer from './reducers'; const store = createstore( reducer, applymiddleware(thunk) );
它的源码超级简单:
function createthunkmiddleware(extraargument) { return ({ dispatch, getstate }) => next => action => { if (typeof action === 'function') { return action(dispatch, getstate, extraargument); } return next(action); }; } const thunk = createthunkmiddleware(); thunk.withextraargument = createthunkmiddleware; export default thunk;
从这个里面可以看出,它就是加强了dispatch的功能,在dispatch一个action之前,去判断action是否是一个函数,如果是函数,那么就执行这个函数。
那么我们使用起来就很简单了,此时我们修改actions.js
import axios from 'axios'; import * as t from './actiontypes'; export const changebtntext = (text) => { return { type: t.change_btn_text, payload: text }; }; export const changebtntextasync = (text) => { return (dispatch, getstate) => { if (!getstate().isloading) { dispatch(changebtntext('正在加载中')); } axios.get(`http://test.com/${text}`).then(() => { if (getstate().isloading) { dispatch(changebtntext('加载完毕')); } }).catch(() => { dispatch(changebtntext('加载有误')); }); }; };
而原来mapdispatchtoprops中的玩法和同步action的玩法是一样的:
const mapdispatchtoprops = (dispatch) => { return { changetext: (text) => { dispatch(changebtntextasync(text)); } }; };
通过redux-thunk我们可以简单地进行异步操作,并且可以获取到各个异步操作时期状态的值。
redux-actions:简化redux的使用
redux虽然好用,但是里面还是有些重复代码,所以有了redux-actions来简化那些重复代码。
这部分简化工作主要集中在构造action和处理reducers方面。
先来看看原先的actions
import axios from 'axios'; import * as t from './actiontypes'; export const changebtntext = (text) => { return { type: t.change_btn_text, payload: text }; }; export const changebtntextasync = () => { return (dispatch, getstate) => { if (!getstate().isloading) { dispatch(changebtntext('正在加载中')); } axios.get('http://test.com').then(() => { if (getstate().isloading) { dispatch(changebtntext('加载完毕')); } }).catch(() => { dispatch(changebtntext('加载有误')); }); }; };
然后再来看看修改后的:
import axios from 'axios'; import * as t from './actiontypes'; import { createaction } from 'redux-actions'; export const changebtntext = createaction(t.change_btn_text, text => text); export const changebtntextasync = () => { return (dispatch, getstate) => { if (!getstate().isloading) { dispatch(changebtntext('正在加载中')); } axios.get('http://test.com').then(() => { if (getstate().isloading) { dispatch(changebtntext('加载完毕')); } }).catch(() => { dispatch(changebtntext('加载有误')); }); }; };
这一块代码替换上面的部分代码后,程序运行结果依然保持不变,也就是说createaction只是对上面的代码进行了简单的封装而已。
这里注意到,异步的action就不要用createaction,因为这个createaction返回的是一个对象,而不是一个函数,就会导致redux-thunk的代码没有起到作用。
这里也可以使用createactions这个函数同时创建多个action,但是讲道理,这个语法很奇怪,用createaction就好。
同样redux-actions对reducer的部分也进行了处理,比如handleaction以及handelactions。
先来看看原先的reducers
import * as t from './actiontypes'; const initialstate = { btntext: '我是按钮', }; const pagemainreducer = (state = initialstate, action) => { switch (action.type) { case t.change_btn_text: return { ...state, btntext: action.payload }; default: return state; } }; export default pagemainreducer;
然后使用handleactions来处理
import { handleactions } from 'redux-actions'; import * as t from './actiontypes'; const initialstate = { btntext: '我是按钮', }; const pagemainreducer = handleactions({ [t.change_btn_text]: { next(state, action) { return { ...state, btntext: action.payload, }; }, throw(state) { return state; }, }, }, initialstate); export default pagemainreducer;
这里handleactions可以加入异常处理,并且帮助处理了初始值。
注意,无论是createaction还是handleaction都只是对代码做了一点简单的封装,两者可以单独使用,并不是说使用了createaction就必须要用handleaction。
redux-promise:redux-actions的好基友,轻松创建和处理异步action
还记得上面在使用redux-actions的createaction时,我们对异步的action无法处理。
因为我们使用createaction后返回的是一个对象,而不是一个函数,就会导致redux-thunk的代码没有起到作用。
而现在我们将使用redux-promise来处理这类情况。
可以看看之前我们使用 createaction的例子:
export const changebtntext = createaction(t.change_btn_text, text => text);
现在我们先加入redux-promise中间件:
import thunk from 'redux-thunk'; import createlogger from 'redux-logger'; import promisemiddleware from 'redux-promise'; import reducer from './reducers'; const store = createstore(reducer, applymiddleware(thunk, createlogger, promisemiddleware));
然后再处理异步action:
export const changebtntextasync = createaction(t.change_btn_text_async, (text) => { return axios.get(`http://test.com/${text}`); });
可以看到我们这里返回的是一个promise对象.(axios的get方法结果就是promise对象)
我们还记得redux-thunk中间件,它会去判断action是否是一个函数,如果是就执行。
而我们这里的redux-promise中间件,他会在dispatch时,判断如果action不是类似
{ type:'', payload: '' }
这样的结构,也就是 fsa,那么就去判断是否为promise对象,如果是就执行action.then的玩法。
很明显,我们createaction后的结果是fsa,所以会走下面这个分支,它会去判断action.payload是否为promise对象,是的话那就
action.payload .then(result => dispatch({ ...action, payload: result })) .catch(error => { dispatch({ ...action, payload: error, error: true }); return promise.reject(error); })
也就是说我们的代码最后会转变为:
axios.get(`http://test.com/${text}`) .then(result => dispatch({ ...action, payload: result })) .catch(error => { dispatch({ ...action, payload: error, error: true }); return promise.reject(error); })
这个中间件的代码也很简单,总共19行,大家可以在github上直接看看。
redux-sage:控制器与更优雅的异步处理
我们的异步处理用的是redux-thunk + redux-actions + redux-promise,其实用起来还是蛮好用的。
但是随着es6中generator的出现,人们发现用generator处理异步可以更简单。
而redux-sage就是用generator来处理异步。
以下讲的知识是基于generator的,如果您对这个不甚了解,可以简单了解一下相关知识,大概需要2分钟时间,并不难。
redux-sage文档并没有说自己是处理异步的工具,而是说用来处理边际效应(side effects),这里的边际效应你可以理解为程序对外部的操作,比如请求后端,比如操作文件。
redux-sage同样是一个redux中间件,它的定位就是通过集中控制action,起到一个类似于mvc中控制器的效果。
同时它的语法使得复杂异步操作不会像promise那样出现很多then的情况,更容易进行各类测试。
这个东西有它的好处,同样也有它不好的地方,那就是比较复杂,有一定的学习成本。
并且我个人而言很不习惯generator的用法,觉得promise或者await更好用。
这里还是记录一下用法,毕竟有很多框架都用到了这个。
应用这个中间件和我们的其他中间件没有区别:
import react from 'react'; import { createstore, applymiddleware } from 'redux'; import promisemiddleware from 'redux-promise'; import createsagamiddleware from 'redux-saga'; import {watchdelaychangebtntext} from './sagas'; import reducer from './reducers'; const sagamiddleware = createsagamiddleware(); const store = createstore(reducer, applymiddleware(promisemiddleware, sagamiddleware)); sagamiddleware.run(watchdelaychangebtntext);
创建sage中间件后,然后再将其中间件接入到store中,最后需要用中间件运行sages.js返回的generator,监控各个action。
现在我们给出sages.js的代码:
import { delay } from 'redux-saga'; import { put, call, takeevery } from 'redux-saga/effects'; import * as t from './components/pagemain/actiontypes'; import { changebtntext } from './components/pagemain/actions'; const consolemsg = (msg) => { console.info(msg); }; /** * 处理编辑效应的函数 */ export function* delaychangebtntext() { yield delay(1000); yield put(changebtntext('123')); yield call(consolemsg, '完成改变'); } /** * 监控action的函数 */ export function* watchdelaychangebtntext() { yield takeevery(t.watch_change_btn_text, delaychangebtntext); }
在redux-sage中有一类用来处理边际效应的函数比如put、call,它们的作用是为了简化操作。
比如put相当于redux的dispatch的作用,而call相当于调用函数。(可以参考上面代码中的例子)
还有另一类函数就是类似于takeevery,它的作用就是和普通redux中间件一样拦截到action后作出相应处理。
比如上面的代码就是拦截到t.watch_change_btn_text这个类型的action,然后调用delaychangebtntext。
然后可以回看我们之前的代码,有这么一行代码:
sagamiddleware.run(watchdelaychangebtntext);
这里实际就是引入监控的这个生成器后,再运行监控生成器。
这样我们在代码里面dispatch类型为t.watch_change_btn_text的action时就会被拦截然后做出相应处理。
当然这里有人可能会提出疑问,难道每一个异步都要这么写吗,那岂不是要run很多次?
当然不是这个样子,我们可以在sage中这么写:
export default function* rootsaga() { yield [ watchdelaychangebtntext(), watchotheraction() ] }
我们只需要按照这个格式去写,将watchdelaychangebtntext这样用于监控action的生成器放在上面那个代码的数组中,然后作为一个生成器返回。
现在只需要引用这个rootsaga即可,然后run这个rootsaga。
以后如果要监控更多的action,只需要在sages.js中加上新的监控的生成器即可。
通过这样的处理,我们就将sages.js做成了一个像mvc中的控制器的东西,可以用来处理各种各样的action,处理复杂的异步操作和边际效应。
但是这里要注意,一定要加以区分sages.js中使用监控的action和真正功能用的action,比如加个watch关键字,以免业务复杂后代码混乱。
总结
总的来说:
- redux是一个可预测的状态容器,
- react-redux是将store和react结合起来,使得数据展示和修改对于react项目而言更简单
- redux中间件就是在dispatch action前对action做一些处理
- redux-thunk用于对异步做操作
- redux-actions用于简化redux操作
- redux-promise可以配合redux-actions用来处理promise对象,使得异步操作更简单
- redux-sage可以起到一个控制器的作用,集中处理边际效用,并使得异步操作的写法更优雅。
ok,虽然说不想写那么多,结果还是写了一大堆。
如果您觉得对您还有帮助,那么也请点个赞吧。