为何在Redux中咱们须要中间件来实现异步流?

根据文档, “没有中间件,Redux存储仅支持同步数据流” 。 我不明白为何会这样。 为何容器组件不能调用异步API,而后dispatch操做? html

例如,想象一个简单的UI:一个字段和一个按钮。 当用户按下按钮时,该字段将填充来自远程服务器的数据。 前端

字段和按钮

import * as React from 'react';
import * as Redux from 'redux';
import { Provider, connect } from 'react-redux';

const ActionTypes = {
    STARTED_UPDATING: 'STARTED_UPDATING',
    UPDATED: 'UPDATED'
};

class AsyncApi {
    static getFieldValue() {
        const promise = new Promise((resolve) => {
            setTimeout(() => {
                resolve(Math.floor(Math.random() * 100));
            }, 1000);
        });
        return promise;
    }
}

class App extends React.Component {
    render() {
        return (
            <div>
                <input value={this.props.field}/>
                <button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button>
                {this.props.isWaiting && <div>Waiting...</div>}
            </div>
        );
    }
}
App.propTypes = {
    dispatch: React.PropTypes.func,
    field: React.PropTypes.any,
    isWaiting: React.PropTypes.bool
};

const reducer = (state = { field: 'No data', isWaiting: false }, action) => {
    switch (action.type) {
        case ActionTypes.STARTED_UPDATING:
            return { ...state, isWaiting: true };
        case ActionTypes.UPDATED:
            return { ...state, isWaiting: false, field: action.payload };
        default:
            return state;
    }
};
const store = Redux.createStore(reducer);
const ConnectedApp = connect(
    (state) => {
        return { ...state };
    },
    (dispatch) => {
        return {
            update: () => {
                dispatch({
                    type: ActionTypes.STARTED_UPDATING
                });
                AsyncApi.getFieldValue()
                    .then(result => dispatch({
                        type: ActionTypes.UPDATED,
                        payload: result
                    }));
            }
        };
    })(App);
export default class extends React.Component {
    render() {
        return <Provider store={store}><ConnectedApp/></Provider>;
    }
}

渲染导出的组件后,我能够单击按钮,而且输入已正确更新。 react

注意connect调用中的update功能。 它调度一个动做,告诉应用程序正在更新,而后执行异步调用。 调用完成后,将提供的值做为另外一个操做的有效负载进行分派。 git

这种方法有什么问题? 如文档所示,我为何要使用Redux Thunk或Redux Promise? 程序员

编辑:我在Redux仓库中搜索了线索,发现过去须要Action Creators是纯函数。 例如, 如下用户试图为异步数据流提供更好的解释: es6

动做建立者自己仍然是一个纯函数,可是它返回的thunk函数并不须要,它能够执行异步调用 github

动做建立者再也不须要是纯粹的。 所以,过去确定须要使用thunk / promise中间件,但彷佛再也不是这种状况了? ajax


#1楼

简短的回答 :对我来讲,这彷佛是一种彻底合理的解决异步问题的方法。 有几个警告。 npm

在刚开始工做的新项目中,我有很是类似的思路。 我是Vanilla Redux优雅的系统的忠实粉丝,该系统用于更新商店和从新渲染组件,而这种方式不影响React组件树。 我彷佛迷上了那种优雅的dispatch机制来处理异步问题。 编程

最后,我采用了一种很是相似的方法,该方法与我从项目中剔除出来的库中的库相似,咱们称之为react-redux-controller

因为如下几个缘由,我最终没有采用您所拥有的确切方法:

  1. 按照您编写的方式,这些分派功能没法访问商店。 您能够经过让UI组件传递分派功能所需的全部信息来解决该问题。 可是我认为这没必要要地将这些UI组件耦合到调度逻辑。 更成问题的是,没有明显的方法可使分派函数以异步连续的方式访问更新后的状态。
  2. 调度功能能够经过词法范围进行dispatch 。 一旦connect语句失控,这将限制重构的选项-仅使用一种update方法就显得很是笨拙。 所以,若是须要将这些调度程序功能分解为单独的模块,则须要一些系统来组成这些调度程序功能。

