Redux专题:中间件

本文是『horseshoe·Redux专题』系列文章之一,后续会有更多专题推出javascript

来个人 GitHub repo 阅读完整的专题文章前端

来个人 我的博客 得到无与伦比的阅读体验java

Redux暴露很是少的API,优雅的将单向数据流落地。但有这些,Redux的做者Dan Abramov仍然以为远远不够。一个工具的强大之处体如今它的扩展能力上。Redux的中间件机制让这种扩展能力一样变的异常优雅。react

中间件在前端的意思是插入某两个流程之间的一段逻辑。具体到Redux,就是在dispatch一个动做先后插入第三方的处理函数。git

使用

还记得吗?Store构造器createStore有三个参数,第三个参数叫作enhancer,翻译过来就是加强器。咱们先将enhancer按下不表,而且告诉你其实Redux的另外一个APIapplyMiddleware就是一个enhancer。github

import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import { userReducer } from './user/reducer';
import { todoReducer } from './todo/reducer';

const reducers = combineReducers({
    userStore: userReducer,
    todoStore: todoReducer,
});

const enhancer = applyMiddleware(thunk, logger);
const store = createStore(reducers, null, enhancer);

export default store;
复制代码

只须要把全部中间件依次传入applyMiddleware,就生成了一个加强器,它们就能够发挥做用了。编程

若是preloadedState为空,enhancer能够做为第二个参数传入。看源代码:redux

if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState;
    preloadedState = undefined;
}

if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
        throw new Error('Expected the enhancer to be a function.');
    }
    return enhancer(createStore)(reducer, preloadedState);
}

if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.');
}
复制代码

服务器请求

一个组件免不了向服务器请求数据,然而开发者不但愿组件内部有过多的逻辑,请求应该封装成函数给组件调用,同时组件须要实时获取请求的状态以便展现不一样的界面。最好的办法就是将请求也归入Redux的管理中。api

import api from './api';

export const fetchMovieAction = () => {
    dispatch({ type: 'FETCH_MOVIE_START' });
    api.fetchMovie().then(res => {
        dispatch({ type: 'FETCH_MOVIE_END', payload: { movies: res.data } });
    }).catch(err => {
        dispatch({ type: 'FETCH_MOVIE_ERROR', error: true, payload: { msg: err } });
    });
};
复制代码
import React, { Component } from 'react';
import { connect } from 'react-redux';

import { fetchMovieAction } from './actions';
import Card from './Card';

class App extends Component {
    render() {
        const { movies } = this.props;
        return (
            <div className="movie"> {movies.map(movie => <Card key={movie.id} {...movies} />)} </div> ); } componentDidMount() { this.props.fetchMovie(); } } const mapState = (state) => { return { movies: state.payload.movies, }; }; const mapDispatch = (dispatch) => { return { fetchMovie: () => dispatch(fetchMovieAction()), }; }; export default connect(mapState, mapDispatch)(App); 复制代码

大功告成了。数组

只须要将请求封装成一个函数,而后假装成Action被发射出去,请求调用先后,真正的Action会被发射,在Store中存储请求的状态,而且可以被组件订阅到。

异步Action

你是否是发现了什么?对咯,这里的Action不是一个纯对象。

由于请求必定是一个函数,为了让请求入会,只能反过头来修改大会章程。可是大会章程岂能随便推翻,这时意见领袖出来讲话了:

当初规定Action必须是一个纯对象不是为了搞我的崇拜,而是出于实际须要。由于reducer必须是一个纯函数,这决定了dispatch的参数Action必须是一个带type字段的纯对象。现现在咱们要拉异步请求入会,而中间件又能够中途拦截作一些处理,那Action为何不能是一个函数呢?Action必须是一个纯对象这种说法是完彻底全的教条主义!

你们还动脑筋想出了一个异步Action的名头,这下函数类型的Action终于名正言顺了。

闭包

你是否是还发现了什么?对咯,请求函数中的dispatch哪去了。

不知道,可能会报错吧(无辜脸)。

其实咱们还有一件事没干:把dispatch方法偷渡到请求函数中。

