为什么我们需要在Redux中使用asynchronousstream的中间件?

根据文档, “没有中间件,Redux商店只支持同步数据stream” 。 我不明白为什么是这样。 为什么容器组件不能调用asynchronousAPI,然后dispatch这些操作?

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

一个字段和一个按钮

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

当导出的组件被渲染时,我可以点击button,input被正确更新。

请注意connect呼叫中的updatefunction。 它分派一个动作,告诉应用程序正在更新,然后执行asynchronous调用。 调用完成后,提供的值将作为另一个操作的有效负载调度。

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

编辑:我searchRedux回购线索,发现行动创造者被要求是纯粹的function在过去。 例如, 这里是一个用户试图为asynchronous数据stream提供更好的解释:

动作创build者本身仍然是一个纯函数,但它返回的thunk函数不需要,它可以做我们的asynchronous调用

动作创作者不再被要求是纯粹的。 所以,过去肯定需要thunk / promise中间件,但似乎不再是这样了?

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

这种方法没有错。 在一个大的应用程序中,这只是不方便的,因为你将会有不同的组件执行相同的动作,你可能想要去除一些动作,或者保持一些本地状态,比如自动增加ID接近动作创build者,等等。将操作创build者提取到不同的function的维护angular度。

您可以阅读我的答案“如何使用超时发送Redux操作”以获得更详细的演练。

像Redux Thunk或Redux Promise这样的中间件只是给你“语法糖”来调度thunk或promises,但是你不必使用它。

所以,没有任何中间件,你的动作创造者可能看起来像

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

所以没有太大的区别 我喜欢后一种方法的一件事是组件不关心动作创build者是asynchronous的。 它只是调用dispatch正常,它也可以使用mapDispatchToProps绑定这样的动作创造者与一个简短的语法等。组件不知道如何实现行动创造者,你可以切换不同的asynchronous方法(Redux Thunk,Redux Promise, Redux Saga)而不更改组件。 另一方面,使用前者的显式方法,组件确切地知道特定的调用是asynchronous的,并且需要通过一些约定来传递dispatch (例如,作为同步参数)。

也想想这个代码将如何改变。 假设我们想要有第二个数据加载function,并将它们合并成一个单独的动作创build器。

用第一种方法,我们需要注意我们正在呼叫什么样的动作创造者:

 // 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动作创build者可以dispatch其他动作创build者的结果,甚至不用考虑它们是同步的还是asynchronous的:

 // 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! } 

使用这种方法,如果您稍后希望动作创build者查看当前的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等中间件的好处是,组件不知道动作创build者是如何实现的,他们是否在意Redux状态,是否同步或asynchronous,以及是否调用其他动作创build者。 缺点是有些间接,但是我们相信它在实际应用中是值得的。

最后,Redux Thunk和朋友只是Redux应用程序中asynchronous请求的一种可能的方法。 另一个有趣的方法是Redux Saga ,它可以让你定义长时间运行的守护进程(“sagas”),在进行动作的时候执行动作。 这将动作创造者的逻辑转化为传奇。 你可能要检查一下,然后select最适合你的东西。

我searchRedux回购索引,发现行动创造者在过去被要求是纯粹的function。

这是不正确的。 文件这样说,但文件是错的。
行动创造者从来没有被要求是纯粹的function。
我们修复了文档以反映这一点。

你没有。

但是…你应该使用redux-saga 🙂

丹·阿布拉莫夫(Dan Abramov)的回答是正确的,但我会多谈一些比较相似但更强大的复杂传奇 。

必要的VS声明

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

当你手中有一个如同一个IO单子或一个承诺时,你一旦执行就不会轻易知道它会做什么。 testingthunk的唯一方法是执行它,并嘲笑调度员(或整个外部世界,如果它与更多的东西交互…)。

如果你正在使用模拟,那么你没有做function编程。

从副作用的angular度来看,嘲笑是一个标志,你的代码是不纯的,在function程序员的眼中,certificate有什么错误。 我们不应该下载一个图书馆来帮助我们检查冰山是否完整,而是应该绕过它。 一个硬核的TDD / Java人曾经问过我如何在Clojure中嘲笑。 答案是,我们通常不会。 我们通常将其视为我们需要重构代码的标志。

资源

传奇(因为他们已经实现了redux-saga )是声明式的,就像Free monad或者React组件一样,它们在没有任何模拟的情况下更容易testing。

另见这篇文章 :

在现代计算机体系中,我们不应该编写程序 – 我们应该编写程序的描述,然后我们可以随意反思,转换和解释。

(实际上,Redux-saga就像是一个混合体:stream动势在必行,但效果是陈述性的)