总而言之,您必须装配一些系统以容许dispatch以及将商店以及事件的参数注入到您的调度功能中。 我知道这种依赖注入的三种合理方法:

  • redux-thunk经过将它们传递到您的thunk中(经过圆顶定义使其彻底不彻底是thunk)以一种功能性的方式实现了此目的。 我没有使用其余dispatch中间件方法,可是我认为它们基本相同。
  • react-redux-controller用协程执行此操做。 另外,它还使您能够访问“选择器”,这些选择器是您做为connect的第一个参数传递的功能,而没必要直接与原始的规范化商店一块儿使用。
  • 您还能够经过多种可能的机制将它们注入this上下文中,从而以面向对象的方式进行操做。

更新资料

在我看来,这个难题的一部分是react-redux的局限性。 connect的第一个参数获取状态快照,但不发送。 第二个参数获取调度,但不获取状态。 因为可以在继续/回调时看到更新的状态,所以两个参数都不会关闭当前状态。


#2楼

要回答开始时提出的问题:

为何容器组件不能调用异步API,而后分派操做?

请记住,这些文档适用于Redux,而不适用于Redux plus React。 链接到React组件的 Redux存储能够彻底按照您所说的进行操做,可是没有中间件的Plain Jane Redux存储不接受除普通ol'对象以外的用于dispatch参数。

没有中间件,您固然仍然能够作

const store = createStore(reducer);
MyAPI.doThing().then(resp => store.dispatch(...));

但这是相似的状况,其中异步被围绕在 Redux上,而不是 Redux处理。 所以,中间件经过修改能够直接传递给dispatch内容来实现异步。


也就是说,我认为您的建议的精神是正确的。 固然,您还可使用其余方法在Redux + React应用程序中处理异步。

使用中间件的好处之一是,您能够继续正常使用动做建立者,而没必要担忧它们是如何链接的。 例如,使用redux-thunk ,您编写的代码看起来很像

function updateThing() {
  return dispatch => {
    dispatch({
      type: ActionTypes.STARTED_UPDATING
    });
    AsyncApi.getFieldValue()
      .then(result => dispatch({
        type: ActionTypes.UPDATED,
        payload: result
      }));
  }
}

const ConnectedApp = connect(
  (state) => { ...state },
  { update: updateThing }
)(App);

看起来与原始版本没有什么不一样-只是改了一点-而且connect不知道updateThing是(或须要是)异步的。

若是你也想支持的承诺观测传奇 ,或疯狂的定制高度声明动做的创造者,那么终极版能够只经过改变你传递什么作dispatch (又名,你的行动创造者返回的内容)。 无需对React组件(或connect调用)进行处理。


#3楼

这种方法有什么问题? 如文档所示,我为何要使用Redux Thunk或Redux Promise?

这种方法没有错。 这在大型应用程序中很不方便,由于您将有不一样的组件执行相同的操做,您可能但愿对某些操做进行反跳操做,或者将某些本地状态(例如,自动递增ID)保持在操做建立者附近,等等。从维护角度将动做建立者提取到单独的功能中。

您能够阅读我对“如何在超时时分派Redux操做”的回答,以获取更详细的演练。

中间件像终极版咚或终极版无极只是给你“语法糖”派遣的thunk或承诺,但你没必要使用它。

所以,没有任何中间件,您的动做建立者可能看起来像

// action creator
function loadData(dispatch, userId) { // needs to dispatch, so it is first argument
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch
}

可是,使用Thunk中间件,您能够这样编写:

// action creator
function loadData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`) // Redux Thunk handles these
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  this.props.dispatch(loadData(this.props.userId)); // dispatch like you usually do
}

因此没有太大的区别。 我喜欢后一种方法的一件事是,该组件不在意动做建立者是异步的。 它只是正常地调用dispatch ,也可使用mapDispatchToPropsmapDispatchToProps法绑定此类操做建立者,等等。这些组件不知道操做建立者的实现方式,所以您能够在不一样的异步方法之间进行切换(Redux Thunk,Redux Promise, Redux Saga)而无需更改组件。 另外一方面,使用前一种显式方法,您的组件能够确切地知道特定的调用是异步的,而且须要经过某种约定来传递dispatch (例如,做为sync参数)。

还考虑一下此代码将如何更改。 假设咱们要具备第二个数据加载功能,并将它们组合在一个动做建立器中。

使用第一种方法时,咱们须要注意咱们正在调用哪一种动做建立者:

// action creators
function loadSomeData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(dispatch, userId) {
  return Promise.all(
    loadSomeData(dispatch, userId), // pass dispatch first: it's async
    loadOtherData(dispatch, userId) // pass dispatch first: it's async
  );
}


// component
componentWillMount() {
  loadAllData(this.props.dispatch, this.props.userId); // pass dispatch first
}

使用Redux Thunk动做建立者能够dispatch其余动做建立者的结果,甚至不考虑它们是同步的仍是异步的:

// action creators
function loadSomeData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(userId) {
  return dispatch => Promise.all(
    dispatch(loadSomeData(userId)), // just dispatch normally!
    dispatch(loadOtherData(userId)) // just dispatch normally!
  );
}


// component
componentWillMount() {
  this.props.dispatch(loadAllData(this.props.userId)); // just dispatch normally!
}

使用这种方法,若是之后您但愿动做建立者查看当前的Redux状态,则可使用传递给thunk的第二个getState参数,而无需彻底修改调用代码:

function loadSomeData(userId) {
  // Thanks to Redux Thunk I can use getState() here without changing callers
  return (dispatch, getState) => {
    if (getState().data[userId].isLoaded) {
      return Promise.resolve();
    }

    fetch(`http://data.com/${userId}`)
      .then(res => res.json())
      .then(
        data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
        err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
      );
  }
}

若是须要将其更改成同步,则也能够在不更改任何调用代码的状况下执行此操做:

// I can change it to be a regular action creator without touching callers
function loadSomeData(userId) {
  return {
    type: 'LOAD_SOME_DATA_SUCCESS',
    data: localStorage.getItem('my-data')
  }
}

所以,使用像Redux Thunk或Redux Promise这样的中间件的好处是组件不知道动做建立者的实现方式,它们是否关心Redux状态,它们是同步仍是异步以及是否调用其余动做建立者。 缺点是间接的,但咱们认为在实际应用中值得这么作。

最后,Redux Thunk和朋友只是Redux应用程序中异步请求的一种可能方法。 另外一个有趣的方法是Redux Saga ,它容许您定义长时间运行的守护程序(“ sagas”),这些守护程序会在操做到来时执行操做,并在输出操做以前转换或执行请求。 这将逻辑从动做建立者转移到了传奇。 您可能想要检查一下,而后选择最适合您的东西。

我在Redux存储库中搜索了线索,发现过去过去要求Action Creators是纯函数。

这是不正确的。 文档说了这一点,可是文档是错误的。
从未要求动做建立者是纯粹的功能。
咱们修复了文档以反映这一点。


#4楼

你不知道

可是...您应该使用redux-saga :)

丹·阿布拉莫夫(Dan Abramov)的答案是关于redux-thunk可是我将进一步讨论redux-saga ,它很是类似,但功能更强大。

命令式VS声明式

  • DOM :jQuery是命令性的/ React是声明性的
  • Monads :IO势在必行/ Free是声明式
  • Redux效果redux-thunk是命令性的/ redux-saga是声明性的

当您手头有重击时,例如IO monad或诺言,执行后就不会轻易知道它会作什么。 测试一个thunk的惟一方法是执行它,并模拟调度程序(若是它与更多的东西交互,则模拟整个外部世界……)。

若是您正在使用模拟,那么您就不在进行函数式编程。

从反作用的角度来看,模拟标志着您的代码是不纯的,而且在功能程序员的眼中,它证实了某些错误。 与其下载图书馆来帮助咱们检查冰山是否无缺,不如咱们绕着它航行。 一个TDD / Java顽固的家伙曾经问我如何在Clojure中进行模拟。 答案是,咱们一般不这样作。 咱们一般将其视为咱们须要重构代码的标志。

资源

sagas(由于它们在redux-saga )是声明性的,而且像Free monad或React组件同样,它们无需任何模拟就易于测试。

另请参阅本文

在现代FP中,咱们不该该编写程序,而应该编写程序的描述,而后能够对其进行自省,转换和解释。

(实际上,Redux-saga就像是一个混合体:流程是必须的,但效果是声明性的)

混乱:动做/事件/命令...

前端世界对如何将某些后端概念(如CQRS / EventSourcing和Flux / Redux)相关联感到困惑,主要是由于在Flux中,咱们使用术语“操做”来表示命令性代码( LOAD_USER )和事件( USER_LOADED )。 我相信,像事件来源同样,您应该只调度事件。

在实践中使用sagas

