redux中间件
中间件原理
redux中间件可以理解为类似node的web框架(express、koa)所涉及到的中间件,express、koa中间件其实是一个个放置在请求(request)到响应(response)过程之间的需要执行的代码段,如:日志记录、请求处理、错误处理等。与express、koa相比较redux的中间件解决是不同的问题,但是在理论上都是采取的同一种解决方法。redux中间件都是从redux的dispatch方法着手而产生的第三方库。我们可以使用redux中间件做如:日志(redux-logger)、异步调用(redux-thunk、redux-promise等)、路由。下面以日志处理为例讲述redux中间件的原理。
使用redux的好处就是,每一个状态的变化都是透明的可以预测的,每次一个action被dispatched,新的状态便会被计算和保存。状态不能自己改变,需要dispatch指定的action状态才会发生响应的变化。如果希望在dispatch每一个action时能在控制台中看到触发的action的类型,同时也能看到触发后状态的变化。如何实现?下面有以下解决方案:
reudx全局只有一个store(ps:示例中不使用react-redux)
手动log
比较直接的方式是在组件内部直接打日志。
class MessageListWrapper extends React.Component {
constructor(props) {
super(props);
this.state = {
messages: store.getState().messages
};
this.unSubscribeHandle = () => { };
}
render() {
return (
<MessageList messages={this.state.messages}
getMessages={this.getMessages}
addMessage={this.addMessage}
deleteMessage={this.deleteMessage}></MessageList>
)
}
componentDidMount() {
this.unSubscribeHandle = store.subscribe(() => {
// 手动log state
console.log('next state',store.getState())
this.setState(store.getState())
});
}
componentWillUnmount() {
this.unSubscribeHandle();
}
addMessage() {
console.log('dispatching',addMessageAction(new Date().getTime()));
store.dispatch(addMessageAction(new Date().getTime()));
}
deleteMessage() {
console.log('dispatching',deleteMessageAction());
store.dispatch(deleteMessageAction());
}
}
手动log的方法确实可以实现,但每次写一个组件都要写log这显然不是很明智的选择。
函数包裹dispatch
const dispatchAndLog = (store, action) => {
console.log('dispatching', action);
store.dispatch(action);
console.log('next state', store.getState());
}
class MessageListWrapper extends React.Component {
constructor(props) {
super(props);
this.state = {
messages: store.getState().messages
};
this.unSubscribeHandle = () => { };
}
render() {
return (
<MessageList messages={this.state.messages}
getMessages={this.getMessages}
addMessage={this.addMessage}
deleteMessage={this.deleteMessage}></MessageList>
)
}
componentDidMount() {
this.unSubscribeHandle = store.subscribe(() => {
this.setState(store.getState())
});
}
componentWillUnmount() {
this.unSubscribeHandle();
}
addMessage() {
dispatchAndLog(store, addMessageAction(new Date().getTime()));
}
deleteMessage() {
dispatchAndLog(store, deleteMessageAction());
}
}
重构store.dispatch
方式1
const originDispatch = store.dispatch;
store.dispatch = (action) => {
console.log('dispatching', action);
originDispatch(action);
console.log('next state', store.getState());
}
class MessageListWrapper extends React.Component {
constructor(props) {
super(props);
this.state = {
messages: store.getState().messages
};
this.unSubscribeHandle = () => { };
}
render() {
return (
<MessageList messages={this.state.messages}
getMessages={this.getMessages}
addMessage={this.addMessage}
deleteMessage={this.deleteMessage}></MessageList>
)
}
componentDidMount() {
this.unSubscribeHandle = store.subscribe(() => {
this.setState(store.getState())
});
}
componentWillUnmount() {
this.unSubscribeHandle();
}
addMessage() {
store.dispatch(addMessageAction(new Date().getTime()));
}
deleteMessage() {
store.dispatch(deleteMessageAction());
}
}
如果在redux的使用过程中不光只patch一次store.dispacth,还希望添加异常处理等功能,具体改进方案如下。
方式2
const logger1 = function (store) {
let next = store.dispatch;
store.dispatch = function dispatchAndLog1(action) {
console.log('dispatching logger1', action);
next(action);
console.log('next state logger1', store.getState());
}
}
const logger2 = function (store) {
let next = store.dispatch;
store.dispatch = function dispatchAndLog2(action) {
console.log('dispatching logger2', action);
next(action);
console.log('next state logger2', store.getState());
}
}
logger1(store);
logger2(store);
分析上述代码:
- 执行logger1
- logger1内部 next为redux原始的dispatch,返回dispatchAndLog1(dispatchAndLog1内部执行redux原始的dispatch),赋值给store.dispatch,
- 执行logger2。
- logger2内部 此时的store.dispacth为dispatchAndLog2(dispatchAndLog2内部执行dispatchAndLog1),赋值给store.dispatch。
隐藏 dispatch
之前我们都是直接把store.dispatch换掉,如果我們不直接换掉,而是传回一個function,这样的好处就是我们可以在一个地方统一的进行处理,store.dispacth补丁的问题。
// 下面的代码只隐藏了直接在外面修改store.dispacth,改为在函数内部替换。
const logger1 = function (store) {
// 此时next为最原始的store.dispacth
let next = store.dispatch;
return function dispatchAndLog1(action) {
console.log('dispatching logger1', action);
next(action);
console.log('next state logger1', store.getState());
}
}
const logger2 = function (store) {
// 此时next为logger1返回的函数
let next = store.dispatch;
return function dispatchAndLog2(action) {
console.log('dispatching logger2', action);
next(action);
console.log('next state logger2', store.getState());
}
}
const applyMiddlewareByMonkeypatching = (store, middlewares) => {
middlewares = middlewares.slice();
// 将middlewares翻转
middlewares.reverse();
middlewares.forEach((middleware) => {
store.dispatch = middleware(store);
});
}
applyMiddlewareByMonkeypatching(store,[logger2,logger1]);
分析上述代码:
- 执行applyMiddlewareByMonkeypatching
- applyMiddlewareByMonkeypatching内部middlewares翻转,遍历middlewares,将middlewares返回的函数一次赋值给store.dispatch,优先执行logger1。
- logger1内部 next为redux原始的dispatch,返回dispatchAndLog1(dispatchAndLog1内部执行redux原始的dispatch),赋值给store.dispatch,再次执行logger2。
- logger2内部 此时的store.dispacth为dispatchAndLog2(dispatchAndLog2内部执行dispatchAndLog1),赋值给store.dispatch。
采用上述方式后,组件中使用store.dispatch()方法后将执行如下流程:
- 执行dispatchAndLog2函数内部函数,dispatchAndLog2内next为logger1执行后所返回的函数即dispatchAndLog1。
- dispatchAndLog1函数内部next为redux原始的dispatch,执行完后回到dispatchAndLog2继续执行剩余的代码。
控制台显示如下:
移除Monkeypatching(store.dispatch)
我们对store.dispatch进行补丁目的就是,当前的中间件可以使用上一次补丁过的store.dispatch。
let next = store.dispatch;
如果中间件内这段代码没有添加那么就无法达成调用链的效果。但这种写法还可以将next作为参数传递进去,修改代码如下:
const logger1 = function (store, next) {
return function dispatchAndLog1(action) {
console.log('dispatching logger1', action);
next(action);
console.log('next state logger1', store.getState());
}
}
const logger2 = function (store, next) {
return function dispatchAndLog2(action) {
console.log('dispatching logger2', action);
next(action);
console.log('next state logger2', store.getState());
}
}
let dispatch = store.dispatch;
dispatch = logger1(store, dispatch);
dispatch = logger2(store, dispatch);
store = Object.assign({}, store, { dispatch });
将上述代码再进行一次改写:
// ps: logger1、logger2代码不变
function applyMiddleware(originStore, middlewares) {
let dispatch = originStore.dispatch;
middlewares = middlewares.slice();
middlewares.reverse();
middlewares.forEach(middleware => {
dispatch = middleware(originStore, dispatch);
})
store = Object.assign({}, originStore, { dispatch });
}
applyMiddleware(store, [logger2, logger1]);
JS中的柯里化(currying)
又称部分求值(Partial Evaluation),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术,柯里化是函数式编程中的一个技巧,外部函数处理部分应用,剩下的由外部函数的返回函数处理,柯里化有三种常见作用:1.参数复用2.提前返回3.延迟计算/运行。 具体请参考: http://www.zhangxinxu.com/wordpress/2013/02/js-currying/
将上述实现改写为柯里化的形式:
const logger1 = function (store) {
return function (next) {
return function dispatchAndLog1(action) {
console.log('dispatching logger1', action);
next(action);
console.log('next state logger1', store.getState());
}
}
}
const logger2 = function (store) {
return function (next) {
return function dispatchAndLog2(action) {
console.log('dispatching logger2', action);
next(action);
console.log('next state logger2', store.getState());
}
}
}
function applyMiddleware(originStore, middlewares) {
let dispatch = originStore.dispatch;
middlewares = middlewares.slice();
middlewares.reverse();
middlewares.forEach(middleware => {
dispatch = middleware(originStore)(dispatch);
})
store = Object.assign({}, originStore, { dispatch });
}
applyMiddleware(store, [logger2, logger1]);
将logger1、logger2改写:
const logger1 = (store) => (next) => (action) => {
console.log('dispatching logger1', action);
next(action);
console.log('next state logger1', store.getState());
}
const logger2 = (store) => (next) => (action) => {
console.log('dispatching logger2', action);
next(action);
console.log('next state logger2', store.getState());
}
看一下官方提供的使用中间件的方法:
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, combineReducers,applyMiddleware } from 'redux';
// ... 其他代码省略
const logger1 = (store) => (next) => (action) => {
console.log('dispatching logger1', action);
next(action);
console.log('next state logger1', store.getState());
}
const logger2 = (store) => (next) => (action) => {
console.log('dispatching logger2', action);
next(action);
console.log('next state logger2', store.getState());
}
let createStoreWithMiddleware = applyMiddleware(logger2,logger1)(createStore);
let store = createStoreWithMiddleware(reducer);
applyMiddleware源码如下:
从上面的applyMiddleware.jsx源码中可以看其实现和我们之前修改后的代码实现方式很接近了。
官方提供的方法需要我们传入中间件(logger1、logger2),执行后返回需要createStore方法函数,传入createStore再次执行。返回的函数与我们之前的写的函数很相似了,不过store是通过createStore方法生成的。这里的compose其实和我们之前写的执行 dispatch = middleware(originStore)(dispatch);
很相似的。只不过compose增加了代码的灵活度。compose.jsx 源码如下:
源码中funcs.reduce
效果可以理解为 middlewares.reverse()
将中间件从后至前执行,所以使用中间时需要注意这一点。
演示代码:https://github.com/xubaoshi/learn/tree/master/project/js/react/redux/middleware