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

IT技术 javascript node.js reactjs flux
2021-05-07 20:55:07

我正在使用alt作为我的项目的通量实现,并且无法围绕处理两个相关实体的加载存储的最佳方法来解决问题。我正在使用功能和 registerAsync 来处理我的异步/api 调用并使用 AltContainer 将它们绑定到我的视图。

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

在此处输入图片说明

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

我使用源加载作业存储:

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

            },....

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

一般来说,我需要:

  • 调用第三方 API 并将实体列表加载到商店中。
  • 当该数据到达时,我需要使用上述每个属性来调用另一个 API 并将该数据加载到另一个存储中。

我天真的解决方案是从作业存储中引用对话操作并在数据到达时调度事件。像这样的东西:

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 在分派过程中分派一个动作。这对我来说很有意义,因为沿着这条路走下去,我似乎在我的代码中重新引入了各种副作用;失去了我应该看到的“功能管道”的美感。

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

想到了几个替代方案:

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

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

但我错过了另一种方式吗?这样一个常见的用例会打破通量范式的优雅和/或迫使我跳过这么多圈子,这似乎很奇怪。

@dougajmcdonald在这里提出了一个类似的问题,但可能措辞过于笼统,并没有引起任何关注:

3个回答

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

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

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

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

直到您考虑可能有两个挂载的容器请求相同的信息(从而复制任何初始化获取,即模型中的对话),并且如果您添加第三个(或第四个或第五个)容器访问,问题只会变得更糟那些相同的商店。

Container 人群对此的回答是“不要超过一个!”,这是一个很好的建议……如果您的要求没问题。

所以这是我改变的解决方案,围绕相同的概念;在整个应用程序周围(甚至在它旁边)包括一个主容器/组件。与传统容器不同,此主容器/组件不会将存储属性传递给其子项。相反,它全权负责数据完整性和完整性。

这是一个示例实现;

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

到目前为止,我提供的最佳解决方案(以及示例似乎遵循的解决方案)是向我的作业操作添加一个“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?*/

    }

}

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

谁能做得更好?

我一直在查看@gravityplanx 的答案,但不确定它对情况的改善程度。

重申和放大:我真的非常喜欢从源加载我的商店的替代模式。每个需要 store(s) 的组件看起来像这样:

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

}

意图一目了然。我得到了清晰的关注点分离,从而更容易测试我的操作/存储和来源。

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

我最终做的是将商店完全分开(问题中建议的第一个解决方案)。我进行了额外的 ajax 调用以从作业中获取对话 ID,并进行另一个调用以获取 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 与我的组件一起使用的好方法,并丢失了所有的编排管道。现在,我认为由此产生的清晰度值得额外的后端调用。

我也知道我会怎么LIKE它的工作(在符号而言),并且会问@goatslacker一下,或者可以采取射击在做我自己。我希望能够在商店的 exportAsync() 方法中指定依赖项。我很想能够说:

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

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