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

IT技术 javascript reactjs flux
2021-05-18 22:23:08

我正在使用 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)
    });
  }
}

主要问题是 Flux 调度器SentencesActions.fetch_handlePostsStoreChange回调中调用抛出“无法在调度中间调度”不变错误,因为 SentencesActions 方法在前一个操作的调度回调完成之前触发了调度。

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

接受任何已经完成类似事情的人的任何建议!

3个回答

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

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

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

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

我认为你应该有不同的 Store 来反映你的数据模型和一些 POJO 的对象来反映你的对象的实例。因此,您的Post对象将有一个getSentence()方法,方法依次调用SentenceStore.get(id)等。您只需要添加一个方法,例如isReady()向您的Post对象返回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;
    }

});

通过这样做,您只需要在您的组件中监听上述商店的变化并编写这样的东西(伪代码)

你的组件.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因为我们在听他们的isReady()方法Post只有返回true时,这篇文章的句子都准备好了,isReady()方法Sentence只有返回true时,它的所有词都被加载,你什么都没有做:)只是等待被你的帖子的时候被替换的微调您的数据已准备就绪!

我不知道您的应用程序状态是如何处理的,但对我来说,当我遇到 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 方法,然后该方法在一个地方处理与多个请求关联的所有逻辑和状态。

如果我能更好地解释其中任何一个,请告诉我。