在Flux应用程序中,Ajax请求应该在哪里进行?

199

我正在使用Flux架构创建一个React.js应用程序,我正在尝试找出从服务器请求数据的何时何地。是否有相关示例可供参考?(不是TODO应用程序!)

6个回答

127

我十分支持在action creators中放置异步写操作,在store中放置异步读操作。这样做的目的是将store状态修改代码保持在完全同步的action处理程序中;这使得它们易于理解和单元测试。为了防止对同一端点进行多次同时请求(例如,重复读取),我会将实际请求处理移到一个单独的模块中,并使用promises来防止多次请求;例如:

class MyResourceDAO {
  get(id) {
    if (!this.promises[id]) {
      this.promises[id] = new Promise((resolve, reject) => {
        // ajax handling here...
      });
    } 
    return this.promises[id];
  }
}

尽管存储器中的读取涉及异步函数,但存在一个重要的注意点,即存储器不会在异步处理程序中自行更新,而是在响应到达时触发一个操作,触发一个操作。对于此操作的处理程序最终执行实际的状态修改。

例如,组件可能会执行:

getInitialState() {
  return { data: myStore.getSomeData(this.props.id) };
}

商店可能已经实现了一个方法,类似于这样:

class Store {
  getSomeData(id) {
    if (!this.cache[id]) {
      MyResurceDAO.get(id).then(this.updateFromServer);
      this.cache[id] = LOADING_TOKEN;
      // LOADING_TOKEN is a unique value of some kind
      // that the component can use to know that the
      // value is not yet available.
    }

    return this.cache[id];
  }

  updateFromServer(response) {
    fluxDispatcher.dispatch({
      type: "DATA_FROM_SERVER",
      payload: {id: response.id, data: response}
    });
  }

  // this handles the "DATA_FROM_SERVER" action
  handleDataFromServer(action) {
    this.cache[action.payload.id] = action.payload.data;
    this.emit("change"); // or whatever you do to re-render your app
  }
}

1
@Federico 我仍然不清楚什么是“最佳”解决方案。我一直在尝试使用这种数据加载策略,并结合计算未完成异步请求的数量。不幸的是,flux是在构建之后注入到store中的,所以很难在initialize方法中获取actions。你可能会从Yahoo的同构流库中获得一些好的想法;这是Fluxxor v2应该更好支持的内容。如果您想进一步交流,请随时给我发电子邮件。 - Michelle Tilley
1
data: result 应该改为 data: data,对吧?没有 result。或许最好将 data 参数重命名为 payload 或其他类似的名称。 - oligofren
@BinaryMuse 在上面的代码示例中,'return this.cache [primaryKey]'语句不应该在'else'分支之外吗?在当前情况下,'LOADING_TOKEN'值将永远不会返回给getSomeData方法的调用者。此外,在'updateFromServer'方法中,'primaryKey'值从哪里来?我知道这只是一个代码示例,但我想知道我是否理解正确,因为我认为我会将这个原则纳入我的React Flux应用程序中。 - Björn Boxstart
@BjörnBoxstart,你对if语句的正确性是正确的——这是一个已经修复的打字错误。我还更新了伪代码,以使primaryKey来自哪里更加明显(它在服务器响应中)。 - Michelle Tilley
2
我发现这个旧帖子非常有帮助-特别是Bill Fisher和Jing Chen的评论。这非常接近@BinaryMuse提出的方案,唯一的区别在于dispatching发生在action creator中。 - phillipwei
显示剩余4条评论

37

Fluxxor有一个与API进行异步通信的示例

这个博客文章讨论了它,并在React的博客上得到了推荐。


我认为这是一个非常重要且困难的问题,目前还没有明确的答案,因为前端软件与后端的同步仍然很麻烦。
在JSX组件中进行API请求?存储?其他地方?
在存储器中执行请求意味着,如果两个存储器需要给定操作的相同数据,则它们将发出2个类似的请求(除非您介绍存储器之间的依赖项,我真的不喜欢)。
在我的情况下,我发现将Q promises作为操作的有效负载非常方便,因为:
- 我的操作不需要可序列化(我不保留事件日志,我不需要事件源的事件重放功能) - 它消除了需要具有不同操作/事件(请求已发送/请求已完成/请求失败)并且必须使用相关ID将它们匹配的需要,当可以同时发出并发请求时。 - 它允许多个存储器监听相同请求的完成,而不会在存储器之间引入任何依赖关系(但是可能最好引入缓存层?)

Ajax很糟糕

我认为在不久的将来,Ajax将越来越少被使用,因为它很难理解。 正确的方法?将设备视为分布式系统的一部分 我不知道我最初从哪里得到这个想法(也许是在这个启发人心的Chris Granger视频中)。

