Flux存储器或操作(或两者)是否应触及外部服务?

124

商店应该保持自己的状态,并具有调用网络和数据存储服务的能力,在这种情况下,操作只是简单的消息传递者。

或者

商店是否应该成为不可变数据的哑收件人(并且操作应该在外部源之间获取/发送数据)?在这种情况下,Store将充当视图模型,并能够在根据动作提供的不可变数据设置其自身状态之前聚合/过滤其数据。

对我来说,似乎应该选择一种方式(而不是两者混合)。如果是这样,为什么一个方法比另一个更受推荐?


2
这篇文章可能会有所帮助:http://www.code-experience.com/async-requests-with-react-js-and-flux-revisited/ - Markus-ipse
对于那些评估flux模式不同实现的人,我强烈推荐看一下Redux https://github.com/rackt/redux。存储器被实现为纯函数,它们接收当前状态并发出该状态的新版本。由于它们是纯函数,所以它们是否可以调用网络和存储服务的问题不在你的手中:它们不能。 - plaxdan
6个回答

151

我曾经看到通量模式有两种实现方式,在自己尝试了这两种方法后(最初采用前一种方法),我认为存储应该是来自操作的数据的愚笨接收者,并且写入的异步处理应该在操作创建者中进行。(异步读取可以有不同的处理方式)。根据我的经验,这样做有一些好处,按重要性排序如下:

  1. 您的存储变得完全同步。 这使得您的存储逻辑更易于跟踪,并且非常容易进行测试 - 只需使用给定状态实例化一个存储,发送一个操作,然后检查状态是否按预期更改即可。此外,在通量中的核心概念之一是防止级联分派和同时分派多个操作;如果您的存储执行异步处理,则很难实现这一点。

  2. 所有操作分派均来自操作创建者。 如果您在存储中处理异步操作,并且要保持存储的操作处理程序同步(为了获取通量单个分派保证),则存储需要在响应异步处理时触发额外的“成功”和“失败”操作。将这些分派放在操作创建者中可以帮助分离操作创建者和存储的作业;此外,您无需深入挖掘存储逻辑以找出从哪里分派操作。在这种情况下,典型的异步操作可能如下所示(根据您使用的通量类型更改dispatch调用的语法):

    someActionCreator: function(userId) {
      // Dispatch an action now so that stores that want
      // to optimistically update their state can do so.
      dispatch("SOME_ACTION", {userId: userId});
    
      // This example uses promises, but you can use Node-style
      // callbacks or whatever you want for error handling.
      SomeDataAccessLayer.doSomething(userId)
      .then(function(newData) {
        // Stores that optimistically updated may not do anything
        // with a "SUCCESS" action, but you might e.g. stop showing
        // a loading indicator, etc.
        dispatch("SOME_ACTION_SUCCESS", {userId: userId, newData: newData});
      }, function(error) {
        // Stores can roll back by watching for the error case.
        dispatch("SOME_ACTION_FAIL", {userId: userId, error: error});
      });
    }
    

    应将可能在各种操作中重复的逻辑提取到单独的模块中;在这个例子中,该模块将是 SomeDataAccessLayer,用于处理实际的Ajax请求。

  3. 您需要更少的操作创建者。 这不是很重要,但很好有。如#2所述,如果您的存储具有同步操作分发处理(并且应该),则需要触发额外的操作来处理异步操作的结果。在操作创建者中执行分发意味着一个单一的操作创建者可以通过处理异步数据访问的结果来分派所有三种操作类型。


15
我认为发起 Web API 调用的来源(动作创建者 vs. 存储器)不如成功/错误回调函数应创建一个操作这个事实重要。因此,数据流始终是:操作 -> 调度程序 -> 存储器 -> 视图。 - fisherwebdev
1
将实际请求逻辑放在API模块中是否更好/更容易测试?因此,您的API模块可以返回一个承诺,您可以从中调度。操作创建者只是根据发送初始“挂起”操作后的解析/失败进行调度。剩下的问题是组件如何侦听这些“事件”,因为我不确定请求状态是否应该映射到存储状态。 - backdesk
@backdesk 这正是我在上面的示例中所做的:分派初始挂起操作("SOME_ACTION"),使用 API 发出请求(SomeDataAccessLayer.doSomething(userId))返回一个 Promise,在两个 .then 函数中,分派其他操作。如果应用程序需要了解状态的状态,则请求状态可以(或多或少)映射到存储状态。如何映射取决于应用程序(例如,也许每个评论都有单独的错误状态,就像 Facebook 一样,或者可能有一个全局错误组件)。 - Michelle Tilley
@MichelleTilley:“Flux 中的核心概念之一是防止级联调度和同时防止多个调度;当您的存储库进行异步处理时,这非常难做到。” 这对我来说是一个关键点。说得好。 - user99874

51

我在Twitter上向Facebook的开发人员提问,Bill Fisher给我的答复是:

当响应用户与UI交互时,我会在操作创建器方法中进行异步调用。

但是当你有一个ticker或其他非人驱动程序时,从store调用更好。

重要的是在错误/成功回调中创建一个action,以便数据始终起源于action。


