ChatGPT解决这个技术问题 Extra ChatGPT

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

根据文档,"Without middleware, Redux store only supports synchronous data flow"。我不明白为什么会这样。为什么容器组件不能调用异步 API,然后 dispatch 操作?

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

https://i.stack.imgur.com/GBI59.png

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 存储库中搜索了线索,发现过去要求 Action Creator 是纯函数。例如,here's a user trying to provide a better explanation for async data flow:

action creator 本身仍然是一个纯函数,但它返回的 thunk 函数不需要,它可以做我们的异步调用

Action creators are no longer required to be pure. 那么,过去肯定需要 thunk/promise 中间件,但现在似乎不再需要了?

从来没有要求动作创建者是纯函数。这是文档中的错误,而不是改变的决定。
@DanAbramov 对于可测试性,它可能是一个很好的实践。 Redux-saga 允许这样做:stackoverflow.com/a/34623840/82609

C
Community

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

这种方法没有任何问题。这在大型应用程序中很不方便,因为您将有不同的组件执行相同的操作,您可能想要消除一些操作的抖动,或者将一些本地状态(如自动递增 ID)保持在操作创建者附近等。所以从从维护的角度将动作创建者提取到单独的功能中。

您可以阅读 my answer to “How to dispatch a Redux action with a timeout” 以获得更详细的演练。

Redux Thunk 或 Redux Promise 之类的中间件只是为您提供了用于调度 thunk 或 Promise 的“语法糖”,但您不必使用它。

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

// 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 你可以这样写:

// 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(例如,作为同步参数) .

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

对于第一种方法,我们需要注意我们所调用的动作创建者类型:

// 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”),这些守护进程会在动作到来时采取行动,并在输出动作之前转换或执行请求。这将逻辑从动作创建者转移到 sagas 中。您可能想检查一下,然后选择最适合您的。

我在 Redux repo 中搜索了线索,发现过去要求 Action Creator 是纯函数。

这是不正确的。文档是这样说的,但是文档是错误的。从来没有要求动作创建者是纯函数。我们修复了文档以反映这一点。


也许简而言之 Dan 的想法是:中间件是集中式方法,这种方式可以让您的组件更简单、更通用,并在一个地方控制数据流。如果您维护大型应用程序,您会喜欢它=)
@asdfasdfads 我不明白为什么它不起作用。它的工作原理完全相同;将 alert 放在 dispatch() 操作之后。
您的第一个代码示例中的倒数第二行:loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch。为什么我需要传递调度?如果按照惯例只有一个全局存储,我为什么不直接引用它并在需要时执行 store.dispatch,例如在 loadData 中?
@SørenDebois 如果您的应用程序是客户端,那么只有这样才能工作。如果它在服务器上呈现,您将希望每个请求都有一个不同的 store 实例,因此您无法事先定义它。
只想指出,这个答案有 139 行,是由 14 行组成的 redux-thunk 源代码的 9.92 倍:github.com/gaearon/redux-thunk/blob/master/src/index.js
U
Unknown

你没有。

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

Dan Abramov 对 redux-thunk 的回答是正确的,但我将更多地谈论与 redux-saga 非常相似但更强大的内容。

命令式 VS 声明式

DOM:jQuery 是命令式的 / React 是声明式的

Monads:IO 是命令式的 / Free 是声明式的

Redux 效果:redux-thunk 是命令式的 / redux-saga 是声明式的

当你手上有一个 thunk 时,比如 IO monad 或 Promise,你不能轻易知道执行后它会做什么。测试 thunk 的唯一方法是执行它,并模拟调度程序(或者如果它与更多东西交互,则模拟整个外部世界......)。

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

从副作用的角度来看,模拟是您的代码不纯的标志,并且在函数式程序员的眼中,证明有问题。与其下载一个库来帮助我们检查冰山是否完好无损,我们应该绕着它航行。一个铁杆 TDD/Java 家伙曾经问我你是如何在 Clojure 中进行模拟的。答案是,我们通常不会。我们通常将其视为我们需要重构代码的标志。

Source

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

另请参阅此 article

在现代 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 })
    );
}

还原传奇

<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 的使用表示您只对获取最后一次点击的用户名的数据感兴趣(处理并发问题,以防用户在大量用户名上快速点击)。这种东西很难用thunk。如果您不希望这种行为,您可以使用 takeEvery

