如何处理不断变化的嵌套 api 调用

How to handle nested api calls in flux

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

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

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

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

顶级 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 调度程序抛出一个 "Cannot dispatch in the middle of a dispatch" 不变错误,因为 SentencesActions 方法在调度回调之前触发调度前面的动作结束。

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

乐于听取已完成类似事情的任何人的任何建议!

But no, there is no hack to create an action in the middle of a dispatch, and this is by design. Actions are not supposed to be things that cause a change. They are supposed to be like a newspaper that informs the application of a change in the outside world, and then the application responds to that news. The stores cause changes in themselves. Actions just inform them.

还有

Components should not be deciding when to fetch data. This is application logic in the view layer.

Bill Fisher,Flux 的创造者

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

商店应负责 accumulating/fetching 所有需要的数据。值得注意的是,在存储通过 API 调用请求数据后,响应应该触发一个动作,而不是直接存储 handling/saving 响应。

您的商店可能看起来像这样:

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

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

我认为你应该有不同的 Store 来反映你的数据模型和一些 POJO 的对象来反映你的对象的实例。因此,您的 Post 对象将有一个 getSentence() 方法,这些方法依次调用 SentenceStore.get(id) 等。您只需要向 [=14] 添加一个方法,例如 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对象,然后SentenceStore和[=22加载需要的数据=].由于我们正在听他们和 PostisReady() 方法只有 returns true 当 post 的句子准备好时, isReady() 方法Sentence 只有 returns true 当它的所有单词都已加载时,您无事可做 :) 只需等待微调器被您的 post 替换,当您的数据准备好了!

我不知道你的应用程序状态是如何处理的,但对我来说,当我遇到 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 来确定是否呈现某种加载微调器、错误或正常数据。

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

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