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

例如,想象一个简单的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>;
    }
}

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

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

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

编辑:我在Redux repo中搜索线索,发现Action Creator在过去被要求是纯函数。例如,这里有一个用户试图为异步数据流提供更好的解释:

动作创建者本身仍然是一个纯函数,但它返回的thunk函数不需要是纯函数,它可以执行我们的异步调用

动作创作者不再需要纯粹。因此,thunk/promise中间件在过去肯定是必需的,但现在似乎不再是这样了?


当前回答

好了,让我们先来看看中间件是如何工作的,这很好地回答了这个问题,这是Redux中的源代码applyMiddleWare函数:

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

看看这一部分,看看我们的调度是如何成为一个函数的。

  ...
  getState: store.getState,
  dispatch: function dispatch(action) {
  return _dispatch(action);
}

请注意,每个中间件都将作为命名参数提供dispatch和getState函数。

好了,这就是Redux thunk作为Redux最常用的中间件之一介绍自己的方式:

Redux Thunk中间件允许您编写返回函数而不是动作。thunk可用于延迟动作的调度,或仅在某个条件为遇见。内部函数接收存储方法分派和getState作为参数。

正如你所看到的,它将返回一个函数而不是一个操作,这意味着你可以随时等待并调用它,因为它是一个函数。。。

那他妈的是什么?维基百科就是这样介绍的:

在计算机编程中,thunk是一种用于注入将附加计算转换为另一个子例程。雷电主要是用于将计算延迟到需要时,或插入在另一个子例程的开始或结束处的操作。他们有用于编译器代码生成和模块化编程。这个词起源于“思考”的一个滑稽派生词。thunk是一个包装表达式以延迟其评价

//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;

所以,看看这个概念有多简单,它如何帮助您管理异步操作。。。

这是你可以不用它生活的东西,但请记住,在编程中,总是有更好、更整洁和正确的方式来做事情。。。

其他回答

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

同步动作创建器是一个当我们调用它时,它会立即返回一个action对象,其中包含附加到该对象的所有相关数据,并准备好由我们的还原器处理。

异步动作创建器需要一点时间才能最终调度动作。

根据定义,每当您有一个动作创建者发出网络请求时,它总是符合异步动作创建者的资格。

如果您想在Redux应用程序中使用异步动作创建器,则必须安装一种叫做中间件的东西,该中间件将允许您处理这些异步动作创建。

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

那么什么是中间件,为什么我们需要它来实现Redux中的异步流呢?

在redux中间件(如redux thunk)的上下文中,中间件帮助我们处理异步动作创建者,因为这是redux无法开箱即用的。

随着中间件集成到Redux循环中,我们仍在调用动作创建者,这将返回一个将被调度的动作,但现在,当我们调度一个动作时,而不是直接将其发送给所有的reducer,我们将说一个动作将通过应用程序中的所有不同中间件发送。

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

中间件是一个普通的JavaScript函数,我们分派的每个动作都会调用它。在该功能内部,中间件有机会阻止某个动作被分派到任何一个reducer,它可以修改某个动作或以任何方式处理某个动作,例如,我们可以创建一个中间件,控制台记录您分派的每一个动作,只是为了您的观看乐趣。

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

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

中间件的一个更流行的用途(并得到您的答案)是处理异步动作创建者,可能最流行的中间件是redux thunk,它是帮助您处理异步动作创作者。

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

当在企业项目中工作时,中间件中有许多可用的需求,例如(saga)在简单的异步流中不可用,下面是一些:

并行运行请求无需等待,即可采取未来行动无阻塞呼叫比赛效果,示例先拾取响应以启动流程为您的任务排序(首次呼叫)合成任务取消动态分叉任务。支持redux中间件之外的并发运行Saga。使用频道

列表很长,只需查看saga文档中的高级部分

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

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

