如何处理嵌套的api调用

时间:2015-08-27 03:40:59

标签: javascript reactjs flux

我正在使用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 - 其他实体(句子,语法,单词)也有类似的ActionCreator以类似的方式工作:

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

主要问题是Flux调度员抛出一个"无法在调度过程中调度"在SentencesActions.fetch回调中调用_handlePostsStoreChange时出现不变的错误,因为SentencesActions方法在前一个操作的调度回调完成之前触发调度。

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

向任何已完成类似工作的人提供任何建议!

3 个答案:

答案 0 :(得分:5)

  

但不,没有黑客在发送过程中创建一个动作,这是设计的。行动不应该是引起变化的事情。它们应该像报纸一样通知应用程序外部世界的变化,然后应用程序响应该新闻。商店本身会引起变化。行动只是通知他们。

另外

  

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

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

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

答案 1 :(得分:2)

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

以下是使用ImmutableJS的基本实现:

PostSore.js

true

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

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

通过执行此操作,您只需要听取上面存储组件中的更改并编写类似这样的内容(伪代码)

YourComponents.jsx

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

});

当用户点击页面时,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)}) 将首先通过Ajax检索Post对象,并且PostStoreSentenceStore将加载所需的数据。由于我们正在收听它们,而WordStore isReady()方法仅在帖子的句子准备好后才会返回Posttrue方法会isReady()只有在加载完所有单词后才返回Sentence,你无事可做:)只需等待数据准备就绪后用你的帖子替换微调器!

答案 2 :(得分:0)

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

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

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