混乱:行动/事件/命令…

前端世界中有很多关于CQRS / EventSourcing和Flux / Redux等后端概念如何相关的混淆,主要是因为在Flux中我们使用的术语“action”有时代表命令代码( LOAD_USER )和事件( USER_LOADED )。 我相信像事件采购一样,你只应该派遣事件。

在实践中使用传奇

想象一个带有用户configuration文件链接的应用程序。 用两个中间件来处理这个问题的惯用方法是:

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

这个传奇转化为:

每次用户名被点击时,获取用户configuration文件,然后用加载的configuration文件发送一个事件。

正如你所看到的那样, redux-saga有一些优点。

takeLatest的使用允许expression你只是感兴趣的点击最后一个用户名的数据(处理并发问题的情况下,用户点击很多用户名非常快)。 这种东西很难与thunk。 如果你不想要这个行为,你可以使用takeEvery

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

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

您不需要再触发类似于actions.loadUser() rpc调用。 你的用户界面只需要调度已经发生的事情。 我们只是发生事件 (总是过去式)而不是行动了。 这意味着您可以创build分离的“鸭子”或有界的上下文 ,并且这个传奇可以充当这些模块化组件之间的耦合点。

这意味着你的观点更容易pipe理,因为他们不再需要包含已经发生的事情和应该发生的事情之间的翻译层

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

有些人可能会说,发生器本身可以用本地variables隐藏redux存储以外的状态,但是如果你开始通过启动定时器等来编排thunk中的复杂事物,那么无论如何你都会遇到相同的问题。 还有一个select效果,现在允许从你的Redux商店得到一些状态。

萨加斯可以时间旅行,也可以使复杂的stream量logging和开发工具,目前正在进行。 这是一些已经实现的简单的asynchronousstream日志logging:

传奇流量记录

解耦

萨加斯不仅取代了雷克斯。 他们来自后端/分布式系统/事件采购。

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

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

为了简化这个前端的世界,想象有widget1和widget2。 当点击widget1上的某个button时,它应该对widget2有效。 而不是将两个小部件连接在一起(即,小部件1派发一个针对小部件2的动作),小部件1只调度其button被点击。 然后,这个传奇侦听这个button点击,然后通过分散widget2意识到的新事件来更新widget2。

这增加了简单应用程序不必要的间接级别,但是可以更容易地扩展复杂的应用程序。 现在,您可以将widget1和widget2发布到不同的npm存储库,以便他们永远不必知道彼此,而不需要共享全局registry。 这两个小部件现在是可以分开生活的有界环境。 他们不需要彼此保持一致,也可以在其他应用程序中重用。 这个传奇是两个小工具之间的耦合点,它们以一种有意义的方式协调您的业务。

关于如何构build您的Redux应用程序的一些不错的文章,您可以在其中使用Redux-saga解耦原因:

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

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

我不希望我的JSX组件决定何时显示/隐藏通知。 我只是要求它能够请求通知,并将复杂的规则留在传奇中。 这样的东西很难实施,只是做出承诺或承诺。

通知

我在这里描述了如何用传奇完成这件事

为什么叫做佐贺?

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

最初,该术语是在一篇论文中引入的,传奇模式应该被用来处理分布式事务中的最终一致性,但是它的使用已经被后端开发者扩展到更广泛的定义,所以现在它也覆盖了“stream程pipe理器”模式(不知何故,原始的传奇模式是stream程pipe理器的一种特殊forms)。

今天,“传奇”一词很混乱,因为它可以描述两种不同的东西。 由于它用于简化事件,它没有描述处理分布式事务的方式,而是一种协调应用程序中的操作的方式。 redux-saga也可以叫做redux-process-manager

也可以看看:

  • Yassine关于Redux-saga历史的采访
  • 凯拉字节:克拉佐贺模式
  • 微软CQRS之旅:萨加斯传奇
  • Yassine的中等反应

备择scheme

如果您不喜欢使用生成器的想法,但是您对事件模式及其解耦属性感兴趣,那么也可以使用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 vs Redux-thunk with async / await
  • 在Redux Saga中pipe理进程
  • 从ActionCreators到Sagas
  • 用Redux-saga实现的贪吃蛇游戏

2017build议

  • 为了使用它,不要过分使用Redux-saga。 可testing的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' }); } }); 

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

当我开始工作的一个新项目时,我有一个非常相似的思路。 我是一个香草Redux优雅的系统的更大的粉丝,更新存储和rerendering组件的方式,留在了一个React组件树的胆量。 对于我来说,挂钩那个优雅的dispatch机制来处理asynchronous似乎很奇怪。

