乐闻世界logo
搜索文章和话题

Why do we need middleware for async flow in Redux?

7 个月前提问
3 个月前修改
浏览次数105

8个答案

1
2
3
4
5
6
7
8

Redux 本身是一个同步状态管理库,它专注于以可预测的方式管理和更新应用程序的状态。Redux 的核心概念是纯函数的 reducer 和同步的 action。当应用程序需要处理异步操作,如数据的 API 请求时,Redux 单独并不能有效地处理。

异步中间件,如 Redux Thunk 或 Redux Saga,使得在 Redux 应用程序中处理异步逻辑成为可能。下面是一些为什么需要异步中间件的原因:

1. 处理异步操作

Redux 的基本原则是 action 应该是一个具有 type 属性的对象,而且 reducer 应该是同步的纯函数。这种模式并不适用于执行异步操作,例如 API 调用。异步中间件允许我们在 dispatching action 之前执行异步代码,然后根据异步操作的结果来 dispatch 实际的 action。

例子: 假设我们有一个获取用户信息的异步操作。使用 Redux Thunk,我们可以创建一个 thunk action creator,它返回一个函数而非 action 对象。这个函数能够执行异步请求并且在请求完成后 dispatch 一个 action。

javascript
const fetchUserData = (userId) => { return (dispatch) => { dispatch({ type: 'FETCH_USER_REQUEST' }); fetch(`/api/user/${userId}`) .then((response) => response.json()) .then((user) => dispatch({ type: 'FETCH_USER_SUCCESS', payload: user })) .catch((error) => dispatch({ type: 'FETCH_USER_FAILURE', error })); }; };

2. 便于管理复杂的异步逻辑

在大型应用程序中,异步逻辑可能变得非常复杂,包括并发请求、条件请求、请求之间的竞争、错误处理等。异步中间件可以帮助管理这些复杂性,提供更清晰和更可维护的代码结构。

例子: 在使用 Redux Saga 的情况下,我们可以使用 ES6 的 generator 函数来更加直观和声明式地处理复杂的异步流。

javascript
function* fetchUserDataSaga(action) { try { yield put({ type: 'FETCH_USER_REQUEST' }); const user = yield call(fetchApi, `/api/user/${action.userId}`); yield put({ type: 'FETCH_USER_SUCCESS', payload: user }); } catch (error) { yield put({ type: 'FETCH_USER_FAILURE', error }); } }

3. 更好的测试性

异步中间件使得异步逻辑更加独立于组件,这有助于进行单元测试。我们可以在不进行实际的 API 调用的情况下,测试 action creators 和 reducers 的逻辑。

例子: 使用 Redux Thunk,我们可以测试 thunk action creator 是否正确地 dispatch 了相应的 actions。

javascript
// 使用 Jest 测试框架 it('creates FETCH_USER_SUCCESS when fetching user has been done', () => { // 模拟 dispatch 和 getState 函数 const dispatch = jest.fn(); const getState = jest.fn(); // 模拟 fetch API global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ id: 1, name: 'John Doe' }), }) ); // 执行 thunk action creator return fetchUserData(1)(dispatch, getState).then(() => { // 检查是否 dispatch 了正确的 action expect(dispatch).toHaveBeenCalledWith({ type: 'FETCH_USER_SUCCESS', payload: { id: 1, name: 'John Doe' } }); }); });

总结

Redux 需要异步中间件来处理异步操作,帮助维护复杂的异步逻辑,并提高代码的可测试性。这些中间件扩展了 Redux,使其能够以一种既有序又高效的方式处理异步数据流。Redux 作为一个状态管理库,其核心设计是围绕着同步的状态更新。也就是说,在没有任何中间件的情况下,当一个 action 被派发(dispatched)时,它会立即通过同步的 reducers 更新状态。然而,在实际的应用中,我们经常需要处理异步操作,比如从服务器获取数据,这些操作并不能立刻完成并返回数据。

