关于javascript:为什么我们需要Redux中的异步流中间件?

Why do we need middleware for async flow in Redux?

根据文档,"没有中间件,Redux商店只支持同步数据流"。我不明白为什么会这样。为什么容器组件不能调用异步API,然后dispatch操作?

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

A field and a button

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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 (
           
                <input value={this.props.field}/>
                <button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button>
                {this.props.isWaiting && Waiting...}
           
        );
    }
}
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>;
    }
}

渲染导出的组件时,我可以单击该按钮并正确更新输入。

请注意connect调用中的update函数。它调度一个动作,告诉应用程序它正在更新,然后执行异步调用。调用完成后,将提供的值作为另一个操作的有效负载进行调度。

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

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

The action creator itself is still a pure function, but the thunk function it returns doesn't need to be, and it can do our async calls

动作创作者不再需要纯粹。因此,thunk / promise中间件在过去绝对是必需的,但似乎不再是这种情况了?


What is wrong with this approach? Why would I want to use Redux Thunk or Redux Promise, as the documentation suggests?

这种方法没有错。这在大型应用程序中不方便,因为您将有不同的组件执行相同的操作,您可能想要去除某些操作,或保持一些本地状态,如自动递增ID靠近动作创建者等等。所以它更容易从维护的角度来将动作创建者提取到单独的函数中。

您可以阅读我的答案"如何使用超时分派Redux操作",以获得更详细的演练。

像Redux Thunk或Redux Promise这样的中间件只是为你发送thunk或promises提供了"语法糖",但你不必使用它。

因此,没有任何中间件,您的动作创建者可能看起来像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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 Middleware你可以像这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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,它也可以使用mapDispatchToProps来绑定这样的动作创建器,语法很短等。组件不知道如何实现动作创建器,并且可以在不同的异步方法之间切换(Redux Thunk) ,Redux Promise,Redux Saga)无需更改组件。另一方面,使用前一种显式方法,您的组件确切地知道特定调用是异步的,并且需要通过某种约定传递dispatch(例如,作为同步参数)。

还要考虑一下这段代码将如何变化。假设我们想要第二个数据加载功能,并将它们组合在一个动作创建器中。

使用第一种方法,我们需要注意我们正在调用的动作创建者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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其他动作创建者的结果,甚至不认为这些是同步还是异步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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参数,而无需修改调用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 })
      );
  }
}

如果您需要将其更改为同步,您也可以在不更改任何调用代码的情况下执行此操作:

1
2
3
4
5
6
7
// 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"),它们在进行操作时执行操作,并在输出操作之前转换或执行请求。这将逻辑从动作创作者转变为传奇。您可能想要查看它,然后选择最适合您的选项。

I searched the Redux repo for clues, and found that Action Creators were required to be pure functions in the past.

这是不正确的。文档说这个,但文档错了。
行动创造者从未被要求成为纯粹的职能。
我们修改了文档来反映这一点。


你没有。

但是......你应该使用redux-saga :)

丹·阿布拉莫夫的回答是关于redux-thunk的,但我会更多地讨论一下相似但更强大的redux-saga。