export const fetchMovieAction = () => {
    return (dispatch) => {
        dispatch({ type: 'FETCH_MOVIE_START' });
        api.fetchMovie().then(res => {
            dispatch({ type: 'FETCH_MOVIE_END', payload: { movies: res.data } });
        }).catch(err => {
            dispatch({ type: 'FETCH_MOVIE_ERROR', error: true, payload: { msg: err } });
        });
    };
};
复制代码

很简单哪,加一个闭包,dispatch从返回函数的参数中偷渡进来。

脑洞

咱们要的不就是一个dispatch方法么,我能不能这样:

export const fetchMovie = (dispatch) => {
    dispatch({ type: 'FETCH_MOVIE_START' });
    api.fetchMovie().then(res => {
        dispatch({ type: 'FETCH_MOVIE_END', payload: { movies: res.data } });
    }).catch(err => {
        dispatch({ type: 'FETCH_MOVIE_ERROR', error: true, payload: { msg: err } });
    });
};
复制代码
const mapDispatch = (dispatch) => {
    return {
        fetchMovie: () => fetchMovie(dispatch),
    };
};
复制代码

貌似是能行得通的,只不过这时候请求函数已经不能叫Action了。考虑到以前请求函数假装成Action浑水摸鱼,还要插入中间件来帮助特殊处理,咱们这样作也不过度是吧。

好处就是再也不须要可以处理异步Action的中间件了。

坏处就是这不符合规范,是个人脑洞,闯了祸不要打我(蔑视)。

redux-thunk

前面屡次提处处理异步Action的中间件,究竟是何方神圣?

市面上流行的方案有不少种,咱们挑最简单的一种来讲一说(都不点赞怪我咯)。

redux-thunk算是Redux官方出品的异步请求中间件,可是它没有集成到Redux中,缘由仍是为了扩展性,社区能够提出各类方案,开发者各取所需。

让咱们来探讨一下redux-thunk的思路:原来Action只有一种,就是纯对象,如今Action有两种,纯对象和异步请求函数。只不过多了一种状况,不算棘手嘛。若是Action是一个对象,不为难它直接放走;若是Action是一个函数,就地执行,调用异步请求先后,真正的Action天然会释放出来,又回到第一步,放它走。

这是redux-thunk简化后的代码,其实源代码也跟这差很少。是否是很恐慌?

const thunk = ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
        return action(dispatch, getState);
    }
    return next(action);
};
复制代码

上面的函数就部署在个人我的博客中用来处理异步请求,彻底没有问题。既然它这么简单,并且能够预计它万年不会变,那我为何要凭空多一个依赖包。就将它放在个人眼皮底下不是挺好的嘛。

不过,它是一个研究中间件很好的范本。

咱们先将thunk先生降级成普通函数的写法:

const thunk = function({ dispatch, getState }) {
    return function(next) {
        return function(action) {
            if (typeof action === 'function') {
                return action(dispatch, getState);
            }
            return next(action);
        }
    }
};
复制代码

compose

我知道compose是Redux的五大护法之一,可为何挑在这个时候讲它呢?

先不告诉你。

compose在函数式编程中的含义是组合。假如你有一堆函数要依次执行,并且上一个函数的返回结果是下一个函数的参数,咱们怎样写看起来最装逼?

const result = a(b(c(d(e('redux')))));
复制代码

这种写法让人一眼就看穿了调用细节,装逼明显是不够的。

咱们来看Redux是怎么实现compose的:

export default function compose(...funcs) {
    if (funcs.length === 0) {
        return arg => arg;
    }
    if (funcs.length === 1) {
        return funcs[0];
    }
    return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
复制代码

诶,我看见reduce了,而后...就没有而后了。

假设咱们如今有三个函数:

const funcA = arg => console.log('funcA', arg);
const funcB = arg => console.log('funcB', arg);
const funcC = arg => console.log('funcC', arg);
复制代码

执行reduce的第一步返回的accumulator(accumulator是reduce中的概念),结果显而易见:

(...args) => funcA(funcB(...args));
复制代码

执行reduce的第二步返回的accumulator,注意到,这时reduce已经执行完了,返回的是一个函数。

(...args) => funcA(funcB(funcC(...args)));
复制代码

特别提醒:执行compose最终返回的是一个函数。也就是说开发者得这么干compose(a, b, c)()才能让传入的函数依次执行。

另外须要注意的是:传入的函数是从右到左依次执行的。

applyMiddleware

废话少说,先上源代码:

export default function applyMiddleware(...middlewares) {
    return createStore => (...args) => {
        const store = createStore(...args);
        let dispatch = () => {
            throw new Error(
                `Dispatching while constructing your middleware is not allowed. ` +
                `Other middleware would not be applied to this dispatch.`
            );
        };
        const middlewareAPI = {
            getState: store.getState,
            dispatch: (...args) => dispatch(...args),
        };
        const chain = middlewares.map(middleware => middleware(middlewareAPI));
        dispatch = compose(...chain)(store.dispatch);
        return { ...store, dispatch };
    }
}
复制代码

还记得中间件闭包好几层的写法吗?如今咱们就来一层一层的剥开它。

middlewareAPI是一个对象,正好是传给第一层中间件函数的参数。执行它,返回的chain是由第二层函数组成的中间件数组。贴一下redux-thunk第二层转正后的样子:

function(next) {
    return function(action) {
        if (typeof action === 'function') {
            return action(dispatch, getState);
        }
        return next(action);
    }
}
复制代码

中间件第二层函数接收一个next参数,那这个next具体指什么呢?我先透露一下,next是整个Redux中间件机制的题眼,理解了next就能够对Redux中间件的理解达到大彻大悟的化境。

以前咱们已经拆解了compose的内部机制,从右到左执行,最右边的中间件的参数就是store.dispatch,它返回的值就是倒数第二个中间件的next。它返回什么呢?咱们再剥一层:

function(action) {
    if (typeof action === 'function') {
        return action(dispatch, getState);
    }
    return next(action);
}
复制代码

别看redux-thunk麻雀虽小,你们发现没有,第三层函数才是它的逻辑,前面两层都是配合redux的演出。也就是说呀同窗们,除了最后一个中间件的next是原始的dispatch以外,倒数往前的中间件传入的next都是上一个中间件的逻辑函数。

Redux中间件本质上是将dispatch套上一层本身的逻辑。

最终applyMiddleware里获得的这个dispatch是通过无数中间件精心包装,植入了本身的逻辑的dispatch。而后用这个臃肿的dispatch覆盖原有的dispatch,将Store的API返回。

每个Action就是这样穿太重重的逻辑代码才能最后被发射成功。只不过处理异步请求的中间件再也不往下走,直到异步请求发生,真正的Action被发射出来,才会走到下一个中间件的逻辑。

构建dispatch过程当中禁止执行dispatch

middlewareAPI中的dispatch为何是一个抛出错误的函数?

咱们如今已经知道,applyMiddleware的目的只有一个:用全部中间件组装成一个超级dispatch,并将它覆盖原生的dispatch。可是若是超级dispatch还没组装完成,就被中间件调用了原生的dispatch,那这游戏别玩了。

因此Redux来了一手掉包。

middlewareAPI初始传入的dispatch是一个炸弹,中间件的开发者胆敢在头两层闭包函数的外层做用域调用dispatch,炸弹就会引爆。而一旦超级dispatch构建完成,这个超级dispatch就会替换掉炸弹。

怎么替换呢?

函数也是引用类型对吧,炸弹dispatch之因此用let定义,就是为了未来修改它的引用地址:

let dispatch = () => {
    throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
        `Other middleware would not be applied to this dispatch.`
    );
};
// ...
dispatch = compose(...chain)(store.dispatch);
复制代码

固然,这是对中间件开发者的约束,若是你只是一个中间件的使用者,这可有可无。

applyMiddleware的花式调用

咱们注意到,执行applyMiddleware返回的是一个函数,这个函数有惟一的参数createStore。

WTF?

applyMiddleware不是createStore的参数之一么:

const store = createStore(reducer, applyMiddleware(middleware1, middleware2, middleware3));
复制代码

怎么createStore也成了applyMiddleware的参数了?

贵圈真乱。

