Redux 进阶:中间件的使用
什么是 middleware
用过 Express 或 Koa 类似框架的同学可能知道,在 Express 中,中间件(middleware)就是在 req 进来之后,在我们真正对 req 进行处理之前,我们先对 req 进行一定的预处理,而这个预处理的过程就由 middleware 来完成。
同理,在 Redux 中,middleware 就是扩展了在 dispatch action 之后,到 action 到达 reducer 之前之间的中间这段时间,而中间的这段时间就是 dispatch 的过程,所以 Redux 的 middleware 的原理就是改造 dispatch。
自定义 middleware
让我们先从一个最简单的日志 middleware 定义开始:
const logger = store => next => action => { console.group('logger'); console.warn('dispatching', action); let result = next(action); console.warn('next state', store.getState()); console.groupEnd(); return result; };
这个 logger
函数就是一个 Redux 中的 middleware ,它的功能是在 store.dispatch(action)
(对应 middleware 中的 next(action)
) 之前和之后分别打印出一条日志。从我们的 logger
中可以看到,我们向 middleware 中传入了 store
,以便我们在 middleware 中获取使用 store.getState()
获取 state,我们还在之后的函数中传入了 next
,而最后传入的 action
就是我们平时 store.dispatch(action)
中的 action,所以 next(action)
对应的就是 dispatch(action)
。
最后我们还需要调用并 next(action)
来执行原本的 dispatch(action)
。
使用 middleware
最后我们可以在使用 createStore()
创建 store 的时候,把这个 middleware 加入进去,使得每次 store.dispathc(action)
的时候都会打印出日志:
import { createStore, applyMiddleware } from 'redux'; // 导入 applyMiddleware const store = createStore(counter, applyMiddleware(logger));
注意,这里我们使用了 Redux 提供的 applyMiddleware()
来在创建 store 的时候应用 middleware,而 applyMiddleware()
返回的是一个应用了 middleware 的 store enhancer,也就是一个增强型的 store。
createStore()
接受三个参数,第一个是 reducer,第二个如果是对象,那么就被作为 store 的初始状态,第三个就是 store enhancer,如果第二个参数是函数,那么就被当作 store enhancer。
关于 applyMiddleware
和我们自定义的 logger
是如何一起工作的,这个我们稍后再讲。
为了说明后一条日志 console.warn('next state', store.getState())
是在执行了 reducer 之后打印出来的,我们在 reducer 中也打印一个消息。改造后的 reducer:
function counter(state = 0, action) { + console.log('hi,这条 log 从 reducer 中来'); switch(action.type) { case 'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default : return state; } }
结果
这里,我使用了 #1 中的计数器作为例子。
可以看到,在 reducer 中打印的消息处于 middleware 日志的中间,这是因为在 logger
middleware 中,将 let result = next(action);
写在了最后一条消息的前面,一旦调用了 next(action)
,就会进入 reducer 或者进入下一个 middleware(如果有的话)。类似 Koa 中间件的洋葱模型。
其实 next(action)
就相当于 store.dispatch(action)
,意思是开始处理下一个 middleware,如果没有 middleware 了就使用原始 Redux 的 store.dispatch(action)
来分发动作。这个是由 Redux 的 applyMiddleware
来处理的,那么 applyMiddleware()
是如何实现对 middleware 的处理的呢?稍后我们会对它进行简单的讲解 。
❓applyMiddleware 是如何实现的
从 applyMiddleware 的设计思路 中,我们可以看到 Redux 中的 store 只是包含一些方法(dispatch()
、subscribe()
、getState()
、replaceReducer()
)的对象。我们可以使用
const next = store.dispatch;
来先引用原始 store 中的 dispatch 方法,然后等到合适的时机,我们再调用它,实现对 dispatch 方法的改造。
Middleware 接收一个名为next
的 dispatch 函数(只是dispatch
函数的引用),并返回一个改造后的 dispatch 函数,而返回的 dispatch 函数又会被作为下一个 middleware 的next
,以此类推。所以,一个 middleware 看起来就会类似这样:
function logger(next) { return action => { console.log('在这里中一些额外的工作') return next(action) } }
其中,在 middleware 中返回的 dispatch 函数接受一个 action
作为参数(和普通的 dispatch 函数一样),最后再调用 next 函数并返回,以便下一个 middleware 继续,如果没有 middleware 则 直接返回。
由于 store 中类似getState()
的方法依旧非常有用,我们将store
作为顶层的参数,使得它可以在所有 middleware 中被使用。这样的话,一个 middleware 的 API 最终看起来就变成这样:
function logger(store) { return next => { return action => { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } } }
值得一提的是,Redux 中使用到了许多函数式编程的思想,如果你对
- curring
- compose
- ...
比较陌生的话,建议你先去补充以下函数式编程思想的内容。applyMiddleware 的源码
❓middleware 有什么应用的场景
- 打印日志,比如上面我们自定义的 middleware;
- 异步 action,比如用户对服务器发起请求,在等待返回响应的时间里,我们可以更新 UI 为 Loading,等到响应返回时,我们再调用
store.dispatch(action)
来更新新的 UI; - ...
一个使用异步 action 请求 Github API 的例子
通过仿照 redux-thunk,我们也可以自己写一个支持异步 action 的 middleware,如下:
const myThunkMiddleware = store => next => action => { if (typeof action === 'function') { // 如果 action 是函数,一般的 action 为纯对象 return action(store.dispatch, store.getState); // 调用 action 函数 } return next(action); };
异步 action creator :
export function fetchGithubUser(username = 'bbbbx') { return dispatch => { // 先 dispatch 一个同步 action dispatch({ type: 'INCREMENT', text: '加载中...' }); // 异步 fetch Github API fetch(`https://api.github.com/search/users?q=${username}`) .then(response => response.json()) .then(responseJSON => { // 异步请求返回后,再 dispatch 一个 action dispatch({ type: 'INCREMENT', text: responseJSON }); }); }; }
修改 reducer,使它可以处理 action 中的 action.text
:
function counter(state = { value: 0, text: '' }, action) { switch(action.type) { case 'INCREMENT': return { value: state.value + 1, text: action.text }; case 'DECREMENT': return { value: state.value - 1, text: action.text }; default : return state; } }
再改造一下 Counter 组件,展示 Github 用户:
// Counter.js class Counter extends React.Component { constructor(props) { super(props); this.state = { username: '' }; } handleChange(event) { this.setState({ username: event.target.value }); } handleSearch(event) { event.preventDefault(); if (this.state.username === '') { return ; } this.props.fetchGithubUser(this.state.username); } render() { const { text, value, increment, decrement } = this.props; let users = text; if (text.items instanceof Array) { if (text.items.length === 0) { users = '用户不存在!'; } else { users = text.items.map(item => ( <li key={item.id}> <p>用户名:<a href={item.html_url}>{item.login}</a></p> <img width={100} src={item.avatar_url} alt='item.avatar_url' /> </li> )); } } return ( <div> Click: {value} times {' '} <button onClick={increment} >+</button>{' '} <button onClick={decrement} >-</button>{' '} <div> <input type='text' onChange={this.handleChange.bind(this)} /> <button onClick={this.handleSearch.bind(this)} >获取 Github 用户</button>{' '} </div> <br /> <b>state.text:{users}</b> </div> ); } }
结果
使用已有的 Redux 中间件
redux-thunk
利用 redux-thunk ,我们可以完成各种复杂的异步 action,尽管 redux-thunk 这个 middleware 只有 数十行 代码。先导入 redux-thunk:
import thunkMiddleware from 'redux-thunk'; const store = createStore( counter, applyMiddleware(thunkMiddleware) );
之后便可定义异步的 action creator 了:
export function incrementAsync(delay = 1000) { return dispatch => { dispatch(decrement()); setTimeout(() => { dispatch(increment()); }, delay); }; }
使用:
<button onClick={increment} >+</button>{' '} <button onClick={decrement} >-</button>{' '} + <button onClick={() => incrementAsync(1000) } >先 - 1 ,后再 + 1</button>{' '}
注意,异步 action creator 要写成 onClick={() => incrementAsync(1000) }
匿名函数调用的形式。
结果