什么是 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 看起来就会类似这样:
javascript">function logger(next) {
return action => {
console.log('在这里中一些额外的工作')
return next(action)
}
}
其中,在 middleware 中返回的 dispatch 函数接受一个 action
作为参数(和普通的 dispatch 函数一样),最后再调用 next 函数并返回,以便下一个 middleware 继续,如果没有 middleware 则 直接返回。
由于 store 中类似getState()
的方法依旧非常有用,我们将store
作为顶层的参数,使得它可以在所有 middleware 中被使用。这样的话,一个 middleware 的 API 最终看起来就变成这样:
javascript">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) }
匿名函数调用的形式。
结果