你让动作创作者保持纯洁。请注意,保留 actionCreators(在 sagas put 和组件 dispatch 中)仍然很有用,因为它可能会帮助您在将来添加操作验证(断言/流/打字稿)。

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

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

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

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

有些人可能会争辩说,生成器可以固有地使用局部变量将状态隐藏在 redux 存储之外,但是如果您开始通过启动计时器等方式开始在 thunk 内编排复杂的事情,那么无论如何您都会遇到同样的问题。还有一个 select 效果现在允许从您的 Redux 存储中获取一些状态。

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

https://i.stack.imgur.com/F9Jsh.png

解耦

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

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

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

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

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

关于如何构建 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 组件决定何时显示/隐藏通知。我只是赋予它请求通知的能力,并将复杂的规则留在传奇中。这种东西很难用 thunk 或 Promise 来实现。

https://i.stack.imgur.com/XQ8lx.png

我已经描述了 here 如何使用 saga 来完成

为什么叫佐贺?

saga 一词来自后端世界。我最初在 long discussion 中向 Yassine(Redux-saga 的作者)介绍了该术语。

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

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

也可以看看:

Yassine 关于 Redux-saga 历史的采访

Kella Byte:阐明 Saga 模式

Microsoft CQRS 之旅:Saga 上的传奇

Yassine的中等反应

备择方案

如果您不喜欢使用生成器的想法,但您对 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 with async/await

在 Redux Saga 中管理流程

从 actionsCreators 到 Sagas

使用 Redux-saga 实现的蛇游戏

2017 年建议

不要为了使用而过度使用 Redux-saga。仅可测试的 API 调用是不值得的。

对于大多数简单的情况,不要从您的项目中删除 thunk。

如果有意义,请不要犹豫在 yield put(someActionThunk) 中发送 thunk。

如果您害怕使用 Redux-saga(或 Redux-observable)但只需要解耦模式,请检查 redux-dispatch-subscribe:它允许侦听调度并在侦听器中触发新的调度。

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

每次我重新访问时,这都会变得更好。考虑把它变成一篇博文:)。
谢谢你的好文章。但是我不同意某些方面。 LOAD_USER 是如何命令的?对我来说,它不仅是声明性的——它还提供了可读性很好的代码。像例如。 “当我按下这个按钮时,我想添加项目”。我可以查看代码并准确了解发生了什么。如果它被称为“BUTTON_CLICK”的效果,我必须查一下。
不错的答案。现在还有另一种选择:github.com/blesh/redux-observable
@swelet 抱歉迟到了。当您调度 ADD_ITEM 时,这是必要的,因为您调度了一个旨在对您的商店产生影响的操作:您希望该操作做某事。声明式包含事件溯源的理念:您不会发送动作来触发应用程序的更改,而是发送过去的事件来描述应用程序中发生的事情。事件的分派应该足以考虑应用程序的状态已经改变。有一个对事件做出反应的 Redux 存储这一事实是一个可选的实现细节
我不喜欢这个答案,因为它分散了实际问题的注意力,以便推销某人自己的图书馆。这个答案提供了两个库的比较,这不是问题的意图。实际的问题是询问是否使用中间件,这是由接受的答案解释的。
a
acjay

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

在我们刚开始工作的新项目上工作时,我的想法非常相似。我是 vanilla Redux 优雅系统的忠实粉丝,它以一种不受 React 组件树内部结构影响的方式更新 store 和重新渲染组件。对我来说,使用那种优雅的 dispatch 机制来处理异步似乎很奇怪。

我最终采用了与您在我从我们的项目中提取出来的库中的方法非常相似的方法,我们称之为 react-redux-controller

由于以下几个原因,我最终没有采用您上面的确切方法:

按照您编写的方式,那些调度功能无权访问商店。你可以通过让你的 UI 组件传入调度函数所需的所有信息来解决这个问题。但我认为这会不必要地将这些 UI 组件与调度逻辑耦合。更有问题的是,调度函数没有明显的方法来访问异步延续中的更新状态。调度函数可以通过词法范围访问调度本身。一旦 connect 语句失控,这就会限制重构的选项——而且仅使用一种更新方法就显得相当笨拙。因此,如果您将它们分解为单独的模块,您需要一些系统来让您组合这些调度程序功能。

总之,您必须安装一些系统以允许将 dispatch 和 store 以及事件参数注入到您的调度函数中。我知道这种依赖注入的三种合理方法:

redux-thunk 通过将它们传递给您的 thunk 以一种功能性的方式执行此操作(通过圆顶定义,使它们根本不完全是 thunk)。我没有使用其他调度中间件方法,但我认为它们基本相同。

react-redux-controller 使用协程执行此操作。作为奖励,它还使您可以访问“选择器”,这是您可能已作为第一个参数传递给连接的函数,而不必直接使用原始的规范化存储。

您还可以通过各种可能的机制将它们注入到 this 上下文中,从而以面向对象的方式进行。

更新

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


X
XML

Abramov 的目标——以及每个人的理想目标——只是将复杂性(和异步调用)封装在最合适和可重用的地方。

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

减速机?没门。它们应该是没有副作用的纯函数。更新商店是一项严肃而复杂的业务。不要污染它。

哑视图组件?绝对不。他们有一个问题:演示和用户交互,应该尽可能简单。

容器组件?可能,但次优。这是有道理的,因为容器是我们封装一些与视图相关的复杂性并与存储交互的地方,但是:容器确实需要比哑组件更复杂,但它仍然是一个单一的责任:提供视图和状态之间的绑定/店铺。您的异步逻辑与此完全不同。通过将其放置在容器中,您会将异步逻辑锁定在单个上下文中,并与一个或多个视图/路由耦合。馊主意。理想情况下,它都是可重用的,并且与视图完全分离。 (像所有规则一样,如果您有状态绑定逻辑恰好可以跨多个上下文重用,或者如果您可以以某种方式将所有状态概括为集成 GraphQL 模式之类的东西,则可能会有例外。好吧,那可能是很酷。但是……大多数时候,绑定似乎最终都是非常具体的上下文/视图。)

容器确实需要比哑组件更复杂,但它仍然是一个单一的职责:提供视图和状态/存储之间的绑定。您的异步逻辑与此完全不同。

通过将其放置在容器中,您会将异步逻辑锁定在单个上下文中,并与一个或多个视图/路由耦合。馊主意。理想情况下,它都是可重用的,并且与视图完全分离。

(像所有规则一样,如果您有状态绑定逻辑恰好可以跨多个上下文重用,或者如果您可以以某种方式将所有状态概括为集成 GraphQL 模式之类的东西,则可能会有例外。好吧,那可能是很酷。但是……大多数时候,绑定似乎最终都是非常具体的上下文/视图。)

其他一些服务模块?坏主意:您需要注入对商店的访问权限,这是可维护性/可测试性的噩梦。最好使用 Redux 的颗粒并仅使用提供的 API/模型访问商店。

动作和解释它们的中间件?为什么不?!对于初学者来说,这是我们剩下的唯一主要选择。 :-) 从逻辑上讲,动作系统是解耦的执行逻辑,您可以在任何地方使用。它可以访问商店并可以发送更多操作。它有一个单一的职责,就是组织应用程序周围的控制和数据流,大多数异步都适合这一点。动作创作者呢?为什么不在那里做异步,而不是在动作本身和中间件中?首先也是最重要的一点,创建者不能像中间件那样访问存储。这意味着您无法调度新的或有操作,无法从存储中读取来编写异步等。因此,将复杂性放在必要的复杂性中,并保持其他一切简单。然后,创建者可以是简单的、相对纯的、易于测试的函数。

动作创作者呢?为什么不在那里做异步,而不是在动作本身和中间件中?首先也是最重要的一点,创建者不能像中间件那样访问存储。这意味着您无法调度新的或有操作,无法从存储中读取来编写异步等。因此,将复杂性放在必要的复杂性中,并保持其他一切简单。然后,创建者可以是简单的、相对纯的、易于测试的函数。

首先也是最重要的一点,创建者不能像中间件那样访问存储。这意味着您无法调度新的或有操作,无法从存储中读取来编写您的异步等。

因此,将复杂性保留在必要的复杂性中,并保持其他一切简单。然后,创建者可以是简单的、相对纯的、易于测试的函数。


容器组件 - 为什么不呢?由于组件在 React 中扮演的角色,容器可以充当服务类,并且它已经通过 DI(道具)获得了存储。通过将其放置在容器中,您会将异步逻辑锁定在单个上下文中,用于单个视图/路由 - 怎么会?一个组件可以有多个实例。它可以与表示分离,例如使用渲染道具。我想答案可能会从证明这一点的简短示例中受益更多。
这是一个很好的答案
M
Michelle Tilley