因此,为了在 Redux 架构中处理这些异步操作,我们需要一种方式来扩展 Redux 的功能,使其能够处理异步逻辑。这就是异步中间件的用武之地。以下是几个为什么 Redux 需要异步数据流中间件的理由:

  1. 维护纯净的 reducer 函数: Reducer 函数应该是纯函数,这意味着给定相同的输入,总是返回相同的输出,并且不产生任何副作用。异步操作(如 API 调用)会产生副作用,因此不能直接在 reducer 中处理。

  2. 扩展 Redux 的功能: 异步中间件像是 Redux 生态系统中的插件,它允许开发者在不修改原始 Redux 库代码的情况下增加新的功能。例如,可以增加日志记录、错误报告或异步处理等功能。

  3. 异步控制流: 异步中间件允许开发者在派发 action 和到达 reducer 之间插入一个异步操作。这意味着可以先发出一个表示“开始异步操作”的 action,然后在操作完成时发出另一个表示“异步操作完成”的 action。

  4. 更干净的代码结构: 通过将异步逻辑封装在中间件内,我们可以保持组件和 reducer 的简洁。这避免了在组件中混合异步调用和状态管理逻辑,有助于代码分离和维护。

  5. 测试和调试的便捷性: 中间件提供了一个独立的层,可以在这个层中进行单独的测试和模拟异步行为,而不必担心组件逻辑或者 UI 层的细节。

例子

在实际应用中,最常见的异步中间件是 redux-thunkredux-saga

  • redux-thunk 允许 action 创建函数(action creators)返回一个函数而不是一个 action 对象。这个返回的函数接收 dispatchgetState 作为参数,让你可以进行异步操作,并在操作结束后派发一个新的 action。
javascript
const fetchUserData = (userId) => { return (dispatch, getState) => { dispatch({ type: 'USER_FETCH_REQUESTED', userId }); return fetchUserFromApi(userId) .then(userData => { dispatch({ type: 'USER_FETCH_SUCCEEDED', userData }); }) .catch(error => { dispatch({ type: 'USER_FETCH_FAILED', error }); }); }; };
  • redux-saga 则使用 ES6 的 Generator 函数来使异步流更易于读写。Sagas 可以监听派发到 store 的 actions,并在某个 action 被派发时执行复杂的异步逻辑。
javascript
import { call, put, takeEvery } from 'redux-saga/effects'; function* fetchUser(action) { try { const user = yield call(Api.fetchUser, action.payload.userId); yield put({type: 'USER_FETCH_SUCCEEDED', user: user}); } catch (e) { yield put({type: 'USER_FETCH_FAILED', message: e.message}); } } function* mySaga() { yield takeEvery('USER_FETCH_REQUESTED', fetchUser); }

总的来说,异步中间件在处理复杂的异步数据流时,可以提高 Redux 应用的可扩

2024年6月29日 12:07 回复

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

这种做法并没有什么问题。这在大型应用程序中很不方便,因为您将有不同的组件执行相同的操作,您可能想要对某些操作进行反跳,或者保留一些本地状态,例如自动递增 ID 靠近操作创建者等。因此,从从维护的角度将动作创建者提取到单独的功能中。

您可以阅读我对“如何在超时情况下调度 Redux 操作”的回答,以获取更详细的演练。

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

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

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

shell
// 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通过某种约定传递(例如,作为同步参数)。

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

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

shell
// 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得到其他动作创建者的结果,甚至不用考虑它们是同步还是异步:

shell
// 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 状态,您可以只使用getState传递给 thunk 的第二个参数,而无需修改调用代码:

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

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

shell
// 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,它允许您定义长期运行的守护进程(“saga”),这些守护进程在出现操作时采取操作,并在输出操作之前转换或执行请求。这将逻辑从动作创造者转变为传奇。您可能想检查一下,然后选择最适合您的。

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

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

2024年6月29日 12:07 回复

你不知道。

但是......你应该使用 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 中进行模拟。答案是,我们通常不会。我们通常将其视为需要重构代码的标志。

来源

sagas(正如它们在 中实现的那样redux-saga)是声明性的,就像 Free monad 或 React 组件一样,它们更容易在没有任何模拟的情况下进行测试。