我最终采取了一种非常类似的方法来处理我们在我们的项目中被分解出来的一个库,那就是我们所谓的react-redux-controller 。

我最终没有按照上面的几个原因采取确切的方法:

  1. 你写它的方式,这些调度function不能访问商店。 您可以通过让UI组件传递调度function所需的所有信息来解决这个问题。 但是我认为这会将这些UI组件与调度逻辑不必要的结合起来。 更有问题的是,调度function没有明显的方式来访问asynchronous延续中的更新状态。
  2. 调度function可以通过词法范围来dispatch自己。 这限制了重构的选项,一旦connect语句失控 – 而且这只是一个update方法看起来相当笨拙。 所以你需要一些系统让你编写这些调度器function,如果你把它们分解成单独的模块。

综合起来,你必须build立一些系统,让dispatch和商店被注入你的调度function,以及事件的参数。 我知道这种dependency injection的三种合理的方法:

  • 通过将它们传递给你的thunk(通过圆顶定义使它们不完全是thunk)来实现这个function。 我没有与其他dispatch中间件方法,但我认为他们是基本相同的。
  • react-redux-controller使用协程来做到这一点。 作为奖励,它还使您能够访问“select器”,这是您可能已经作为第一个参数进行connect函数,而不是直接与原始的,规范化的商店一起工作。
  • 你也可以通过各种可能的机制将它们注入到this上下文中来实现面向对象的方式。

更新

对我来说,这个难题的一部分是反应减less的局限性。 connect的第一个参数获取状态快照,但不派遣。 第二个参数是调度,但不是状态。 这两个参数都不会得到一个在当前状态下closures的thunk,因为在继续/callback时能够看到更新的状态。

要回答开头提到的问题:

为什么容器组件不能调用asynchronousAPI,然后调度这些操作?

请记住,这些文档是Redux,而不是Redux + React。 Redux商店连接到React组件可以完全按照你所说的做,但是没有中间件的Plain Jane Redux商店不接受除了普通ol对象之外的dispatch参数。

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

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

但是这是一个类似的情况,asynchronous是围绕 Redux而不是 Redux处理 。 所以,中间件允许通过修改可以直接传递到dispatch内容来实现asynchronous。


这就是说,我认为你的build议的精神是有效的。 在Redux + React应用程序中,当然还有其他一些方法可以处理asynchronous。

使用中间件的一个好处是,您可以继续正常使用动作创build器,而不必担心它们是如何连接的。 例如,使用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是(或者需要)是asynchronous的。

如果你还想支持承诺 , 观察者 , 传奇 ,或疯狂的定制和高度声明的动作创造者,那么Redux可以通过改变你传递给你的信息(也就是你从动作创build者返回的信息)来做到这一点。 不需要使用React组件(或connect调用)。

阿布拉莫夫的目标 – 理想上每个人 – 都只是简单地将复杂性和asynchronous性包含在最合适的地方

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

  • 减速器 ? 没门。 它们应该是纯粹的function,没有任何副作用。 更新店面是严重的,复杂的业务。 不要污染它。
  • 哑视图组件? 当然不是。他们有一个担心:演示和用户交互,应尽可能简单。
  • 容器组件? 可能的,但是次优的。 这是有道理的,容器是一个地方,我们封装一些视图相关的复杂性,并与商店互动,但:
    • 容器确实需要比愚蠢的组件复杂,但它仍然是一个单一的责任:在视图和状态/存储之间提供绑定。 你的asynchronous逻辑是一个完全不同的问题。
    • 通过将它放置在一个容器中,您可以将您的asynchronous逻辑locking到一个单一的上下文中,以实现单个视图/路由。 馊主意。 理想情况下,它都是可重用的,并且完全解耦。
  • 其他服务模块? 糟糕的想法:你需要注入访问商店,这是一个可维护性/可testing性的噩梦。 最好使用Redux的谷物,只使用提供的API /模型访问商店。
  • 行动和解释他们的中间件? 为什么不?! 对于初学者来说,这是我们唯一的主要select。 :-)从逻辑上讲,动作系统是可以从任何地方使用的解耦执行逻辑。 它可以访问商店,并可以派出更多的行动。 它有一个单独的责任,就是围绕应用程序组织控制stream和数据stream,而且大部分的asynchronous操作都适用于此。
    • 行动创作者呢? 为什么不在那里做asynchronous,而不是在动作本身和中间件?
      • 首先也是最重要的是,创作者不能像中间件一样访问商店。 这意味着你不能派遣新的应急行动,不能从商店读取构成你的asynchronous等。
      • 所以,把复杂性保持在一个复杂的必要性的地方,保持一切简单。 创build者可以是简单的,相对纯粹的function,很容易testing。