想象一个带有用户配置文件连接的应用程序。 使用两种中间件来处理此问题的惯用方式是:

redux-thunk

<div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>

function loadUserProfile(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'USER_PROFILE_LOADED', data }),
      err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err })
    );
}

redux-saga

<div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div>


function* loadUserProfileOnNameClick() {
  yield* takeLatest("USER_NAME_CLICKED", fetchUser);
}

function* fetchUser(action) {
  try {
    const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)
    yield put({ type: 'USER_PROFILE_LOADED', userProfile })
  } 
  catch(err) {
    yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })
  }
}

这个传奇转化为:

每次单击用户名时,获取用户配置文件,而后使用加载的配置文件调度事件。

如您所见, redux-saga有一些优势。

使用takeLatest能够表示您只对获取最后一次单击的用户名的数据感兴趣(若是用户在许多用户名上快速单击,则能够处理并发问题)。 这样的东西很难与暴徒。 若是您不但愿这种行为,可使用takeEvery

您可使动做创做者保持纯正。 请注意,保留actionCreators(在sagas put和component dispatch )仍然颇有用,由于它可能会帮助您未来添加动做验证(断言/流程/打字稿)。

因为效果是声明性的,所以您的代码变得更具可测试性

您再也不须要触发相似rpc的调用,例如actions.loadUser() 。 您的UI只需调度已发生的事件。 咱们仅触发事件 (始终以过去时!),再也不执行任何操做。 这意味着您能够建立解耦的“鸭子”有界上下文 ,而且传奇能够充当这些模块化组件之间的耦合点。

这意味着您的视图更易于管理,由于它们再也不须要在已发生的事情和应发生的事情之间包含转换层

例如,想象一个无限滚动视图。 CONTAINER_SCROLLED能够致使NEXT_PAGE_LOADED ,可是可滚动容器是否真的有责任决定咱们是否应该加载另外一页? 而后,他必须知道更复杂的内容,例如是否成功加载了最后一页,或者是否已有试图加载的页面,或者是否还有其余要加载的项目? 我不这么认为:为了得到最大的可重用性,可滚动容器应该只描述它已经被滚动。 页面的加载是该滚动的“业务影响”

有人可能会争辩说,生成器可使用本地变量将状态固有地隐藏在redux存储以外,可是若是您经过启动计时器等开始在thunk中编排复杂的东西,则不管如何都会遇到一样的问题。 如今有一个select效果,能够从Redux存储中获取一些状态。

Sagas能够进行时间旅行,还能够启用当前正在使用的复杂流记录和开发工具。 这是一些已经实现的简单异步流日志记录:

传奇流记录

去耦

Sagas不只在替换redux thunk。 它们来自后端/分布式系统/事件源。

一个广泛的误解是,sagas即将到来,以更好的可测试性取代您的redux thunk。 实际上,这只是redux-saga的实现细节。 使用声明性效果比使用thunk更好,可是可在命令性或声明性代码之上实现saga模式。

首先,传奇是一款软件,能够协调长期运行的事务(最终的一致性)以及跨不一样有界上下文的事务(域驱动的设计术语)。

为了简化前端世界的操做,假设有widget1和widget2。 单击widget1上的某个按钮时,它将对widget2产生影响。 与其将2个小部件耦合在一块儿(即,小部件1调度以小部件2为目标的动做),小部件1仅调度其按钮被单击。 而后,传奇侦听此按钮的单击,而后经过调度widget2知道的新事件来更新widget2。

这增长了简单应用程序没必要要的间接级别,但使扩展复杂应用程序更加容易。 如今,您能够将widget1和widget2发布到不一样的npm存储库,这样它们就没必要彼此了解,而没必要共享全局的动做注册表。 如今这2个小部件是能够单独使用的有界上下文。 他们不须要彼此保持一致,也能够在其余应用程序中重复使用。 传奇是两个小部件之间的耦合点,以对您的业务有意义的方式协调它们。

关于如何构建Redux应用的一些不错的文章,出于解耦的缘由,您能够在其中使用Redux-saga:

一个具体的用例:通知系统

我但愿个人组件可以触发应用内通知的显示。 可是我不但愿个人组件与具备本身的业务规则的通知系统高度耦合(最多同时显示3条通知,通知队列,4秒显示时间等)。