要回答开头提出的问题:

为什么容器组件不能调用异步 API,然后分派操作?

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

如果没有中间件,你当然仍然可以

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 是(或必须是)异步的。

如果您还想支持 promisesobservablessagascrazy customhighly declarative 动作创建者,那么 Redux 只需更改您传递给 dispatch 的内容(也就是您返回的内容)即可来自动作创作者)。无需处理 React 组件(或 connect 调用)。


您建议在操作完成时再分派另一个事件。当您需要在操作完成后显示 alert() 时,这将不起作用。 React 组件中的 Promise 确实有效。我目前推荐 Promises 方法。
A
Alireza

好的,我们先来看看中间件是怎么工作的,这很好回答了这个问题,这是 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
      });
    };
  };
}

看这部分,看看我们的 dispatch 是如何变成一个函数的。

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

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

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

Redux Thunk 中间件允许您编写返回函数而不是操作的操作创建器。 thunk 可用于延迟动作的分派,或仅在满足特定条件时分派。内部函数接收 store 方法 dispatch 和 getState 作为参数。

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

那么 thunk 到底是什么?这就是它在维基百科中的介绍:

在计算机编程中,thunk 是用于将附加计算注入另一个子程序的子程序。 Thunks 主要用于延迟计算直到需要它,或者在另一个子例程的开头或结尾插入操作。它们在编译器代码生成和模块化编程方面有多种其他应用。该术语起源于“思考”的诙谐衍生词。 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;

所以看看这个概念是多么简单,以及它如何帮助你管理你的异步操作......

没有它你也可以活下去,但请记住,在编程中总是有更好、更整洁和正确的方法来做事……

https://i.stack.imgur.com/0bYwv.png


第一次上SO,什么都没读。但只是喜欢凝视照片的帖子。惊人的,提示和提醒。
D
Daniel

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

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

异步动作创建者是一种需要一点时间才能准备好最终分派动作的创建者。

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

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

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

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

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

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

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

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

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

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

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

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


S
SM Chinna

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

例如:store.js

  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

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

 export default function updateFruit(props,fruit) {
    return (
       {
         type:"UPDATE_DETAIL",
         payload:fruit,
         props:props
       }
     )
  }

然后是 reducer.js

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

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

试试这个..正在工作


对于只想调用 API 端点以返回实体或实体列表的人来说,这是很严肃的事情。你建议,“就做这个……然后这个,然后这个,然后这个其他的东西,然后那个,然后这个其他的东西,然后继续,然后做......”。但是,伙计,这是 FRONTEND,我们只需要调用 BACKEND 即可为我们提供准备好在前端使用的数据。如果这是要走的路,那就是出了点问题,出了点问题,现在有人没有应用 KISS
嗨,对 API 调用使用 try 和 catch 块。一旦 API 给出响应,调用 Reducer 操作类型。
@zameb 你可能是对的,但是你的抱怨是关于 Redux 本身,以及它在试图降低复杂性时带来的所有偷听。
M
Mselmi Ali

要回答这个问题:

为什么容器组件不能调用异步 API,然后分派操作?

我会说至少有两个原因:

第一个原因是关注点分离,调用 api 并取回数据不是 action creator 的工作,您必须将两个参数传递给 action creator functionaction type 和 { 5}。

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

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

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

并将 Redux-Thunk midleware 的作业 dispache 的结果您的 api call 到相应的 action


F
Feras

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

并行运行请求

无需等待即可拉动未来的行动

非阻塞呼叫竞赛效果,示例先接

响应以启动流程对您的任务进行排序(第一次调用中的第一个)

构成

任务取消 动态分叉任务。

支持在 redux 中间件之外并发运行 Saga。

使用渠道

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


c
coder9833idls

Redux 不能返回一个函数而不是一个动作。这只是一个事实。这就是人们使用 Thunk 的原因。阅读这 14 行代码,了解它如何允许异步循环与一些添加的函数分层一起工作:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

https://github.com/reduxjs/redux-thunk


m
muhammad abubakar

我会说至少有两个原因:

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

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

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

function addTodo(text) { return { type: ADD_TODO, text } } Redux-Thunk 中间件的工作是将您的 api 调用结果发送到适当的操作。