减速器?不可能。它们应该是没有副作用的纯函数。更新商店是一项严肃而复杂的业务。不要污染它。哑视图组件?肯定不会。他们有一个问题:演示和用户交互,并且应该尽可能简单。容器组件?可能,但次优。这是有意义的,因为容器是我们封装一些视图相关复杂性并与存储交互的地方,但是:容器确实需要比哑组件更复杂,但它仍然是一个单一的责任:在视图和状态/存储之间提供绑定。您的异步逻辑与此完全不同。通过将其放置在容器中,可以将异步逻辑锁定到一个上下文中,并与一个或多个视图/路由相耦合。坏主意。理想情况下,它都是可重用的,并且与视图完全分离。(与所有规则一样,如果您有状态绑定逻辑,恰好可以在多个上下文中重用,或者您可以以某种方式将所有状态概括为集成的GraphQL模式,则可能会出现异常。好吧,很好,这可能很酷。但……大多数时候,绑定似乎都是上下文/视图特定的。)其他服务模块?坏主意:您需要注入对存储的访问,这是一个可维护性/可测试性噩梦。最好使用Redux的颗粒,只使用提供的API/模型访问商店。动作和解释它们的中间件?为什么不呢?首先,这是我们剩下的唯一主要选择从逻辑上讲,操作系统是解耦的执行逻辑,您可以在任何地方使用它。它可以访问商店,并可以调度更多操作。它有一个单一的职责,即组织应用程序周围的控制和数据流,而大多数异步正好适合这一点。动作创造者呢?为什么不在那里做异步,而不是在操作本身和中间件中?首先,也是最重要的一点,创建者不能像中间件那样访问存储。这意味着你不能分派新的临时动作,不能从存储中读取以组成异步等等。所以,把复杂性放在一个必要性复杂的地方,把其他一切都保持简单。然后,创建者可以是简单的、相对简单的、易于测试的函数。

你没有。

但是你应该使用redux saga:)

丹·阿布拉莫夫的回答是正确的,关于redux thunk,但我将更多地谈论redux传奇,它非常相似,但更强大。

命令性VS声明性

DOM:jQuery是命令性的/React是声明性的Monads:IO是命令性的/Free是声明性的Redux效果:Redux thunk是命令性的/Redux saga是声明性的

当你手中有一个暴徒,比如一个IO monad或一个承诺,你很难知道一旦你执行了它会做什么。测试thunk的唯一方法是执行它,并模拟调度程序(如果它与更多的东西交互,则模拟整个外部世界…)。

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

从副作用的角度来看,模拟是一个标志,表明你的代码是不纯洁的,在功能程序员的眼里,它是错误的证据。我们应该绕过冰山,而不是下载一个图书馆来帮助我们检查冰山是否完好无损。一个铁杆TDD/Java的家伙曾经问我,你如何在Clojure中进行嘲笑。答案是,我们通常不会。我们通常将其视为需要重构代码的信号。

来源

saga(在redux saga中实现的)是声明性的,与Free monad或React组件一样,它们在没有任何模拟的情况下更容易测试。

另请参阅本文:

在现代FP中,我们不应该编写程序——我们应该编写程序的描述,然后我们可以随意反思、转换和解释。

(实际上,Redux传奇就像一个混合体:流程是强制性的,但效果是声明性的)

困惑:动作/事件/命令。。。

在前端世界中,对于一些后端概念(如CQRS/EventSourcing和Flux/Redux)可能是如何关联的,存在很多困惑,主要是因为在Flux中,我们使用了术语“action”,它有时可以同时表示命令式代码(LOAD_USER)和事件(USER_LOADED)。我认为,与事件源一样,您应该只发送事件。

在实践中使用西米

想象一个带有用户配置文件链接的应用程序。处理每个中间件的惯用方法是:

重复敲击声

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

冗余传奇

<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。

你让动作创作者保持纯洁。注意,保留actionCreator(在sagas put和组件分派中)仍然很有用,因为它可能会帮助您在将来添加动作验证(断言/流/类型脚本)。

由于效果是声明性的,所以代码变得更加可测试

你不需要再触发类似rpc的调用,比如actions.loadUser()。你的UI只需要调度已经发生的事情。我们只发射事件(总是用过去时!),不再发射动作。这意味着您可以创建解耦的“鸭子”或有边界的上下文,并且saga可以充当这些模块化组件之间的耦合点。

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