我不但愿个人JSX组件决定什么时候显示/隐藏通知。 我只是给它请求通知的功能,而将复杂的规则留在了传奇中。 这种东西很难经过大块头或诺言实现。

通知

我在这里描述了佐贺如何作到这一点

为何叫佐贺?

saga一词来自后端世界。 在最初的长时间讨论中 ,我最初将Yassine(Redux-saga的做者)介绍给该术语。

最初,该术语是在论文中引入的,该传奇模式原本应该用于处理分布式事务中的最终一致性,可是后端开发人员已将其用法扩展到更普遍的定义,所以如今它也涵盖了“流程管理器”模式(某种程度上,原始的传奇模式是流程管理器的一种特殊形式)。

今天,“传奇”一词使人困惑,由于它能够描述两种不一样的事物。 因为它在redux-saga中使用,所以它并不描述处理分布式事务的方法,而是协调应用程序中的操做的方法。 redux-saga也能够称为redux-process-manager

也能够看看:

备择方案

若是您不喜欢使用生成器的想法,可是对saga模式及其解耦属性感兴趣,则还可使用redux-observable实现相同的功能,它使用名称epic来描述彻底相同的模式,但使用RxJS。 若是您已经熟悉Rx,就会有宾至如归的感受。

const loadUserProfileOnNameClickEpic = action$ =>
  action$.ofType('USER_NAME_CLICKED')
    .switchMap(action =>
      Observable.ajax(`http://data.com/${action.payload.userId}`)
        .map(userProfile => ({
          type: 'USER_PROFILE_LOADED',
          userProfile
        }))
        .catch(err => Observable.of({
          type: 'USER_PROFILE_LOAD_FAILED',
          err
        }))
    );

一些redux-saga有用的资源

2017年建议

  • 不要仅仅为了使用它而过分使用Redux-saga。 仅可测试的API调用不值得。
  • 在大多数状况下,请勿从您的项目中删除垃圾。
  • 若是有意义的话,请不要犹豫,在yield put(someActionThunk)调度yield put(someActionThunk)

若是您对使用Redux-saga(或Redux-observable)感到恐惧,但只须要使用去耦模式,请检查redux-dispatch-subscribe :它容许侦听调度并在侦听器中触发新的调度。

const unsubscribe = store.addDispatchListener(action => {
  if (action.type === 'ping') {
    store.dispatch({ type: 'pong' });
  }
});

#5楼

Abramov的目标-也是每一个人的理想-只是在最合适的地方封装复杂性(和异步调用)

在标准Redux数据流中最好的位置是哪里? 怎么样:

  • 减速器 ? 没门。 它们应该是纯函数,没有反作用。 更新商店是严肃的,复杂的业务。 不要污染它。
  • 哑吧视图组件? 绝对不能。它们有一个问题:表示和用户交互,而且应尽量简单。
  • 容器组件? 可能,但次优。 有意义的是,容器是一个咱们封装一些与视图相关的复杂性并与商店交互的地方,可是:
    • 容器确实须要比哑组件更为复杂,可是这仍然是一个单一的责任:在视图与状态/存储之间提供绑定。 您的异步逻辑与此彻底无关。
    • 经过将其放在容器中,您能够将异步逻辑锁定在单个上下文中,用于单个视图/路由。 馊主意。 理想状况下,它们都是可重用的,而且彻底是分离的。
  • 还有其余服务模块吗? 坏主意:您须要注入对商店的访问权限,这是可维护性/可测试性的噩梦。 最好使用Redux,仅使用提供的API /模型访问商店。
  • 动做和解释它们的中间件? 为何不?! 对于初学者来讲,这是咱们剩下的惟一主要选择。 :-)从逻辑上讲,动做系统是分离的执行逻辑,能够在任何地方使用。 它能够访问商店,而且能够调度更多操做。 它的职责是组织应用程序周围的控制流和数据流,而且大多数异步处理都适合于此。
    • 动做创做者呢? 为何不在那里异步,而不是在动做自己和中间件中同步呢?
      • 首先也是最重要的一点是,建立者没有中间商能够访问的商店。 这意味着您不能调度新的或有操做,不能从存储中读取信息以组成异步,等等。
      • 所以,请将复杂性放在必不可少的地方,并将其余全部内容保持简单。 而后,建立者能够是易于测试的简单,相对纯净的功能。
相关文章
相关标签/搜索