虽然这是有道理的,但你有任何想法为什么“在动作由非人驱动程序触发时,从存储中调用会更好吗?” - SharpCoder
@SharpCoder 我猜如果你有一个实时更新的股票或类似的东西,你不需要触发一个动作,当你从存储中这样做时,你可能需要写更少的代码,因为存储可以立即访问状态并发出更改。 - Florian Wendelborn

8

商店应该做所有事情,包括获取数据,并向组件发出信号,表明商店的数据已更新。 为什么?因为这样可以使操作变得轻量级、可丢弃和可替换,而不影响重要行为。所有重要的行为和功能都发生在商店中。这也防止了行为的重复,在两个非常相似但不同的操作中会复制行为。商店是您的唯一的(处理)真相来源。

在我看到的每个Flux实现中,Actions基本上是将事件字符串转换为对象,就像传统上你会有一个名为“anchor:clicked”的事件,但在Flux中它将被定义为AnchorActions.Clicked。它们甚至如此“愚蠢”,以至于大多数实现都有单独的Dispatcher对象来实际将事件分派到正在侦听的商店。

个人喜欢Reflux对Flux的实现方式,其中没有单独的Dispatcher对象,而Action对象自己进行分派。


编辑:Facebook的Flux实际上会获取“action creators”,因此他们确实使用智能操作。他们还使用存储器准备有效负载:

https://github.com/facebook/flux/blob/19a24975462234ddc583ad740354e115c20b881d/examples/flux-chat/js/actions/ChatMessageActionCreators.js#L27(第27和28行)

完成后的回调将触发新的操作,这次使用获取的数据作为有效负载:

https://github.com/facebook/flux/blob/19a24975462234ddc583ad740354e115c20b881d/examples/flux-chat/js/utils/ChatWebAPIUtils.js#L51

所以我想那应该是更好的解决方案。


这个Reflux实现是什么?我没听说过。你的回答很有趣。你的意思是你的存储实现应该具备执行API调用等逻辑吗?我原本认为存储只应该接收数据并更新其值。它们会根据特定的操作进行过滤,并更新其存储的某些属性。 - Jeremy D
Reflux 是 Facebook 的 Flux 模式的一个轻微变化:https://github.com/spoike/refluxjs。Stores 管理应用程序中整个“模型”领域,而 Actions/Dispatchers 只是将一切缝合在一起。 - user217782
1
所以我又想了一下,(几乎)回答了自己的问题。我本来想在这里添加一个答案(供其他人投票),但显然我在stackoverflow上的karma太低了,还不能发布答案。所以这是一个链接:https://groups.google.com/d/msg/reactjs/PpsvVPvhBbc/BZoG-bFeOwoJ - plaxdan
谢谢提供 Google 群组链接,看起来非常有用。我也更喜欢所有内容都通过调度程序进行处理,并在存储器中使用非常简单的逻辑,基本上只是更新它们的数据。@Rygu 我会查看 reflux。 - Jeremy D
我用另一种观点编辑了我的答案。看起来两种解决方案都是可行的。我几乎肯定会选择Facebook的解决方案而不是其他方案。 - user217782

3

我将支持“愚蠢”的 Actions。

通过在 Actions 中添加收集视图数据的职责,您将使您的 Actions 与视图的数据要求联系在一起。

相比之下,泛用的 Actions 只是声明用户意图或应用程序中的某些状态转换,允许任何响应该 Action 的 Store 将意图转换为专门针对订阅它的视图定制的状态。

这适用于更多但更小、更专业化的 Stores。我支持这种风格,因为

  • 这使得您可以更灵活地使用视图消耗 Store 数据
  • “智能”存储器,专门针对其消费者的视图,将比“智能”操作更小且在复杂应用程序中相互耦合较少,而可能有多个视图依赖于这些操作

存储器的目的是向视图提供数据。“Action”这个名字让我觉得它的目的是描述我的应用程序中的变化。

假设您需要向现有的仪表板视图添加一个小部件,该小部件显示后端组新推出的一些花哨的聚合数据。

使用“智能” Actions,您可能需要更改“refresh-dashboard” Action,以便使用新 API。但是,“抽象意义上的刷新仪表板”并没有改变。您的视图数据要求已经发生了变化。

使用“愚蠢”的 Actions,您可以为新小部件添加一个新的 Store,并设置它,以便在接收到“refresh-dashboard” Action 类型时,发送请求获取新数据,并在准备就绪后将其显示给新小部件。我认为当视图层需要更多或不同的数据时,我更改的是那些数据源:Stores。


2

gaeron的flux-react-router-demo有一种不错的实用方法。

ActionCreator从外部API服务生成一个Promise,然后将该Promise和三个Action常量传递给代理/扩展Dispatcher中的dispatchAsync函数。dispatchAsync将始终分派第一个Action,例如“GET_EXTERNAL_DATA”,一旦Promise返回,它将分派“GET_EXTERNAL_DATA_SUCCESS”或“GET_EXTERNAL_DATA_ERROR”之一。


1
如果你希望有一天拥有类似于Bret Victor所展示的著名视频“原则发明”中看到的开发环境,你应该使用一些简单的存储器,它们只是数据结构内部操作/事件的投影,没有任何副作用。如果你的存储器实际上是同一个全局不可变数据结构的成员,就像Redux那样,这也会有所帮助。

更多解释请参见:https://dev59.com/1mox5IYBdhLWcg3wFAY1#31388262


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接