Flux/Alt 数据依赖,如何优雅地地处理

Flux/Alt data dependency, how to handle elegantly and idiomatically

我正在使用 alt as my flux implementation for a project and am having trouble wrapping my head around the best way to handle loading stores for two related entities. I'm using sources 功能和 registerAsync 来处理我的 async/api 调用并使用 AltContainer 将它们绑定到我的视图。

我有两个通过 conversationId 一对一关联的实体。两者都是通过 api 调用加载的:

一旦我的作业存储加载了数据,我想填充一个会话存储。

我使用源加载作业存储:

module.exports = {
    fetchJobs() {
        return {
            remote() {
                return axios.get('api/platform/jobs');

            },....

似乎是 waitFor() 方法的工作,但它似乎在一个商店的内容需要转换或与另一个商店的内容合并时使用。我需要根据另一个数据存储的内容获取一个数据存储的内容。

一般来说我需要:

我天真的解决方案是引用作业存储中的对话操作,并在数据到达时分派一个事件。像这样:

var jobActions = require('../actions/Jobs');
var conversationActions = require('../actions/Conversations');

class JobStore {
    constructor() {
        this.bindListeners({
            handlefullUpdate: actions.success
        });...

    }

    handlefullUpdate(jobs) {
        this.jobs = jobs;
        conversationActions.fetch.defer(jobs);
    }
}

当然,这样做违反了商店不应该派发事件的格言,所以我必须在派发过程中使用 defer 来派发一个动作。这对我来说很有意义,因为沿着这条路走下去,我似乎在我的代码中重新引入了各种副作用;失去了 "functional pipelines" 我本应看到的美丽。

此外,我的作业商店必须保存对任何依赖实体的引用,以便它可以调度适当的操作。在这里我只有一个,但我可以想象很多。就实体之间的依赖关系而言,这似乎完全倒退了。

我想到了几个备选方案:

我可以调用 source/action 中的 api/platform/jobs 端点,在那里我获取所有对话,只是为了获取 ID。原来的方法更有效,但这似乎更符合通量的精神,因为我失去了所有的串扰。

我也可以有一个 action/source 来获取两者,返回 {jobs:{}, conversations: in the action}(使用承诺编排那里的依赖)并使用它来填充两个商店。但是这种方法对我来说似乎不必要地复杂(我觉得我不应该这样做!)。

但我是不是错过了另一种方式?如此常见的用例会打破 flux paradim 的优雅,这似乎很奇怪 and/or 迫使我跳过这么多箍。

@dougajmcdonald 提出了一个类似的问题here,但可能措辞过于笼统,没有得到任何关注:

到目前为止,我提出的最佳解决方案(以及示例似乎遵循的解决方案)是向我的作业操作添加一个 "jobsFetched" 操作,并在数据到达时分派它。

var jobActions = require('../actions/Jobs');
var ConversationActions = require('../actions/Conversations');

class JobStore {
    constructor() {
        this.bindListeners({
            handlefullUpdate: jobActions.success...
        });...

    }

    handlefullUpdate(jobs) {
        this.jobs = jobs;
        ConversationActions.jobsFetched.defer(jobs);
    }
}


class ConversationStore {
    constructor() {
        this.bindListeners({
            handleJobUpdate: jobActions.jobsFetched...
        });...

    }

    handleJobUpdate(jobs) {

        /*Now kick off some other action like "fetchConversations" 
          or do the ajax call right here?*/

    }

}

这消除了作业存储必须持有对其所有依赖对象的引用的问题,但我仍然必须从我的存储内部调用一个操作,并且我必须引入 jobsFetched 操作,该操作将 ConversationStore 设置为获取它的数据。所以看起来我不能为我的谈话使用来源。

谁能做得更好?

这绝对是 Flux 设计中的一个缺陷,你是对的,这是一个非常常见的用例。几天来我一直在研究这个问题(以及一些类似的问题),以便在我自己的系统中更好地实施 Flux,虽然肯定有选择,但大多数都有缺点。

最流行的解决方案似乎是使用容器,例如 AltContainer,将从 api 请求数据和从商店加载数据的任务抽象到一个组件中,缺少 ui-逻辑。该容器将负责查看来自商店的信息,并确定是否需要额外的操作来完成这些操作。例如;

static getPropsFromStores() {
    var data = SomeStore.getState();
    if (/*test if data is incomplete */){
        SomeAction.fetchAdditionalData();
    }
    return data;
}

有道理。我们的组件层中有逻辑和组件,这就是 Flux 所说的它所属的地方。

直到你考虑到有两个已安装容器请求相同信息的可能性(并因此复制任何初始化获取,即模型中的对话),如果你添加第三个(或第四个,或第五)访问这些相同商店的容器。

Container 人群对此的固定回应是 "Just don't have more than one!",这是一个很好的建议...如果您的要求uirements 没问题的话。

所以这是我修改后的解决方案,围绕相同的概念;在你的整个应用程序周围(甚至旁边)包括一个 Master Container/Component。与传统容器不同,此 Master Container/Component 将 不会 将存储属性传递给其子容器。相反,它单独负责数据完整性和完成。

这是一个示例实现;

class JobStore {
    constructor() {
        this.bindListeners({
            handleJobUpdate: jobActions.success
        });

    },

    handleJobUpdate(jobs) {
        this.jobs = jobs;            
    }
}

class ConversationStore {
    constructor() {
        this.bindListeners({
            handleJobUpdate: jobActions.success,
            handleConversationUpdate: conversationActions.success
        });

    },

    handleJobUpdate(jobs) {
       this.conversations = {};
       this.tmpFetchInfo = jobs.conversation_id;
       this.isReady = false;

    },

    handleConversationUpdate(conversations) {
       this.conversations = conversations;
       this.tmpFetchInfo = '';
       this.isReady = true;
    }

}

class MasterDataContainer {

    static getPropsFromStores() {
        var jobData = JobStore.getState();
        var conversationData = ConversationStore.getState();

        if (!conversationData.isReady){
            ConversationAction.fetchConversations(conversationData.tmpFetchInfo);
        }  
    },

    render: function(){
        return <div></div>;
    }
}

我一直在查看@gravityplanx 的回答,但不确定它能在多大程度上改善这种情况。

重申并强调:我真的非常喜欢从源加载我的商店的替代模式。每个需要存储的组件看起来像这样:

export default class extends React.Component {
    componentDidMount() {
        CurrentUser.fetch();
        Jobs.fetch(); 
    };
    render() {    
        return(
          <AltContainer stores={{user:CurrentUser, jobs:Jobs}}>
               <Body/>
          </AltContainer>)
    }

}

其用意一目了然。而且我清楚地分离了关注点,使测试我的 actions/stores 和来源变得更加容易。

但是在我的问题的常见用例中,美丽的模式失效了,我最终不得不在我的操作和存储中创建一些相当复杂的管道来协调 ajax 对话调用。另一个答案似乎只是将这种复杂性转移到了别处。我要它消失。

我最后做的是将商店完全分开(问题中建议的第一个解决方案)。我进行了额外的 ajax 调用以从作业中获取 conversationId,并进行了另一个调用以获取 JobConversation 源中的对话。像这样:

  axios.get(window.taxFyleApi + 'api/platform/active-jobs-pick/layerConversation?jobId=' + jobId)
            .then((res)=> {
                let job = res.data[0];  //Returns an array with only one item
                let qBuilder = window.layer.QueryBuilder.messages().forConversation(job.conversationId)...

我保留了将 AltContainer 与我的组件一起使用的好方法,并失去了所有的编排管道。现在,我认为由此产生的清晰度值得额外的后端调用。

我也意识到我 喜欢 它是如何工作的(在符号方面),并且会询问@goatslacker,或者可能会尝试自己做。我希望能够在商店的 exportAsync() 方法中指定依赖项。我很想说:

class JobConvesationStore {
    constructor() {
        this.exportAsync(source, JobStore);
    }

在 JobStore 获得其数据之前,不会调用异步方法 JobConversationStore。意图很容易阅读,不需要复杂的动作编排。