首先咱们明确一点,applyMiddleware是一个加强器,加强器是须要改造Store的API的,这样才能达到加强Store的目的。因此applyMiddleware必须传入createStore以生成初始的Store。

因此生成一个最终的Store其实能够这样写:

const enhancedCreateStore = applyMiddleware(middleware1, middleware2, middleware3)(createStore);
const store = enhancedCreateStore(reducer);
复制代码

那一般的那种写法,Redux内部是怎么处理的呢?

if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
        throw new Error('Expected the enhancer to be a function.')
    }
    return enhancer(createStore)(reducer, preloadedState)
}
复制代码

上面是createStore源代码中的一段。

若是enhancer存在而且是一个函数,那么直接传入createStore执行,再传入reducer和preloadedState执行(这时候再传入enhancer就没完没了了),而后直接返回。

喵,后面还有好多代码呢,怎么就返回了?

不,就这么任性。

这么看下来,如下写法才是正宗的Redux:

const store = applyMiddleware(middleware1, middleware2, middleware3)(createStore)(reducer);
复制代码

如下写法只是Redux为开发者准备的语法糖:

const store = createStore(reducer, applyMiddleware(middleware1, middleware2, middleware3));
复制代码

洋葱圈模型

想必你们都据说过中间件的洋葱圈模型,这个比喻很是形象,乍听上去,啊,好像明白了。可是你们真的对洋葱圈模型有一个具象化的理解吗?

假设如今有三个中间件:

const middleware1 = ({ dispatch, getState }) => next => action => {
    console.log('middleware1 start');
    next(action);
    console.log('middleware1 end');
}

const middleware2 = ({ dispatch, getState }) => next => action => {
    console.log('middleware2 start');
    next(action);
    console.log('middleware2 end');
}

const middleware3 = ({ dispatch, getState }) => next => action => {
    console.log('middleware3 start');
    next(action);
    console.log('middleware3 end');
}
复制代码

如今将它传入applyMiddleware:

function reducer(state = {}, action) {
    console.log('reducer return state');
    return state;
}

const middlewares = [middleware1, middleware2, middleware3];
const store = createStore(reducer, applyMiddleware(...middlewares));
复制代码

咱们看一下打印的结果:

middleware1 start
middleware2 start
middleware3 start
reducer return state
middleware3 end
middleware2 end
middleware1 end
复制代码

对结果感到惊讶吗?其实理解函数调用栈的同窗就能明白为何是这样的结果。reducer执行以前也就是dispatch真正执行以前的日志好理解,dispatch被一层一层包装,一层一层的深刻调用。可是dispatch执行完之后呢?这时候的执行权在调用栈最深的那一层逻辑那里,也就是最接近原始dispatch的逻辑函数那里,因此以后的执行顺序是从最深处往上调用。

总的看下来,一个Action的更新Store之旅就像穿过一个洋葱圈的旅行。一堆中间件簇拥着Action钻到洋葱的中心,Action执行本身的使命更新Store后就地圆寂,而后中间件带着它的遗志再从洋葱的中心钻出来。

回看compose

其实我解释上面的打印日志,还有一个关节没有打通。

记得applyMiddleware的源代码吗?内部调用了compose来执行chain。

咱们强调过,compose的函数类型参数的执行顺序是从右到左的,我相信你们在很多的地方都见到过这样的表述。可是你们想过没有,为何要从右到左执行?原生JavaScript除了实现reduce以外还有一个reduceRight,从左到右执行并无什么技术障碍,那么为何要让执行顺序这么别扭呢?

答案就在上面的打印日志里。

打印日志很好哇,根据传入的顺序执行。对,执行compose是从右到左,可是compose返回的终极dispatch是一层一层从外面包裹的呀,最后一个中间件也就是最左边的中间件的逻辑,包裹在最外面一层,天然它的日志最早被打印出来。

因此compose被设计成参数从右到左执行,不是有技术障碍,也不是Redux特立独行,而是其中原本就要经历一次反转,compose只有再反转一次才能将它扭转过来。

Redux专题一览

考古

实用

中间件

时间旅行

相关文章
相关标签/搜索