另请参阅这篇文章

在现代函数式编程中,我们不应该编写程序——我们应该编写程序的描述,然后我们可以随意内省、转换和解释。

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

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

在前端世界中,对于 CQRS / EventSourcing 和 Flux / Redux 等一些后端概念如何相关可能存在很多困惑,主要是因为在 Flux 中我们使用术语“操作”,它有时可以表示命令式代码 ( )LOAD_USER和事件 ( USER_LOADED)。我相信,就像事件溯源一样,您应该只调度事件。

在实践中使用传奇

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

redux-thunk

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

shell
<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许可证来表达您只对获取最后单击的用户名的数据感兴趣(处理并发问题,以防用户快速单击大量用户名)。这种东西很难用 thunks 来完成。takeEvery如果您不想要这种行为,您可以使用。

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

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

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

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

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

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

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

传奇流日志记录

解耦

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

这是一个非常常见的误解,认为 sagas 只是为了取代 redux thunk 来提供更好的可测试性。实际上这只是 redux-saga 的一个实现细节。对于可测试性而言,使用声明性效果比 thunk 更好,但 saga 模式可以在命令式或声明性代码之上实现。

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

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

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

一些关于如何构建 Redux 应用程序的好文章,您可以在其中使用 Redux-saga 来实现解耦:

具体用例:通知系统

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

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

通知

在这里描述了如何使用 saga 来完成此操作

为什么叫传奇呢?

saga 这个术语来自后端世界。我最初在一次长时间的讨论中向 Yassine(Redux-saga 的作者)介绍了这个术语。

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

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

也可以看看:

备择方案

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

shell
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 调用是不值得的。
  • 对于大多数简单的情况,不要从项目中删除 thunk。
  • yield put(someActionThunk)如果有意义的话,请毫不犹豫地发出重击。

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

shell
const unsubscribe = store.addDispatchListener(action => { if (action.type === 'ping') { store.dispatch({ type: 'pong' }); } });
2024年6月29日 12:07 回复

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

在我们刚刚开始工作的新项目中,我也有非常相似的想法。我是 vanilla Redux 优雅系统的忠实粉丝,该系统以一种远离 React 组件树内部的方式更新存储和重新渲染组件。对我来说,使用这种优雅的dispatch机制来处理异步似乎很奇怪。

我最终采用了与我从项目中提取的库中的方法非常相似的方法,我们将其称为“ react-redux-controller”

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

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

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

  • redux-thunk通过将它们传递到你的 thunk 中,以一种函数式的方式做到这一点(根据 dome 的定义,使它们根本不完全是 thunk)。我没有使用过其他dispatch中间件方法,但我认为它们基本上是相同的。
  • React-redux-controller 使用协程来完成此操作。作为奖励,它还使您可以访问“选择器”,这些函数是您可能作为第一个参数传递给 的函数connect,而不必直接使用原始的标准化存储。
  • this您还可以通过各种可能的机制将它们注入上下文中,以面向对象的方式完成此操作。

更新

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

2024年6月29日 12:07 回复

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

