本文来自#React系列教程:https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzg5MDAzNzkwNA==&action=getalbum&album_id=1566025152667107329)
一. 中间件的使用
1.1. 组件中异步请求
在之前简单的案例中,redux中保存的counter
是一个本地定义的数据,我们可以直接通过同步的操作来dispatch action
,state
就会被立即更新。
但是真实开发中,redux中保存的很多数据可能来自服务器,我们需要进行异步的请求,再将数据保存到redux中。
在之前学习网络请求的时候我们讲过,网络请求可以在class
组件的componentDidMount
中发送,所以我们可以有这样的结构:
我现在完成如下案例操作:
- 在
Home
组件中请求banners
和recommends
的数据; - 在
Profile
组件中展示banners
和recommends
的数据;
redux代码进行如下修改:
在reducer.js
中添加state
初始化数据和reducer
函数中处理代码:
const initialState = {counter: 0,banners: [],recommends: []
}function reducer(state = initialState, action) {switch (action.type) {case ADD_NUMBER:return { ...state, counter: state.counter + action.num };case SUB_NUMBER:return { ...state, counter: state.counter - action.num };case CHANGE_BANNER:return { ...state, banners: action.banners };case CHANGE_RECOMMEND:return { ...state, recommends: action.recommends };default:return state;}
}
constants
中增加常量:
const CHANGE_BANNER = "CHANGE_BANNER";
const CHANGE_RECOMMEND = "CHANGE_RECOMMEND";
actionCreators.js
中添加actions
:
const changeBannersAction = (banners) => ({type: CHANGE_BANNER,banners
}) const changeRecommendsAction = (recommends) => ({type: CHANGE_RECOMMEND,recommends
})
组件中代码代码修改:
Home
组件:
import React, { PureComponent } from 'react';
import { connect } from "react-redux";import axios from 'axios';import {addAction,changeBannersAction,changeRecommendsAction
} from '../store/actionCreators';class Home extends PureComponent {componentDidMount() {axios.get("http://123.207.32.32:8000/home/multidata").then(res => {const data = res.data.data;this.props.changeBanners(data.banner.list);this.props.changeRecommends(data.recommend.list);})}...其他业务代码
}const mapStateToProps = state => {return {counter: state.counter}
}const mapDispatchToProps = dispatch => {return {addNumber: function(number) {dispatch(addAction(number));},changeBanners(banners) {dispatch(changeBannersAction(banners));},changeRecommends(recommends) {dispatch(changeRecommendsAction(recommends));}}
}export default connect(mapStateToProps, mapDispatchToProps)(Home);
Profile
组件:
import React, { PureComponent } from 'react';
import { connect } from "react-redux";import {subAction
} from '../store/actionCreators';class Profile extends PureComponent {render() {return (<div>Profile<div><h2>当前计数: {this.props.counter}</h2><button onClick={e => this.decrement()}>-1</button><button onClick={e => this.subCounter()}>-5</button></div><h1>Banners</h1><ul>{this.props.banners.map((item, index) => {return <li key={item.acm}>{item.title}</li>})}</ul><h1>Recommends</h1><ul>{this.props.recommends.map((item, index) => {return <li key={item.acm}>{item.title}</li>})}</ul></div>)}...其他逻辑代码
}const mapStateToProps = state => {return {counter: state.counter,banners: state.banners,recommends: state.recommends}
}const mapDispatchToProps = dispatch => {return {subNumber: function (number) {dispatch(subAction(number));}}
}export default connect(mapStateToProps, mapDispatchToProps)(Profile);
1.2. redux中异步请求
上面的代码有一个缺陷:
- 我们必须将网络请求的异步代码放到组件的生命周期中来完成;
- 事实上,网络请求到的数据也属于我们状态管理的一部分,更好的一种方式应该是将其也交给redux来管理;
但是在redux中如何可以进行异步的操作呢?
- 答案就是使用中间件(Middleware);
- 学习过Express或Koa框架的童鞋对中间件的概念一定不陌生;
- 在这类框架中,Middleware可以帮助我们在请求和响应之间嵌入一些操作的代码,比如
cookie
解析、日志记录、文件压缩等操作;
redux也引入了中间件(Middleware)的概念:
- 这个中间件的目的是在
dispatch
的action
和最终达到的reducer
之间,扩展一些自己的代码; - 比如日志记录、调用异步接口、添加代码调试功能等等;
我们现在要做的事情就是发送异步的网络请求,所以我们可以添加对应的中间件:
- 这里官网推荐的、包括演示的网络请求的中间件是使用
redux-thunk
;
redux-thunk
是如何做到让我们可以发送异步的请求呢?
- 我们知道,默认情况下的
dispatch(action)
,action
需要是一个JavaScript的对象; redux-thunk
可以让dispatch(action函数)
,action
可以是一个函数;- 该函数会被调用,并且会传给这个函数一个
dispatch
函数和getState
函数;dispatch
函数用于我们之后再次派发action
;getState
函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态;
如何使用 redux-thunk 呢?
- 安装 redux-thunk
yarn add redux-thunk
- 在创建
store
时传入应用了middleware
的enhance
函数
- 通过
applyMiddleware
来结合多个Middleware
, 返回一个enhancer
; - 将
enhancer
作为第二个参数传入到createStore
中;
// 通过applyMiddleware来结合多个Middleware, 返回一个enhancer
const enhancer = applyMiddleware(thunkMiddleware);
// 将enhancer作为第二个参数传入到createStore中
const store = createStore(reducer, enhancer);
- 定义返回一个函数的
action
:
const getHomeMultidataAction = () => {return (dispatch) => {axios.get("http://123.207.32.32:8000/home/multidata").then(res => {const data = res.data.data;dispatch(changeBannersAction(data.banner.list));dispatch(changeRecommendsAction(data.recommend.list));})}
}
- 注意:这里不是返回一个对象了,而是一个函数;
- 该函数在
dispatch
之后会被执行;
- 修改
home.js
中的代码:
import React, { PureComponent } from 'react';
import { connect } from "react-redux";import {addAction,getHomeMultidataAction
} from '../store/actionCreators';class Home extends PureComponent {componentDidMount() {this.props.getHomeMultidata();}...其他逻辑代码
}...mapStatetoPropsconst mapDispatchToProps = dispatch => {return {addNumber: function(number) {dispatch(addAction(number));},getHomeMultidata() {dispatch(getHomeMultidataAction());}}
}export default connect(mapStateToProps, mapDispatchToProps)(Home);
1.3. redux-devtools
我们之前讲过,redux可以方便的让我们对状态进行跟踪和调试,那么如何做到呢?
- redux官网为我们提供了
redux-devtools
的工具; - 利用这个工具,我们可以知道每次状态是如何被修改的,修改前后的状态变化等等;
安装该工具需要两步:
- 第一步:在对应的浏览器中安装相关的插件(比如Chrome浏览器扩展商店中搜索Redux DevTools即可,其他方法可以参考GitHub);
- 第二步:在redux中集成devtools
import { createStore, applyMiddleware, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';
import reducer from './reducer.js';const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;// 通过applyMiddleware来结合多个Middleware, 返回一个enhancer
const enhancer = composeEnhancers(applyMiddleware(thunkMiddleware));
// 将enhancer作为第二个参数传入到createStore中
const store = createStore(reducer, enhancer);export default store;
trace打开:
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;
1.4. redux-saga
1.4.1. ES6的generator
saga中间件使用了ES6的generator
语法,所以我们有必须简单讲解一下:
- 注意:我这里并没有列出
generator
的所有用法,事实上它的用法非常的灵活,大家可以自行去学习一下。
在JavaScript中编写一个普通的函数,进行调用会立即拿到这个函数的返回结果:
function foo() {return "Hello World";
}foo() // Hello World
如果我们将这个函数编写成一个生成器函数:
function *foo() {yield "Hello";yield "World";
}const iterator = foo();
console.log(iterator, typeof iterator); // 一个object类型的iterator对象
调用iterator
的next
函数,会销毁一次迭代器,并且返回一个yield
的结果:
// 调用一次next()是消耗一次迭代器
iterator.next(); // {value: "Hello", done: false}
iterator.next(); // {value: "World", done: false}
iterator.next(); // {value: undefined, done: true}
研究一下foo
生成器函数代码的执行顺序:
function *foo() {console.log("111111");yield "Hello";console.log("222222");yield "World";console.log("333333");
}// 调用一次next()是消耗一次迭代器
iterator.next(); // {value: "Hello", done: false}
// 打印111111
iterator.next(); // {value: "World", done: false}
// 打印222222
iterator.next(); // {value: undefined, done: true}
// 打印333333
generator
和promise
一起使用:
function *bar() {const result = yield new Promise((resolve, reject) => {setTimeout(() => {resolve("Hello Generator");return "Hello";}, 2000);});console.log(result);
}const bIterator = bar();
bIterator.next().value.then(res => {bIterator.next(res);
});
1.4.2. redux-saga的使用
- 装 redux-saga
yarn add redux-saga
- 集成 redux-saga 中间件
import { createStore, applyMiddleware, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';
import createSagaMiddleware from 'redux-saga';
import reducer from './reducer.js';
import mySaga from './saga';// 通过createSagaMiddleware函数来创建saga中间件
const sagaMiddleware = createSagaMiddleware();const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;// 通过applyMiddleware来结合多个Middleware, 返回一个enhancer
const enhancer = composeEnhancers(applyMiddleware(thunkMiddleware, sagaMiddleware));
// 将enhancer作为第二个参数传入到createStore中
const store = createStore(reducer, enhancer);// 必须启动saga中间件,并且传入其要监听的generator
sagaMiddleware.run(mySaga);export default store;
saga.js
文件的编写:
import { takeEvery, put, all } from 'redux-saga/effects';
import axios from 'axios';import {FETCH_HOME_MULTIDATA
} from "./constants";
import {changeBannersAction,changeRecommendsAction,
} from './actionCreators';function* fetchHomeMultidata(action) {const res = yield axios.get("http://123.207.32.32:8000/home/multidata");console.log(res);const data = res.data.data;yield all([put(changeBannersAction(data.banner.list)),put(changeRecommendsAction(data.recommend.list))])
}function* mySaga() {yield takeEvery(FETCH_HOME_MULTIDATA, fetchHomeMultidata)
}export default mySaga;
takeEvery
:可以传入多个监听的actionType
,每一个都可以被执行(对应有一个takeLastest
,会取消前面的)put
:在saga中派发action
不再是通过dispatch
,而是通过put
;all
:可以在yield
的时候put
多个action
;
二. 中间件的原理
2.1. 打印日志需求
前面我们已经提过,中间件的目的是在redux中插入一些自己的操作:
- 比如我们现在有一个需求,在
dispatch
之前,打印一下本次的action
对象,dispatch
完成之后可以打印一下最新的store state
; - 也就是我们需要将对应的代码插入到redux的某部分,让之后所有的
dispatch
都可以包含这样的操作;
如果没有中间件,我们是否可以实现类似的代码呢?
当然可以,类似下面的方式即可:
console.log("dispatching:", addAction(5));
store.dispatch(addAction(5));
console.log("new state:", store.getState());console.log("dispatching:", addAction(10));
store.dispatch(subAction(10));
console.log("new state:", store.getState());
但是这种方式缺陷非常明显:
- 首先,每一次的
dispatch
操作,我们都需要在前面加上这样的逻辑代码; - 其次,存在大量重复的代码,会非常麻烦和臃肿;
是否有一种更优雅的方式来处理这样的相同逻辑呢?
- 我们可以将代码封装到一个独立的函数中
function dispatchAndLog(action) {console.log("dispatching:", action);store.dispatch(addAction(5));console.log("新的state:", store.getState());
}dispatchAndLog(addAction(10));
但是这样的代码有一个非常大的缺陷:
- 调用者(使用者)在使用我的
dispatch
时,必须使用我另外封装的一个函数dispatchAndLog
; - 显然,对于调用者来说,很难记住这样的API,更加习惯的方式是直接调用
dispatch
;
我们来进一步对代码进行优化;
2.2. 修改dispatch
事实上,我们可以利用一个hack一点的技术:Monkey Patching,利用它可以修改原有的程序逻辑;
我们对代码进行如下的修改:
let next = store.dispatch;function dispatchAndLog(action) {console.log("dispatching:", addAction(10));next(addAction(5));console.log("新的state:", store.getState());
}store.dispatch = dispatchAndLog;
- 这样就意味着我们已经直接修改了
dispatch
的调用过程; - 在调用
dispatch
的过程中,真正调用的函数其实是dispatchAndLog
;
当然,我们可以将它封装到一个模块中,只要调用这个模块中的函数,就可以对store
进行这样的处理:
function patchLogging(store) {let next = store.dispatch;function dispatchAndLog(action) {console.log("dispatching:", action);next(addAction(5));console.log("新的state:", store.getState());}store.dispatch = dispatchAndLog;
}
2.3. thunk需求
redux-thunk
的作用:
- 我们知道redux中利用一个中间件
redux-thunk
可以让我们的dispatch
不再只是处理对象,并且可以处理函数; - 那么
redux-thunk
中的基本实现过程是怎么样的呢?事实上非常的简单。
我们来看下面的代码:
function patchThunk(store) {let next = store.dispatch;function dispatchAndThunk(action) {if (typeof action === "function") {action(store.dispatch, store.getState);} else {next(action);}}store.dispatch = dispatchAndThunk;
}
- 我们又对
dispatch
进行转换,这个dispatch
会判断传入的
将两个patch
应用起来,进行测试:
patchLogging(store);
patchThunk(store);store.dispatch(addAction(10));function getData(dispatch) {setTimeout(() => {dispatch(subAction(10));}, 1000)
}// 传入函数
store.dispatch(getData);
2.4. 合并中间件
单个调用某个函数来合并中间件并不是特别的方便,我们可以封装一个函数来实现所有的中间件合并:
function applyMiddleware(store, middlewares) {middlewares = middlewares.slice();middlewares.forEach(middleware => {store.dispatch = middleware(store);})
}applyMiddleware(store, [patchLogging, patchThunk]);
我们来理解一下上面操作之后,代码的流程:
当然,真实的中间件实现起来会更加的灵活,这里我们仅仅做一个抛砖引玉,有兴趣可以参考redux合并中间件的源码流程。