Redux 入门教程(二): 中间件与异步操作

tips: 当前文章并非原创,详情请关注 阮一峰老师的个人网站

上一篇 redux 入门教程(一)
下一篇 Redux 入门教程(三):React-Redux 的用法

headerImage

但是,一个关键问题没有解决: 异步操作怎么办? Action发出后,Reducer 立即算出 State,这叫做同步;
Action 发出以后,过一段时间再执行 Reducer,这就是异步。

怎么才能 Reduc 在异步操作结束后自动执行呢? 这就要用到新的工具: 中间件 (middleware)。

midelewares

一、中间件的概念

为了理解中间件,让我们站在框架作者的角度思考问题: 如果要添加功能,你会在哪个环节添加?

(1)Reducer:纯函数,只承担计算 State 的功能,不适合承担其他功能,也承担不了,因为理论上,纯函数不能进行读写操作。
(2)View: 与 State 一一对应,可以看作 State 的视觉层,也不适合承担其他功能。
(3)Action: 存放数据的对象,即消息的载体,只能被别人操作,自己不能进行任何操作。

想来想去,只有发送 Action 的这个步骤,即 store.dispatch()方法,可以添加功能。举例来说,要添加日志功能,把 Action和 State打印出来,可以对 store.dispatch进行如下改造。

let next = store.dispatch;
store.dispatch =  function dispatchAndLog(action){
    console.log('dispatching',action);
    next(action);
    console.log('next state', store.getState());
}

上面代码中,对store.dispatch进行了重定义,在发送 Action 前后添加了打印功能。这就是中间件的雏形。
中间件就是一个函数,对 store.dispatch 方法进行了改造,在发出 Action 和执行 Reducer 这两步之间,添加了其他功能。

二、中间件的用法

本教程不涉及如何编写中间件,因为常用的中间件都有现成的,只要引用别人写好的模块即可。比如,上一节日志中间件,就有现成的 redux-logger模块。这里只介绍怎么使用中间件。

import {applyMiddleware,createStore} from 'redux';
import  createLogger from 'redux-logger';
const logger = createLogger();

const store = createStore(
    reducer,
    applyMiddleware(logger)
);

上面代码中, redux-logger提供了一个生产 createLogger,可以生成中间件 logger。然后,将它放在 applyMiddleware 方法之中,传入 createStore 方法,就完成了 store.dispatch()的功能增强。

这里有两点需要注意:

(1) createStore方法可以接受整个应用的初始状态作为参数,这样的话, applyMiddleware就是第三个参数了。

const store = createStore(
   reducer,
   initial_state,
   applyMiddleware(logger)
);

(2)中间件的次序有讲究。

const store = createStore(
    reducer,
    applyMiddleware(thunk,promise,logger)
);

上面代码中, applyMiddleware 方法的三个参数,就是三个中间件。有的中间件有次序要求,使用前要查一下文档。比如,logger就一定要放在最后,否则输出结果会不正确。

三、 applyMiddleware()

看到这里,你可能会问, applyMiddleware 这个方法到底是干什么的?

它是 Redux 的原生方法,作用是将所有中间件组成一个数组,依次执行。下面是它的源码。

export default function applyMiddleware(...midelewares){
    return (createStore) => (reducer,preloadedState,enhancer)=>{
        var store = createStore(reducer,preloadedState,enhancer);
        var dispatch = store.dispatch;
        var chain = [];
        
        var middlewareAPI = {
            getState: store.getState,
            dispatch: (action)=>dispatch(action)
        };
        chain = middlewares.map(middleware=>middleware(middlewareAPI));
        dispatch = compose(...chain)(store.dispatch);
        return {...store,dispatch}
    }
    
}

上面代码中,所有中间件被放进了一个数组 chain,然后嵌套执行 store.dispatch。可以看到,中间件内部(middlewareAPI)可以拿到 getStatedispatch这两个方法。

四、异步操作的基本思路

理解了中间件以后,就可以处理异步操作了。

同步操作只要发出一种 Action 即可,异步操作的差别是它要发出三种 Action。

操作发起时的 Action
操作成功时的 Action
操作失败时的 Action

以向服务器取出数据为例,三种 Action 可以有两种不同的写法。

//写法一: 名称相同,参数不同
{type: 'FETCH_POSTS'}
{type: 'FETCH_POSTS',status: 'error',error:'oops'}
{type: 'FETCH_POSTS',status: 'success',response:{...}}

//写法二: 名称不同
{type: 'FETCH_POSTS_REQUEST'}
{type: 'FETCH_POSTS_FAILURE',error:'oops'}
{type: 'FETCH_POSTS_SUCCESS',response:{...}}

除了 Action 种类不同,异步操作的 state 也要进行改造,反映不同的操作状态。下面是 State 的一个例子。

let state = {
    //...
    isFetching: true,
    didInvalide: true,
    lastUpdated: 'xxxxxx'
};

上面代码中,State 的属性 'isFetching' 表示是否在抓取数据。 didInvalidate表示数据是否过时, ‘lastUpdated’表示上一次更新时间。

现在,整个异步操作的思路就很清楚了。

操作开始时,送出一个 Action ,触发 State 更新为 “正在操作”状态,View重新渲染
操作结束后,再送出一个 Action,触发 State 更新为“操作结束”状态,View 再一次重新渲染