在标准 Redux 数据流中执行此操作的最佳位置在哪里?怎么样:

  • 减速机?决不。它们应该是没有副作用的纯函数。更新商店是一件严肃而复杂的事情。不要污染它。
  • **愚蠢的视图组件?**绝对不是。他们关心的只有一个:演示和用户交互,并且应该尽可能简单。
  • **容器组件?**可能,但不是最优的。这是有道理的,因为容器是我们封装一些与视图相关的复杂性并与存储交互的地方,但是:
    • 容器确实需要比哑组件更复杂,但它仍然是单一职责:提供视图和状态/存储之间的绑定。您的异步逻辑与此完全不同。
    • 通过将其放置在容器中,您可以将异步逻辑锁定到单个上下文中,并与一个或多个视图/路由耦合。馊主意。理想情况下,它都是可重用的,并且与视图完全分离。
    • (与所有规则一样,如果您拥有可在多个上下文中重用的有状态绑定逻辑,或者您可以以某种方式将所有状态概括为集成 GraphQL 模式之类的东西,则可能存在例外。好吧,好吧,这可能是很酷。但是......大多数时候,绑定似乎最终都非常特定于上下文/视图。)
  • **其他一些服务模块?**坏主意:您需要注入对商店的访问权限,这是可维护性/可测试性的噩梦。最好遵循 Redux 的原则,仅使用提供的 API/模型访问商店。
  • **操作和解释它们的中间件?**为什么不?!对于初学者来说,这是我们剩下的唯一主要选择。:-) 从逻辑上讲,操作系统是解耦的执行逻辑,您可以在任何地方使用它。它可以访问商店并可以调度更多操作。它有一个单一的职责,那就是围绕应用程序组织控制流和数据流,大多数异步都适合这一点。
    • 那么动作创作者呢?为什么不直接在那里进行异步,而不是在操作本身和中间件中进行异步?
      • 首先也是最重要的是,创建者无法像中间件那样访问商店。这意味着您无法调度新的或有操作,无法从存储中读取数据来编写异步操作等。
      • 因此,将复杂性保留在必要性复杂的地方,并保持其他一切简单。创建者可以是简单、相对纯粹、易于测试的函数。
2024年6月29日 12:07 回复

回答一开始提出的问题:

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

请记住,这些文档适用于 Redux,而不是 Redux 加 React。_连接到 React 组件的_Redux 存储可以完全按照你所说的操作,但是没有中间件的 Plain Jane Redux 存储不接受除dispatch普通 ol' 对象之外的参数。

没有中间件,你当然仍然可以这样做

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

但这是一个类似的情况,异步是_围绕_Redux 进行的,而不是_由_Redux 处理。因此,中间件通过修改可以直接传递给dispatch.


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

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

shell
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是(或需要)异步的。

如果您还想支持PromiseObservablesSagas疯狂的自定义高度声明性的Action Creators,那么 Redux 只需更改您传递给的内容dispatch(也就是您从 Action Creators 返回的内容)即可做到这一点。无需对 React 组件(或connect调用)进行任何改动。

2024年6月29日 12:07 回复

Redux 本身是一个同步状态管理库,它专注于以可预测的方式管理和更新应用程序的状态。Redux 的核心概念是纯函数的 reducer 和同步的 action。当应用程序需要处理异步操作,如数据的 API 请求时,Redux 单独并不能有效地处理。

异步中间件,如 Redux Thunk 或 Redux Saga,使得在 Redux 应用程序中处理异步逻辑成为可能。下面是一些为什么需要异步中间件的原因:

1. 处理异步操作

Redux 的基本原则是 action 应该是一个具有 type 属性的对象,而且 reducer 应该是同步的纯函数。这种模式并不适用于执行异步操作,例如 API 调用。异步中间件允许我们在 dispatching action 之前执行异步代码,然后根据异步操作的结果来 dispatch 实际的 action。

例子: 假设我们有一个获取用户信息的异步操作。使用 Redux Thunk,我们可以创建一个 thunk action creator,它返回一个函数而非 action 对象。这个函数能够执行异步请求并且在请求完成后 dispatch 一个 action。

javascript
const fetchUserData = (userId) => { return (dispatch) => { dispatch({ type: 'FETCH_USER_REQUEST' }); fetch(`/api/user/${userId}`) .then((response) => response.json()) .then((user) => dispatch({ type: 'FETCH_USER_SUCCESS', payload: user })) .catch((error) => dispatch({ type: 'FETCH_USER_FAILURE', error })); }; };

2. 便于管理复杂的异步逻辑

在大型应用程序中,异步逻辑可能变得非常复杂,包括并发请求、条件请求、请求之间的竞争、错误处理等。异步中间件可以帮助管理这些复杂性,提供更清晰和更可维护的代码结构。

例子: 在使用 Redux Saga 的情况下,我们可以使用 ES6 的 generator 函数来更加直观和声明式地处理复杂的异步流。

