【问题标题】:How to handle nested api calls in flux如何处理不断变化的嵌套 api 调用
【发布时间】:2023-04-03 20:22:01
【问题描述】:

我正在使用 Facebook 的 Flux Dispatcher 创建一个简单的 CRUD 应用程序来处理英语学习网站的帖子的创建和编辑。我目前正在处理一个看起来像这样的 api:

/posts/:post_id
/posts/:post_id/sentences
/sentences/:sentence_id/words
/sentences/:sentence_id/grammars

在应用程序的显示和编辑页面上,我希望能够在一个页面上显示给定帖子的所有信息以及所有句子以及句子的单词和语法详细信息。

我遇到的问题是弄清楚如何启动收集所有这些数据所需的所有异步调用,然后将我需要的来自所有商店的数据组合成一个对象,我可以将其设置为我的状态顶级组件。我一直在尝试做的一个当前(可怕的)示例是这样的:

顶层 PostsShowView:

class PostsShow extends React.Component {
  componentWillMount() {
    // this id is populated by react-router when the app hits the /posts/:id route
    PostsActions.get({id: this.props.params.id});

    PostsStore.addChangeListener(this._handlePostsStoreChange);
    SentencesStore.addChangeListener(this._handleSentencesStoreChange);
    GrammarsStore.addChangeListener(this._handleGrammarsStoreChange);
    WordsStore.addChangeListener(this._handleWordsStoreChange);
  }

  componentWillUnmount() {
    PostsStore.removeChangeListener(this._handlePostsStoreChange);
    SentencesStore.removeChangeListener(this._handleSentencesStoreChange);
    GrammarsStore.removeChangeListener(this._handleGrammarsStoreChange);
    WordsStore.removeChangeListener(this._handleWordsStoreChange);
  }

  _handlePostsStoreChange() {
    let posts = PostsStore.getState().posts;
    let post = posts[this.props.params.id];

    this.setState({post: post});

    SentencesActions.fetch({postId: post.id});
  }

  _handleSentencesStoreChange() {
    let sentences = SentencesStore.getState().sentences;

    this.setState(function(state, sentences) {
      state.post.sentences = sentences;
    });

    sentences.forEach((sentence) => {
      GrammarsActions.fetch({sentenceId: sentence.id})
      WordsActions.fetch({sentenceId: sentence.id})
    })
  }

  _handleGrammarsStoreChange() {
    let grammars = GrammarsStore.getState().grammars;

    this.setState(function(state, grammars) {
      state.post.grammars = grammars;
    });
  }

  _handleWordsStoreChange() {
    let words = WordsStore.getState().words;

    this.setState(function(state, words) {
      state.post.words = words;
    });
  }
}

这是我的 PostsActions.js - 其他实体(句子、语法、单词)也有类似的 ActionCreators 以类似的方式工作:

let api = require('api');

class PostsActions {
  get(params = {}) {
    this._dispatcher.dispatch({
      actionType: AdminAppConstants.FETCHING_POST
    });

    api.posts.fetch(params, (err, res) => {
      let payload, post;

      if (err) {
        payload = {
          actionType: AdminAppConstants.FETCH_POST_FAILURE
        }
      }
      else {
        post = res.body;

        payload = {
          actionType: AdminAppConstants.FETCH_POST_SUCCESS,
          post: post
        }
      }

      this._dispatcher.dispatch(payload)
    });
  }
}

主要问题是当在_handlePostsStoreChange 回调中调用SentencesActions.fetch 时,Flux 调度程序抛出“无法在调度中间调度”不变错误,因为 SentencesActions 方法在调度回调之前触发了调度上一个动作结束。

我知道我可以通过使用 _.defersetTimeout 之类的东西来解决这个问题——但这真的感觉就像我只是在这里修补问题。此外,我考虑在操作本身中执行所有这些获取逻辑,但这似乎也不正确,并且会使错误处理更加困难。我将我的每个实体都分离到它们自己的商店和操作中 - 在组件级别中不应该有某种方式来组合我需要从每个实体各自的商店中获得的东西吗?

欢迎任何完成过类似事情的人提出任何建议!