五、redux-thunk 中间件

异步操作至少要送出两个 Action: 用户触发第一个 Action,这个跟同步操作一样,没有问题;如何才能在操作结束时,系统自动送出第二个Action 呢?

奥妙就在 Action Creator 之中。

class AsyncApp extends Component {
    componentDidMount(){
        const {dispatch, selectPost} = this.props
        dispatch(fetchPosts(selectedPost))
    }
}

上面代码是一个异步组件的例子。加载成功后(componentDidMount方法),它送出了(dispatch方法)一个 Action,向服务器要求数据 fetchPosts(selectedSubreddit)。这里的 fetchPosts就是 Action Creator。

下面就是 'fetchPosts'的代码,关键之处就在里面。

Async Action
const fetchPosts = postTitle => (dispatch,getState)=>{
    dispatch(requestPosts(postTitle));
    return fetch(`/some/API/${postTitle}.json`)
    .then(response =>response.json())
    .then(json=>dispatch(receivePosts(postTitle.json)));
    };
};

//使用方法一
store.dispatch(fetchPosts('reactjs'));
//使用方法二
store.dispatch(fetchPosts('reactjs')).then(()=>
    console.log(store.getState())
);

上面代码中, fetchPosts 是一个 Action Creator (动作生成器),返回一个函数。这个函数执行后,先发出一个Action(requestPosts(postTitle)),然后进行异步操作。拿到结果后,先将结果转成 JSON 格式,然后再发出一个 Action(receivePosts(postTitle,json))。

上面代码中,有几个地方需要注意。

(1)`fetchPosts` 返回了一个函数,而普通的 Action Creator 默认返回一个对象。
(2)返回的函数的参数是 `dispatch`和 `getState` 这两个 Redux 方法,普通的 Action Creator 的参数是 Action 的内容。
(3)在返回的函数之中,先发出一个 Action(`requestPosts(postTitle)`),表示操作开始。
(4)异步操作结束之后,再发出一个 Action(`receivePosts(postTitle,json)`),表示操作结束。

这样的处理,就解决了自动发送第二个 Action 的问题。但是又带来了一个新问题, Action是由 store.dispatch方法发送的。而store.dispatch方法正常情况下,参数只能是对象,不能是函数。这时,就要使用中间件 redux-thunk

import {createStore,applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';

//Note: this API requires redux@>=3.1.0
const store = createStore(
    reducer,
    applyMiddleware(thunk)
); 

上面代码使用 redux-thunk中间件,改造store.dispatch,使得后者可以接受函数作为参数。

因此,异步操作的第一种解决方案就是,写出一个返回函数的 Action Creator,然后使用 redux-thunk 中间件改造 store.dispatch

六、redux-promise 中间件

既然 Action Creator 可以返回函数,当然也可以返回其他值。另一种异步操作的解决方案,就是让 Action Creator 返回一个promise对象。

这就需要使用 redux-promise中间件。

import { createStore,applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import reducer from './reducers';

const store = createStore(
    reducer,
    applyMiddleware(promiseMiddleware)
);

这个中间件使得 store.dispatch方法可以接受 Promise 对象作为参数。这时, Action Creator 有两种写法。
写法一,返回值是一个 Promise 对象。

const fetchPosts = 
    (dispatch,postTitle) => new Promise(function(resolve,reject){
        dispatch(requestPosts(postTitle));
        return fetch(`/some/API/${postTitle}.json`)
            .then(response=>{
                type: 'FETCH_POSTS',
                payload: response.json()
            });
    });
    
    

写法二, Action 对象的 payload 属性是一个 Promise 对象。这需要从 redux-actions 模块引入 createAction方法,并且写法也要变成下面这样。

import { createAction } from 'redux-actions';

class AsyncApp extends Component {
    componentDidMount() {
        const {dispatch,selectedPost } = this.props
        //发出同步Action
        dispatch(requestPosts(selectedPost));
        //发出异步 Action
        dispatch(createAction(
            'FETCH_POSTS',
            fetch(`/some/API/${postTitle}.json`)
                .then(response=>response.json)
        ));
    }
}

上面代码中,第二个 dispatch 方法发出的是异步 Action,只有等到操作结束,这个Action 才会实际发出。注意, createAction 的第二个参数必须是一个 Promise 对象。

看一下 redux-promise源码,你就明白它内部是怎么操作的。

export default function promiseMiddleware({dispatch}) {
    return next => action => {
        if(!isFSA(action)) {
            return isPromise(action)
            ? action.then(dispatch)
            : next(action);
        }
    return isPromise(action.payload)
        ? action.payload.then(
        result => dispatch({...action,payload:result}),
        error => {
            dispatch({...action,payload:error,error:true});
            return Promise.reject(error);
            }
        )
        : next(action); 
    };
}

从上面代码可以看出,如果 Action 本身是一个 Promise,它 resolve 以后的值应该是一个 Action 对象,会被dispatch方法送出(action.then(dispatch)),但 reject 以后不会有任何动作;如果 Action 对象的 payload 属性是一个 Promise 对象,那么无论 resolve 和 reject,dispatch方法都会发出 Action。

上一篇 redux 入门教程(一)
下一篇 Redux 入门教程(三):React-Redux 的用法

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容