javascript
function* fetchUserDataSaga(action) { try { yield put({ type: 'FETCH_USER_REQUEST' }); const user = yield call(fetchApi, `/api/user/${action.userId}`); yield put({ type: 'FETCH_USER_SUCCESS', payload: user }); } catch (error) { yield put({ type: 'FETCH_USER_FAILURE', error }); } }

3. 更好的测试性

异步中间件使得异步逻辑更加独立于组件,这有助于进行单元测试。我们可以在不进行实际的 API 调用的情况下,测试 action creators 和 reducers 的逻辑。

例子: 使用 Redux Thunk,我们可以测试 thunk action creator 是否正确地 dispatch 了相应的 actions。

javascript
// 使用 Jest 测试框架 it('creates FETCH_USER_SUCCESS when fetching user has been done', () => { // 模拟 dispatch 和 getState 函数 const dispatch = jest.fn(); const getState = jest.fn(); // 模拟 fetch API global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ id: 1, name: 'John Doe' }), }) ); // 执行 thunk action creator return fetchUserData(1)(dispatch, getState).then(() => { // 检查是否 dispatch 了正确的 action expect(dispatch).toHaveBeenCalledWith({ type: 'FETCH_USER_SUCCESS', payload: { id: 1, name: 'John Doe' } }); }); });

总结

Redux 需要异步中间件来处理异步操作,帮助维护复杂的异步逻辑,并提高代码的可测试性。这些中间件扩展了 Redux,使其能够以一种既有序又高效的方式处理异步数据流。

2024年6月29日 12:07 回复

Redux 本身是一个同步状态管理库,它专注于以可预测的方式管理和更新应用程序的状态。Redux 的核心概念是纯函数的 reducer 和同步的 action。当应用程序需要处理异步操作,如数据的 API 请求时,Redux 单独并不能有效地处理。

异步中间件,如 Redux Thunk 或 Redux Saga,使得在 Redux 应用程序中处理异步逻辑成为可能。下面是一些为什么需要异步中间件的原因:

1. 处理异步操作

Redux 的基本原则是 action 应该是一个具有 type 属性的对象,而且 reducer 应该是同步的纯函数。这种模式并不适用于执行异步操作,例如 API 调用。异步中间件允许我们在 dispatching action 之前执行异步代码,然后根据异步操作的结果来 dispatch 实际的 action。

例子: 假设我们有一个获取用户信息的异步操作。使用 Redux Thunk,我们可以创建一个 thunk action creator,它返回一个函数而非 action 对象。这个函数能够执行异步请求并且在请求完成后 dispatch 一个 action。

javascript
const fetchUserData = (userId) => { return (dispatch) => { dispatch({ type: 'FETCH_USER_REQUEST' }); fetch(`/api/user/${userId}`) .then((response) => response.json()) .then((user) => dispatch({ type: 'FETCH_USER_SUCCESS', payload: user })) .catch((error) => dispatch({ type: 'FETCH_USER_FAILURE', error })); }; };

2. 便于管理复杂的异步逻辑

在大型应用程序中,异步逻辑可能变得非常复杂,包括并发请求、条件请求、请求之间的竞争、错误处理等。异步中间件可以帮助管理这些复杂性,提供更清晰和更可维护的代码结构。

例子: 在使用 Redux Saga 的情况下,我们可以使用 ES6 的 generator 函数来更加直观和声明式地处理复杂的异步流。

javascript
function* fetchUserDataSaga(action) { try { yield put({ type: 'FETCH_USER_REQUEST' }); const user = yield call(fetchApi, `/api/user/${action.userId}`); yield put({ type: 'FETCH_USER_SUCCESS', payload: user }); } catch (error) { yield put({ type: 'FETCH_USER_FAILURE', error }); } }

3. 更好的测试性

异步中间件使得异步逻辑更加独立于组件,这有助于进行单元测试。我们可以在不进行实际的 API 调用的情况下,测试 action creators 和 reducers 的逻辑。

例子: 使用 Redux Thunk,我们可以测试 thunk action creator 是否正确地 dispatch 了相应的 actions。