想想看。现在,为了可扩展性,我们使用具有最终一致性作为存储引擎的分布式系统(因为我们无法打败CAP定理,而且我们经常希望可用)。这些系统不通过轮询彼此进行同步(除了可能进行共识操作?),而是使用类似CRDT和事件日志的结构,使分布式系统的所有成员最终一致(成员将在足够的时间内收敛到相同的数据)。

现在想想移动设备或浏览器是什么。它只是分布式系统的一个成员,可能会遭受网络延迟和网络分区。(即您在地铁上使用智能手机)

如果我们能够构建网络分区和网络速度容错的数据库(我的意思是我们仍然可以对隔离节点执行写操作),那么我们可能可以构建受这些概念启发的前端软件(移动或桌面),这些软件支持离线模式,无需应用程序功能不可用。
我认为我们应该真正借鉴数据库的工作方式来设计我们的前端应用程序架构。需要注意的一点是,这些应用程序不会执行POST、PUT和GET ajax请求来相互发送数据,而是使用事件日志和CRDT来确保最终一致性。
那么为什么前端不能这样做呢? 请注意,后端已经朝着这个方向发展,像Kafka这样的工具已被大型公司广泛采用。这与事件溯源/CQRS/DDD有关。
请查看Kafka作者的这些精彩文章,以说服自己:

也许我们可以通过向服务器发送命令,并通过流接收服务器事件(例如通过Websockets),而不是触发Ajax请求。

我从未对Ajax请求感到非常舒适。由于我们React开发人员倾向于成为函数式程序员,因此我认为很难推断出本地数据应该是您前端应用程序的“真相来源”,而真正的真相来源实际上在服务器数据库上,您的“本地”真相来源可能已经过时,除非您按下某些无聊的刷新按钮,否则永远不会趋近于真正的真相值......这是工程学吗?

但是,由于一些明显的原因,设计这样的东西仍然有些困难:

您的移动/浏览器客户端资源有限,不能保证所有数据都存储在本地(因此有时需要使用ajax请求轮询重内容)。为了安全起见,您的客户端不应看到分布式系统的所有数据,因此需要对其接收到的事件进行过滤。

3
能否提供一个使用 Q promises 和 actions 的例子? - Matt Foxx Duncan
@MattFoxxDuncan 不确定这是否是一个好主意,因为它使“事件日志”不可序列化,并使存储异步更新在操作被触发时,所以它有一些缺点。然而,如果它适用于您的用例并且您理解这些缺点,它非常方便并减少样板文件。使用Fluxxor,您可能可以做类似于this.dispatch("LOAD_DATA", {dataPromise: yourPromiseHere});的事情。 - Sebastien Lorber
完全不同意你的 AJAX 论点。实际上,读起来非常令人讨厌。你看过你自己的评论了吗?想想那些赚大钱的商店、游戏和应用程序 - 所有这些都需要 API 和 AJAX 服务器调用……如果你想要“无服务器”或类似的东西,请看 Firebase,但是 AJAX 还会继续存在下去。我希望至少没有其他人同意你的逻辑。 - TheBlackBenzKid
@TheBlackBenzKid Firebase、Meteor等平台后台的数据模型不够好。你知道这些系统如何处理并发写入吗?是否采用了最后一次写入覆盖之前内容的策略,而非因果一致性/合并策略?在两个人都在使用不可靠的连接时,你觉得能承受覆盖同事应用程序中的工作吗?此外,请注意这些系统往往会将本地和服务器模型耦合在一起。你知道有哪些著名的协同应用程序在脱机情况下可以正常运行,并且宣称是满意的Firebase用户吗?我不认为有。 - Sebastien Lorber
嗨@SebastienLorber,我正在阅读这篇文章,很快就会分享我的想法。到目前为止,听起来不错。 - TheBlackBenzKid
显示剩余2条评论

20

你可以在action creators或stores中调用数据。重要的是不直接处理响应,而是在错误/成功回调中创建一个操作。在store中直接处理响应会导致设计更加脆弱。