例如,想象一个无限的滚动视图。CONTAINER_SSCROLLED可以导致NEXT_PAGE_LOADED,但可滚动容器是否真的有责任决定我们是否应该加载另一个页面?然后,他必须意识到更复杂的事情,比如最后一个页面是否成功加载,是否已经有一个页面试图加载,或者是否没有更多的项目可以加载?我不这么认为:为了实现最大的可重用性,可滚动容器应该只描述它已经被滚动了。加载页面是该滚动条的“业务效果”

有些人可能会认为生成器可以用本地变量内在地隐藏redux存储之外的状态,但如果您开始通过启动计时器等在thunk内部协调复杂的事情,那么无论如何都会遇到同样的问题。还有一个选择效果,现在可以从你的Redux商店获得一些状态。

佐贺可以是耗时的,也可以实现目前正在开发的复杂的流日志记录和开发工具。下面是一些已经实现的简单异步流日志记录:

解耦

传奇不仅取代了重复的thunks。它们来自后端/分布式系统/事件源。

这是一个非常普遍的误解,即sagas只是为了用更好的可测试性来替换你的redux thunks。实际上,这只是redux saga的一个实现细节。对于可测试性,使用声明性效果比thunks更好,但saga模式可以在命令性或声明性代码之上实现。

首先,传奇是一个软件,它允许协调长期运行的事务(最终一致性),以及跨不同边界环境的事务(领域驱动的设计术语)。

为了简化前端世界,假设有小部件1和小部件2。当单击小部件1上的某个按钮时,它应该会对小部件2产生影响。与其将两个小部件耦合在一起(即,小部件1调度一个以小部件2为目标的操作),小部件2只调度其按钮被单击。然后,saga监听此按钮,然后通过发送小部件2知道的新事件来更新小部件2。

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

关于如何构建Redux应用程序的一些好文章,出于脱钩的原因,您可以在上面使用Redux saga:

http://jaysoo.ca/2016/02/28/organizing-redux-application/http://marmelab.com/blog/2015/12/17/react-directory-structure.htmlhttps://github.com/slorber/scalable-frontend-with-elm-or-redux

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

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

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

我已经在这里描述了如何用saga实现这一点

为什么叫佐贺?

传奇这个词来自后端世界。我最初在一次长时间的讨论中介绍了亚辛(《红色传奇》的作者)这个词。

最初,这一术语是在一篇论文中引入的,saga模式本应用于处理分布式事务中的最终一致性,但后端开发人员已将其用法扩展到更广泛的定义,因此它现在也涵盖了“流程管理器”模式(不知何故,最初的saga模式是流程管理器的一种特殊形式)。

今天,“传奇”一词令人困惑,因为它可以描述两种不同的事情。正如redux saga中使用的那样,它没有描述处理分布式事务的方法,而是描述在应用程序中协调操作的方法。redux saga也可以被称为redux过程管理器。

另请参见:

采访亚辛,讲述Redux传奇的历史凯拉·拜特:讲述传奇模式微软CQRS之旅:传奇故事亚辛的中等反应

选择

如果您不喜欢使用生成器的想法,但对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有用的资源

Redux saga vs Redux thunk与异步/等待Redux Saga的流程管理从动作创作者到传奇用Redux saga实现的Snake游戏

2017年建议

不要为了使用它而过度使用Redux传奇。只调用可测试的API是不值得的。对于大多数简单的情况,不要从项目中删除thunk。如果有意义的话,不要犹豫在yield put(someActionThunk)中发送thunk。

如果您害怕使用Redux saga(或Redux observable),但只需要解耦模式,请检查Redux分派订阅:它允许侦听分派并在侦听器中触发新分派。

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

回答问题:

为什么容器组件不能调用异步API,然后调度行动?

我认为至少有两个原因:

第一个原因是关注点分离,调用api并获取数据不是动作创建者的工作,您必须向动作创建者函数传递两个参数,即动作类型和有效载荷。

第二个原因是因为redux存储正在等待一个具有强制操作类型和可选有效负载的普通对象(但这里也必须传递有效负载)。

动作创建者应该是一个简单的对象,如下所示:

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

Redux Thunk middleware的任务是将api调用的结果显示给适当的操作。