javascript
// 使用 Jest 测试框架 it('creates FETCH_USER_SUCCESS when fetching user has been done', () => { // 模拟 dispatch 和 getState 函数 const dispatch = jest.fn(); const getState = jest.fn(); // 模拟 fetch API global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ id: 1, name: 'John Doe' }), }) ); // 执行 thunk action creator return fetchUserData(1)(dispatch, getState).then(() => { // 检查是否 dispatch 了正确的 action expect(dispatch).toHaveBeenCalledWith({ type: 'FETCH_USER_SUCCESS', payload: { id: 1, name: 'John Doe' } }); }); });

总结

Redux 需要异步中间件来处理异步操作,帮助维护复杂的异步逻辑,并提高代码的可测试性。这些中间件扩展了 Redux,使其能够以一种既有序又高效的方式处理异步数据流。 Redux 作为一个状态管理库,其核心设计是围绕着同步的状态更新。也就是说,在没有任何中间件的情况下,当一个 action 被派发(dispatched)时,它会立即通过同步的 reducers 更新状态。然而,在实际的应用中,我们经常需要处理异步操作,比如从服务器获取数据,这些操作并不能立刻完成并返回数据。

因此,为了在 Redux 架构中处理这些异步操作,我们需要一种方式来扩展 Redux 的功能,使其能够处理异步逻辑。这就是异步中间件的用武之地。以下是几个为什么 Redux 需要异步数据流中间件的理由:

  1. 维护纯净的 reducer 函数: Reducer 函数应该是纯函数,这意味着给定相同的输入,总是返回相同的输出,并且不产生任何副作用。异步操作(如 API 调用)会产生副作用,因此不能直接在 reducer 中处理。

  2. 扩展 Redux 的功能: 异步中间件像是 Redux 生态系统中的插件,它允许开发者在不修改原始 Redux 库代码的情况下增加新的功能。例如,可以增加日志记录、错误报告或异步处理等功能。

  3. 异步控制流: 异步中间件允许开发者在派发 action 和到达 reducer 之间插入一个异步操作。这意味着可以先发出一个表示“开始异步操作”的 action,然后在操作完成时发出另一个表示“异步操作完成”的 action。

  4. 更干净的代码结构: 通过将异步逻辑封装在中间件内,我们可以保持组件和 reducer 的简洁。这避免了在组件中混合异步调用和状态管理逻辑,有助于代码分离和维护。

  5. 测试和调试的便捷性: 中间件提供了一个独立的层,可以在这个层中进行单独的测试和模拟异步行为,而不必担心组件逻辑或者 UI 层的细节。

例子

在实际应用中,最常见的异步中间件是 redux-thunkredux-saga

  • redux-thunk 允许 action 创建函数(action creators)返回一个函数而不是一个 action 对象。这个返回的函数接收 dispatchgetState 作为参数,让你可以进行异步操作,并在操作结束后派发一个新的 action。
javascript
const fetchUserData = (userId) => { return (dispatch, getState) => { dispatch({ type: 'USER_FETCH_REQUESTED', userId }); return fetchUserFromApi(userId) .then(userData => { dispatch({ type: 'USER_FETCH_SUCCEEDED', userData }); }) .catch(error => { dispatch({ type: 'USER_FETCH_FAILED', error }); }); }; };
  • redux-saga 则使用 ES6 的 Generator 函数来使异步流更易于读写。Sagas 可以监听派发到 store 的 actions,并在某个 action 被派发时执行复杂的异步逻辑。
javascript
import { call, put, takeEvery } from 'redux-saga/effects'; function* fetchUser(action) { try { const user = yield call(Api.fetchUser, action.payload.userId); yield put({type: 'USER_FETCH_SUCCEEDED', user: user}); } catch (e) { yield put({type: 'USER_FETCH_FAILED', message: e.message}); } } function* mySaga() { yield takeEvery('USER_FETCH_REQUESTED', fetchUser); }

总的来说,异步中间件在处理复杂的异步数据流时,可以提高 Redux 应用的可扩

2024年6月29日 12:07 回复

你的答案