乐趣区

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) } 匿名函数调用的形式。
结果

退出移动版