9
请问您能否详细解释一下这个问题?比如说,我需要从服务器加载初始数据。在控制器视图中,我会开始一个名为“INIT”的动作,而存储器会根据此动作启动其异步初始化过程。现在,我的想法是当存储器获取到数据后,它只需发出更改通知,而不是启动另一个动作。因此,在初始化完成后发出更改通知可以告诉视图它们可以从存储器获取数据。但为什么需要在成功加载后发出更改通知,而是启动另一个动作呢?谢谢。 - Attila Kling
Fisherwebdev,关于商店调用数据,这样做不会破坏Flux范例吗?我能想到的唯一两种适当的调用数据的方式是:1.使用Actions来加载数据的引导类;2.再次使用Actions来加载数据的视图。 - Yotam
4
请求数据并不等同于接收数据。Jim-Y:只有在存储器中的数据实际发生变化后,才应该触发更改。Yotam:不,请求存储器中的数据不会破坏范式。数据应该只通过操作进行接收,以便所有存储器都可以获得有关进入应用程序的任何新数据的信息。所以我们可以在存储器中请求数据,但当响应返回时,我们需要创建一个新的操作而不是直接处理它。这可以使应用程序灵活并且有能力对新功能进行开发。 - fisherwebdev

2
我一直在使用Binary Muse的示例,来自 Fluxxor ajax example。这是我使用相同方法的非常简单的示例。
我有一个简单的产品商店,一些产品操作控制器视图组件,它具有所有响应对产品商店所做更改的子组件。例如产品滑块产品列表产品搜索组件。 虚假产品客户端 这是一个虚假的客户端,您可以将其替换为调用返回产品的实际端点。
var ProductClient = {

  load: function(success, failure) {
    setTimeout(function() {
      var ITEMS = require('../data/product-data.js');
      success(ITEMS);
    }, 1000);
  }    
};

module.exports = ProductClient;

产品商店
这里是产品商店,显然这是一个非常简约的商店。
var Fluxxor = require("fluxxor");

var store = Fluxxor.createStore({

  initialize: function(options) {

    this.productItems = [];

    this.bindActions(
      constants.LOAD_PRODUCTS_SUCCESS, this.onLoadSuccess,
      constants.LOAD_PRODUCTS_FAIL, this.onLoadFail
    );
  },

  onLoadSuccess: function(data) {    
    for(var i = 0; i < data.products.length; i++){
      this.productItems.push(data.products[i]);
    }    
    this.emit("change");
  },

  onLoadFail: function(error) {
    console.log(error);    
    this.emit("change");
  },    

  getState: function() {
    return {
      productItems: this.productItems
    };
  }
});

module.exports = store;

现在产品动作会发起 AJAX 请求,成功后触发 LOAD_PRODUCTS_SUCCESS 动作将产品返回到存储中。
产品动作:
var ProductClient = require("../fake-clients/product-client");

var actions = {

  loadProducts: function() {

    ProductClient.load(function(products) {
      this.dispatch(constants.LOAD_PRODUCTS_SUCCESS, {products: products});
    }.bind(this), function(error) {
      this.dispatch(constants.LOAD_PRODUCTS_FAIL, {error: error});
    }.bind(this));
  }    

};

module.exports = actions;

所以,从任何监听此存储的组件调用 this.getFlux().actions.productActions.loadProducts() 将加载产品。
您可以想象有不同的操作响应用户交互,如 addProduct(id)removeProduct(id) 等... 遵循相同的模式。
希望这个例子能够有所帮助,因为我发现这个实现有点棘手,但肯定有助于保持我的存储器100%同步。

2

我在这里回答了一个相关的问题:如何在Flux中处理嵌套的API调用

Actions不应该是引起变化的东西。它们应该像报纸一样通知应用程序外部世界的变化,然后应用程序响应这个新闻。存储会导致自身的变化。操作只是通知他们。

Flux的创建者Bill Fisherhttps://dev59.com/SV8d5IYBdhLWcg3wpzlf#26581808说过,你基本上需要做的就是通过操作说明你需要什么数据。如果存储通过操作得到通知,它应该决定是否需要获取一些数据。

存储应负责累积/获取所有所需数据。但重要的是要注意,在存储请求数据并获得响应后,它应该使用已获取的数据触发一个操作,而不是直接处理/保存响应。

存储可能看起来像这样:

class DataStore {
  constructor() {
    this.data = [];

    this.bindListeners({
      handleDataNeeded: Action.DATA_NEEDED,
      handleNewData: Action.NEW_DATA
    });
  }

  handleDataNeeded(id) {
    if(neededDataNotThereYet){
      api.data.fetch(id, (err, res) => {
        //Code
        if(success){
          Action.newData(payLoad);
        }
      }
    }
  }

  handleNewData(data) {
    //code that saves data and emit change
  }
}

0

8
按照指南投反对票。在外部网站上发布答案会降低本网站的实用性和答案质量,降低本网站的实用性。外部网址可能随时间而失效。投反对票并不表示该文章没有用,顺便说一下,该文章非常好 :) - oligofren
2
好的帖子,但是添加每种方法的优缺点的简短摘要将会让你得到更多的赞。在SO上,我们不应该需要点击链接来获取你的答案要点。 - Cory House

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