命令式VS声明

  • DOM:jQuery是必需的/ React是声明性的
  • Monads:IO是势在必行/ Free是声明性的
  • Redux效果:redux-thunk是必需的/ redux-saga是声明性的
  • 如果你手上有一个thunk,比如IO monad或promise,你就不能轻易知道一旦你执行它会做什么。测试thunk的唯一方法是执行它,并模拟调度程序(如果它与更多东西交互,则模拟整个外部世界......)。

    如果您使用的是模拟,那么您就不会进行函数式编程。

    Seen through the lens of side-effects, mocks are a flag that your code is impure, and in the functional programmer's eye, proof that something is wrong. Instead of downloading a library to help us check the iceberg is intact, we should be sailing around it.
    A hardcore TDD/Java guy once asked me how you do mocking in Clojure. The answer is, we usually don't. We usually see it as a sign we need to refactor our code.

    Ok.

    资源

    传奇(因为它们在redux-saga中实现)是声明性的,就像Free monad或React组件一样,它们更容易测试而不需要任何模拟。

    另见本文:

    in modern FP, we shouldn’t write programs — we should write descriptions of programs, which we can then introspect, transform, and interpret at will.

    Ok.

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

    困惑:行动/事件/命令......

    关于CQRS / EventSourcing和Flux / Redux等后端概念可能与之相关的前端世界存在很多混淆,主要是因为在Flux中我们使用术语"动作",它有时代表命令性代码(LOAD_USER)和事件(USER_LOADED)。我相信像事件采购一样,你应该只发送事件。

    在实践中使用传奇

    想象一个带有用户个人资料链接的应用。使用这两个中间件处理这个问题的惯用方法是:

    redux-thunk

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     dispatch(actions.loadUserProfile(123)}>Robert

    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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
     dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert


    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 })
      }
    }

    这个传奇转换为:

    every time a username gets clicked, fetch the user profile and then dispatch an event with the loaded profile.

    Ok.

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

    takeLatest的使用允许表示您只对获取上次用户名的数据感兴趣(如果用户在很多用户名上非常快速地点击,则处理并发问题)。这种东西很难用thunk。如果您不想要此行为,则可以使用takeEvery

    你让动作创作者保持纯洁。请注意,保留actionCreators(在sagas put和components dispatch中)仍然很有用,因为它可能有助于您将来添加操作验证(assertions / flow / typescript)。

    由于效果是声明性的,因此您的代码变得更加可测试

    您不再需要触发类似actions.loadUser()的类似rpc的调用。您的UI只需要发送已经发生的事情。我们只发射事件(总是以过去时态!)而不再是动作。这意味着您可以创建解耦的"ducks"或Bounded Contexts,并且saga可以充当这些模块化组件之间的耦合点。

    这意味着您的视图更易于管理,因为他们不再需要在已发生的事件和作为效果的事件之间包含该转换层

    例如,想象一个无限滚动视图。 CONTAINER_SCROLLED可以导致NEXT_PAGE_LOADED,但是可滚动容器的责任是决定我们是否应该加载另一个页面?然后他必须知道更复杂的事情,比如最后一页是否成功加载,或者是否已经有一个试图加载的页面,或者是否还有剩余的项目要加载?我不这么认为:为了获得最大的可重用性,可滚动容器应该只描述它已滚动。页面的加载是该滚动的"业务效果"

    有些人可能会争辩说,生成器可以通过局部变量固有地隐藏在redux存储区之外的状态,但是如果你开始通过启动计时器等在thunks内部编排复杂的东西,那么无论如何都会遇到同样的问题。并且有一个select效果,现在允许从Redux商店获得一些状态。

    Sagas可以进行时间旅行,还可以实现当前正在进行的复杂流量记录和开发工具。以下是一些已经实现的简单异步流日志:

    saga flow logging

    解耦

    Sagas不仅取代了redux thunk。它们来自后端/分布式系统/事件源。

    这是一个非常普遍的误解,传奇只是在这里以更好的可测试性取代你的redux thunk。实际上这只是redux-saga的一个实现细节。使用声明效果优于thunks的可测试性,但是saga模式可以在命令式或声明性代码之上实现。

    首先,saga是一个允许协调长期运行事务(最终一致性)和跨不同有界上下文(域驱动设计术语)的事务的软件。

    为了简化前端世界,假设有widget1和widget2。当单击widget1上的某个按钮时,它应该对widget2有影响。而不是将两个小部件耦合在一起(即widget1调度以widget2为目标的动作),widget1仅调度其按钮被单击。然后saga侦听此按钮单击,然后通过分析widget2知道的新事件来更新widget2。

    这增加了简单应用程序不必要的间接级别,但使得复杂应用程序的扩展更加容易。您现在可以将widget1和widget2发布到不同的npm存储库,这样他们就不必了解彼此,也不必共享全局的操作注册表。这两个小部件现在是有限的上下文,可以单独生活。他们不需要彼此保持一致,也可以在其他应用程序中重复使用。传奇是两个小部件之间的耦合点,它们以有意义的方式为您的业务协调它们。

    关于如何构建Redux应用程序的一些很好的文章,您可以使用Redux-saga解耦原因:

  • http://jaysoo.ca/2016/02/28/organizing-redux-application/
  • http://marmelab.com/blog/2015/12/17/react-directory-structure.html
  • https://github.com/slorber/scalable-frontend-with-elm-or-redux
  • 具体用例:通知系统

    我希望我的组件能够触发应用内通知的显示。但我不希望我的组件高度耦合到具有自己的业务规则的通知系统(同时显示最多3个通知,通知排队,4秒显示时间等...)。

    我不希望我的JSX组件决定何时显示/隐藏通知。我只是让它能够请求通知,并将复杂的规则留在传奇中。这种东西很难用thunks或promises来实现。

    notifications

    我在这里描述了如何使用saga来完成这项工作

    为什么称它为传奇?

    传奇这个词来自后端世界。我最初在长时间的讨论中介绍了Yassine(Redux-saga的作者)。

    最初,这个术语是用一篇论文引入的,saga模式应该用于处理分布式事务中的最终一致性,但是它的用法已经被后端开发人员扩展到更广泛的定义,所以它现在也涵盖了"流程管理器"模式(不知何故,原始的传奇模式是流程管理器的一种特殊形式)。

    今天,"saga"一词令人困惑,因为它可以描述两种不同的东西。正如它在redux-saga中使用的那样,它没有描述处理分布式事务的方法,而是一种协调应用程序中的操作的方法。 redux-saga也可以被称为redux-process-manager

    也可以看看:

  • Yassine关于Redux-saga历史的采访
  • Kella Byte:Cagefing Saga模式
  • Microsoft CQRS之旅:Sagas上的Saga
  • Yassine的中等反应
  • 备择方案

    如果你不喜欢使用生成器的想法,但你对saga模式及其解耦属性感兴趣,你也可以使用名称epic来描述完全相同的模式的redux-observable实现相同的效果,但是RxJS。如果您已经熟悉Rx,那么您会感到宾至如归。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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有用的资源

  • Redux-saga vs Redux-thunk with async / await
  • 管理Redux Saga中的流程
  • 从actionsCreators到Sagas
  • 用Redux-saga实现的Snake游戏
  • 2017年建议

  • 不要仅仅为了使用而过度使用Redux-saga。可测试的API调用不值得。
  • 对于大多数简单情况,请勿从项目中删除thunk。
  • 如果有意义的话,不要犹豫在yield put(someActionThunk)中发送thunk。
  • 如果您害怕使用Redux-saga(或Redux-observable)但只需要解耦模式,请检查redux-dispatch-subscribe:它允许监听调度并在侦听器中触发新的调度。

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

    好。


    简短的回答:对我来说,异步问题似乎是一种完全合理的方法。有几个警告。

    在我刚刚开始工作的新项目时,我有一个非常相似的思路。我是香草Redux优雅系统的忠实粉丝,用于更新商店和重新渲染组件,使其远离React组件树的内核。我很难挂钩那个优雅的dispatch机制来处理异步。

    我最终采用了一种非常类似的方法来处理我在项目中所拥有的内容,这是我们的项目,我们称之为react-redux-controller。

    由于以下几个原因,我最终没有采用上述方法:

  • 您编写的方式,这些调度功能无法访问商店。您可以通过让UI组件传递调度功能所需的所有信息来解决这个问题。但我认为这会将这些UI组件与调度逻辑不必要地耦合在一起。更有问题的是,调度函数没有明显的方法来访问异步延续中的更新状态。
  • 调度函数可以通过词法范围访问dispatch。一旦connect语句失控,这就限制了重构的选项 - 而且只用那个update方法看起来非常笨重。因此,如果将它们分解为单独的模块,您需要一些系统来让您编写这些调度程序功能。
  • 总而言之,您必须安装一些系统以允许dispatch并将商店注入您的调度功能以及事件的参数。我知道这种依赖注入的三种合理方法:

    • redux-thunk通过将它们传递到你的thunk(通过圆顶定义使它们完全不是thunks)以功能性的方式完成它。我没有使用其他dispatch中间件方法,但我认为它们基本相同。
    • react-redux-controller用协程做到这一点。作为奖励,它还允许您访问"选择器",这些函数可能是作为connect的第一个参数传递的函数,而不是直接使用原始的规范化存储。
    • 您也可以通过各种可能的机制将它们注入this上下文,从而实现面向对象的方式。

    更新

    在我看来,这个难题的一部分是反应 - 减少的限制。 connect的第一个参数获取状态快照,但不是调度。第二个参数得到调度而不是状态。两个参数都没有得到关闭当前状态的thunk,因为能够在继续/回调时看到更新状态。


    阿布拉莫夫的目标 - 理想的每个人 - 只是将复杂性(和异步调用)封装在最合适的地方。

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

    • 减速?没门。它们应该是纯粹的功能,没有副作用。更新商店是严肃,复杂的业务。不要污染它。
    • 哑视图组件?绝对不会。他们有一个问题:演示和用户互动,应该尽可能简单。
    • 容器组件?可能,但次优。有意义的是,容器是我们封装一些与视图相关的复杂性并与商店交互的地方,但是:

      • 容器确实需要比哑组件更复杂,但它仍然是一个单一的责任:提供视图和状态/存储之间的绑定。你的异步逻辑是一个完全独立的问题。
      • 通过将其放在容器中,您可以将异步逻辑锁定到单个上下文中,用于单个视图/路径。馊主意。理想情况下,它都是可重复使用的,并且完全分离。
    • 其他一些服务模块?不好的想法:你需要注入商店的访问权限,这是一个可维护性/可测试性的噩梦。最好使用Redux并使用提供的API /模型访问商店。
    • 行动和解释它们的中间件?为什么不?!对于初学者来说,这是我们留下的唯一主要选择。 :-)更逻辑地说,动作系统是可以在任何地方使用的解耦执行逻辑。它可以访问商店并可以发送更多操作。它有一个单一的职责,即组织应用程序周围的控制和数据流,大多数异步适合于此。

      • 动作创作者怎么样?为什么不在那里做异步,而不是在动作本身和中间件?

        • 首先也是最重要的是,创建者无法访问商店,就像中间件一样。这意味着您无法发送新的临时操作,无法从商店读取以组成您的异步等。
        • 因此,在一个复杂的必需品的地方保持复杂性,并保持其他一切简单。然后,创建者可以是简单,相对纯粹的功能,易于测试。


    要回答开头提出的问题:

    Why can't the container component call the async API, and then dispatch the actions?

    请记住,这些文档适用于Redux,而不是Redux和React。连接到React组件的Redux存储可以完全按照您的说法执行,但是没有中间件的Plain Jane Redux存储不接受dispatch的参数,除了普通的ol'对象。

    没有中间件,你当然可以做到

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

    但这是一个类似的情况,其中异步是围绕Redux而不是由Redux处理。因此,中间件通过修改可以直接传递给dispatch的内容来允许异步。

    也就是说,我认为你的建议的精神是有效的。在Redux + React应用程序中,当然还有其他方法可以处理异步。

    使用中间件的一个好处是,您可以继续正常使用动作创建器,而无需担心它们是如何连接起来的。例如,使用redux-thunk,您编写的代码看起来很像

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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是(或需要)异步的。

    如果你还想支持promises,observables,sagas或疯狂的自定义和高度声明的动作创建者,那么Redux可以通过改变你传递给dispatch的东西(也就是你从动作创建者那里返回的东西)来做到这一点。不需要使用React组件(或connect调用)。


    好的,让我们开始看看中间件如何工作,这完全回答了问题,这是Redux中源代码applyMiddleWare的功能:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    function applyMiddleware() {
      for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
        middlewares[_key] = arguments[_key];
      }

      return function (createStore) {
        return function (reducer, preloadedState, enhancer) {
          var store = createStore(reducer, preloadedState, enhancer);
          var _dispatch = store.dispatch;
          var chain = [];

          var middlewareAPI = {
            getState: store.getState,
            dispatch: function dispatch(action) {
              return _dispatch(action);
            }
          };
          chain = middlewares.map(function (middleware) {
            return middleware(middlewareAPI);
          });
          _dispatch = compose.apply(undefined, chain)(store.dispatch);

          return _extends({}, store, {
            dispatch: _dispatch
          });
        };
      };
    }

    看看这个部分,看看我们的调度如何成为一个功能。

    1
    2
    3
    4
    5
      ...
      getState: store.getState,
      dispatch: function dispatch(action) {
      return _dispatch(action);
    }
    • Note that each middleware will be given the dispatch and getState functions as named arguments.

    好吧,这就是Redux-thunk作为Redux最常用的中间件之一的介绍:

    Redux Thunk middleware allows you to write action creators that return
    a function instead of an action. The thunk can be used to delay the
    dispatch of an action, or to dispatch only if a certain condition is
    met. The inner function receives the store methods dispatch and
    getState as parameters.

    所以如你所见,它将返回一个函数而不是一个动作,这意味着你可以随时随地调用它,因为它是一个函数......

    那是什么笨蛋?这就是它在维基百科中的介绍:

    In computer programming, a thunk is a subroutine used to inject an
    additional calculation into another subroutine. Thunks are primarily
    used to delay a calculation until it is needed, or to insert
    operations at the beginning or end of the other subroutine. They have
    a variety of other applications to compiler code generation and in
    modular programming.

    The term originated as a jocular derivative of"think".

    A thunk is a function that wraps an expression to delay its
    evaluation.

    1
    2
    3
    4
    5
    6
    7
    8
    //calculation of 1 + 2 is immediate
    //x === 3
    let x = 1 + 2;

    //calculation of 1 + 2 is delayed
    //foo can be called later to perform the calculation
    //foo is a thunk!
    let foo = () => 1 + 2;

    所以看看概念是多么容易,以及它如何帮助您管理异步操作......

    没有它你就可以生活,但是在编程中记住总是有更好,更整洁和正确的方法来做事......

    Apply middleware Redux


    使用Redux-saga是React-redux实现中最好的中间件。

    例如:
    store.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
      import createSagaMiddleware from 'redux-saga';
      import { createStore, applyMiddleware } from 'redux';
      import allReducer from '../reducer/allReducer';
      import rootSaga from '../saga';

      const sagaMiddleware = createSagaMiddleware();
      const store = createStore(
         allReducer,
         applyMiddleware(sagaMiddleware)
       )

       sagaMiddleware.run(rootSaga);

     export default store;

    然后是saga.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    import {takeLatest,delay} from 'redux-saga';
    import {call, put, take, select} from 'redux-saga/effects';
    import { push } from 'react-router-redux';
    import data from './data.json';

    export function* updateLesson(){
       try{
           yield put({type:'INITIAL_DATA',payload:data}) // initial data from json
           yield* takeLatest('UPDATE_DETAIL',updateDetail) // listen to your action.js
       }
       catch(e){
          console.log("error",e)
         }
      }

    export function* updateDetail(action) {
      try{
           //To write store update details
       }  
        catch(e){
           console.log("error",e)
        }
     }

    export default function* rootSaga(){
        yield [
            updateLesson()
           ]
        }

    然后是action.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
     export default function updateFruit(props,fruit) {
        return (
           {
             type:"UPDATE_DETAIL",
             payload:fruit,
             props:props
           }
         )
      }

    然后reducer.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import {combineReducers} from 'redux';

    const fetchInitialData = (state=[],action) => {
        switch(action.type){
          case"INITIAL_DATA":
              return ({type:action.type, payload:action.payload});
              break;
          }
         return state;
      }
     const updateDetailsData = (state=[],action) => {
        switch(action.type){
          case"INITIAL_DATA":
              return ({type:action.type, payload:action.payload});
              break;
          }
         return state;
      }
    const allReducers =combineReducers({
       data:fetchInitialData,
       updateDetailsData
     })
    export default allReducers;

    然后是main.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from './app/components/App.jsx';
    import {Provider} from 'react-redux';
    import store from './app/store';
    import createRoutes from './app/routes';

    const initialState = {};
    const store = configureStore(initialState, browserHistory);

    ReactDOM.render(
           <Provider store={store}>
              <App />  /*is your Component*/
           </Provider>,
    document.getElementById('app'));

    试试这个......工作正常


    回答这个问题:

    Why can't the container component call the async API, and then
    dispatch the actions?

    我会说至少有两个原因:

    第一个原因是关注点的分离,调用api并获取数据不是action creator的工作,你必须将两个参数传递给action creator functionaction type和a < X4>。

    第二个原因是因为redux store正在等待具有强制操作类型的普通对象,并且可选地等待payload(但是在这里你也必须传递有效负载)。

    动作创建者应该是如下的普通对象:

    1
    2
    3
    4
    5
    6
    function addTodo(text) {
      return {
        type: ADD_TODO,
        text
      }
    }

    Redux-Thunk midlewaredispache的工作是api call到适当的action的结果。


    有同步动作创建者,然后有异步动作创建者。

    同步动作创建器是指当我们调用它时,它会立即返回一个Action对象,其中所有相关数据都附加到该对象,并且它可以由我们的reducers处理。

    异步操作创建器是指在准备最终调度操作之前需要一点时间的创建器。

    根据定义,只要您有一个发出网络请求的动作创建者,它总是有资格成为异步动作创建者。

    如果你想在Redux应用程序中安装异步动作创建器,你必须安装一个叫做中间件的东西,它允许你处理那些异步动作创建者。

    您可以在错误消息中验证这一点,该消息告诉我们使用自定义中间件进行异步操作。

    那么什么是中间件?为什么我们需要它用于Redux中的异步流?

    在redux中间件(如redux-thunk)的上下文中,中间件可以帮助我们处理异步操作创建器,因为这是Redux无法开箱即用的东西。

    将中间件集成到Redux周期中,我们仍然会调用动作创建者,即将返回将被调度的动作,但现在当我们发送动作时,而不是将其直接发送给我们的所有减速器,我们将继续说一个动作将通过应用程序内的所有不同中间件发送。

    在单个Redux应用程序内部,我们可以拥有任意数量的中间件。在大多数情况下,在我们工作的项目中,我们将有一个或两个中间件连接到我们的Redux商店。

    中间件是一个普通的JavaScript函数,将在我们调度的每个操作中调用。在该函数内部,中间件有机会阻止将动作分派给任何reducers,它可以修改动作或者只是以任何方式搞乱你的动作,例如,我们可以创建一个控制台日志的中间件您发送的每一个动作都只是为了您的观赏乐趣。

    有大量的开源中间件可以作为依赖项安装到项目中。

    您不仅限于使用开源中间件或将它们作为依赖项安装。您可以编写自己的自定义中间件并在Redux商店中使用它。

    中间件(并得到你的答案)更受欢迎的用途之一是处理异步动作创建者,可能是最流行的中间件有redux-thunk,它是关于帮助你处理异步动作创建者。

    还有许多其他类型的中间件也可以帮助您处理异步操作创建器。