【问题讨论】:

  • 你试过使用waitFor吗? facebook.github.io/flux/docs/dispatcher.html
  • @knowbody 是的,我尝试使用waitFor,但它似乎并没有真正解决问题,因为问题是第二个动作在第一个动作完成之前被调度.但是,也许我对waitFor 的理解是错误的,只是我没有正确使用它?
  • @joeellis:您可以整理一个 jsFiddle 演示,请展示您的问题情况吗?
  • 不看所有代码很难说,但第一次调用 PostActions.get() 是触发全局更改,即触发 _handlePostsStoreChange,然后调用 SentencesActions.fetch()初始调度完成。我会推荐更细粒度的事件,即注册一个“ON_POST_FETCH”事件,在您发出FetchChange()时触发您的加载gif开/关,并注册一个特定的“POST_DATA_CHANGED”事件以响应emitPostDataChange()并调用您的SentencesActions.fetch() .不确定这是否会有所帮助,但我已经通过这种方式解决了类似的问题。

标签: javascript reactjs flux


【解决方案1】:

但是不,在调度过程中创建动作是没有技巧的,这是设计使然。行动不应该是导致变化的事情。它们应该像报纸一样,通知应用程序外部世界的变化,然后应用程序响应该消息。商店本身会引起变化。行动只是通知他们。

还有

组件不应决定何时获取数据。这是视图层中的应用程序逻辑。

Bill Fisher,Flux 的创建者https://stackoverflow.com/a/26581808/4258088

您的组件正在决定何时获取数据。那是不好的做法。 您基本上应该做的是让您的组件通过操作说明它确实需要哪些数据。

商店应负责累积/获取所有需要的数据。不过需要注意的是,在商店通过 API 调用请求数据之后,响应应该触发一个操作,而不是商店直接处理/保存响应。

您的商店可能如下所示:

class Posts {
  constructor() {
    this.posts = [];

    this.bindListeners({
      handlePostNeeded: PostsAction.POST_NEEDED,
      handleNewPost: PostsAction.NEW_POST
    });
  }

  handlePostNeeded(id) {
    if(postNotThereYet){
      api.posts.fetch(id, (err, res) => {
        //Code
        if(success){
          PostsAction.newPost(payLoad);
        }
      }
    }
  }

  handleNewPost(post) {
    //code that saves post
    SentencesActions.needSentencesFor(post.id);
  }
}

那么您需要做的就是聆听商店的声音。还取决于您是否使用框架以及需要哪个框架来发出更改事件(手动)。

【讨论】:

  • 感谢您(以及所有其他答案)。听起来我出错的地方是我假设顶级智能组件应该充当视图控制器类型,即它应该能够针对所有动作采取行动并存储它需要组合/组装它的所有状态子组件需要使用。看来这个想法是错误的,商店应该真正处理这些异步请求。虽然很遗憾,因为我宁愿让这些行为对它们负责,但唉,我想我已经证明,在当前的通量范式下这是不可能的。
【解决方案2】:

我认为你应该有不同的 Store 来反映你的数据模型和一些 POJO 的对象来反映你的对象的实例。因此,您的 Post 对象将有一个 getSentence() 方法,这些方法将依次调用 SentenceStore.get(id) 等。您只需向您的 Post 对象添加一个方法,例如 isReady() 返回 true 或 `false是否已获取所有数据。

这是使用ImmutableJS的基本实现:

PostSore.js

var _posts = Immutable.OrderedMap(); //key = post ID, value = Post

class Post extends Immutable.Record({
    'id': undefined,
    'sentences': Immutable.List(),
}) {

    getSentences() {
        return SentenceStore.getByPost(this.id)
    }

    isReady() {
        return this.getSentences().size > 0;
    }
}

var PostStore = assign({}, EventEmitter.prototype, {

    get: function(id) {
        if (!_posts.has(id)) { //we de not have the post in cache
            PostAPI.get(id); //fetch asynchronously the post
            return new Post() //return an empty Post for now
        }
        return _post.get(id);
    }
})

SentenceStore.js

var _sentences = Immutable.OrderedMap(); //key = postID, value = sentence list

class Sentence extends Immutable.Record({
    'id': undefined,
    'post_id': undefined,
    'words': Immutable.List(),
}) {

    getWords() {
        return WordsStore.getBySentence(this.id)
    }

    isReady() {
        return this.getWords().size > 0;
    }
}

var SentenceStore = assign({}, EventEmitter.prototype, {

    getByPost: function(postId) {
        if (!_sentences.has(postId)) { //we de not have the sentences for this post yet
            SentenceAPI.getByPost(postId); //fetch asynchronously the sentences for this post
            return Immutable.List() //return an empty list for now
        }
        return _sentences.get(postId);
    }
})

var _setSentence = function(sentenceData) {
    _sentences = _sentences.set(sentenceData.post_id, new Bar(sentenceData));
};

var _setSentences = function(sentenceList) {
    sentenceList.forEach(function (sentenceData) {
        _setSentence(sentenceData);
    });
};

SentenceStore.dispatchToken = AppDispatcher.register(function(action) {
    switch (action.type)
    {   
        case ActionTypes.SENTENCES_LIST_RECEIVED:
            _setSentences(action.sentences);
            SentenceStore.emitChange();
            break;
    }
});

WordStore.js

var _words = Immutable.OrderedMap(); //key = sentence id, value = list of words

class Word extends Immutable.Record({
    'id': undefined,
    'sentence_id': undefined,
    'text': undefined,
}) {

    isReady() {
        return this.id != undefined
    }
}

var WordStore = assign({}, EventEmitter.prototype, {

    getBySentence: function(sentenceId) {
        if (!_words.has(sentenceId)) { //we de not have the words for this sentence yet
            WordAPI.getBySentence(sentenceId); //fetch asynchronously the words for this sentence
            return Immutable.List() //return an empty list for now
        }
        return _words.get(sentenceId);
    }

});

var _setWord = function(wordData) {
    _words = _words.set(wordData.sentence_id, new Word(wordData));
};

var _setWords = function(wordList) {
    wordList.forEach(function (wordData) {
        _setWord(wordData);
    });
};

WordStore.dispatchToken = AppDispatcher.register(function(action) {
    switch (action.type)
    {   
        case ActionTypes.WORDS_LIST_RECEIVED:
            _setWords(action.words);
            WordStore.emitChange();
            break;
    }

});

通过这样做,你只需要在你的组件中监听上述存储的变化并编写类似这样的东西(伪代码)

YourComponents.jsx

getInitialState:
    return {post: PostStore.get(your_post_id)}

componentDidMount:
    add listener to PostStore, SentenceStore and WordStore via this._onChange

componentWillUnmount:
    remove listener to PostStore, SentenceStore and WordStore

render:
    if this.state.post.isReady() //all data has been fetched

    else
        display a spinner        

_onChange:
    this.setState({post. PostStore.get(your_post_id)})

当用户点击页面时,PostStore 将首先通过 Ajax 检索 Post 对象,然后由SentenceStoreWordStore 加载所需的数据。由于我们正在听它们,而PostisReady() 方法仅在帖子的句子准备好时返回true,而SentenceisReady() 方法仅在其所有单词已加载时返回true,您无事可做 :) 当您的数据准备好时,只需等待微调器被您的帖子替换!

【讨论】:

    【解决方案3】:

    我不知道你的应用程序状态是如何处理的,但对我来说,当我遇到 Flux 问题时,最有效的系统是将更多状态和更多逻辑移动到存储中。我已经尝试过多次解决这个问题,但它总是会咬我。因此,在最简单的示例中,我将调度一个处理整个请求的操作,以及随之而来的任何状态。这是一个非常简单的示例,它应该与 Flux 框架无关:

    var store = {
      loading_state: 'idle',
      thing_you_want_to_fetch_1: {},
      thing_you_want_to_fetch_2: {}
    }
    
    handleGetSomethingAsync(options) {
      // do something with options
      store.loading_state = 'loading'
      request.get('/some/url', function(err, res) {
        if (err) {
          store.loading_state = 'error';
        } else {
          store.thing_you_want_to_fetch_1 = res.body;
          request.get('/some/other/url', function(error, response) {
            if (error) {
              store.loading_state = 'error';
            } else {
              store.thing_you_want_to_fetch_2 = response.body;
              store.loading_state = 'idle';
            }
          }
        }
      }
    }
    

    然后在您的 React 组件中,您使用 store.loading_state 来确定是渲染某种加载微调器、错误还是正常数据。

    请注意,在这种情况下,操作只是将选项对象传递给 store 方法,然后该方法在一个地方处理与多个请求相关的所有逻辑和状态。

    如果我能更好地解释这些,请告诉我。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-06-10
      • 2020-11-29
      • 2014-06-12
      • 2014-12-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-03-15
      相关资源
      最近更新 更多