【问题标题】:Should we normalize the state in an angular app?我们应该在 Angular 应用程序中标准化状态吗?
【发布时间】:2019-09-08 16:47:15
【问题描述】:

在我们公司,我们正在开发我们的第一个 Angular 项目,包括 ngrx-store,我们开始讨论是否应该对状态进行规范化 - 这个问题的含义不同。

有些人认为嵌套存储更容易维护/使用,因为 api 已经发送嵌套数据,并且在某些情况下应该从存储中清除整个对象。

示例:假设我们有一个名为customer的功能商店。在其中我们存储客户列表和选定的客户。一点:选定的客户对象比客户列表中的客户对象具有更多的属性。

我们在一个组件中显示来自商店的客户列表(模型:CustomerList),如果用户点击一个条目,他将被重定向到详细页面,其中客户详细信息(模型:CustomerDetail)。

在详细信息页面中,用户可以创建/编辑/删除所有客户子列表​​(如地址、电话、传真等)。 如果某个特定客户的详情页被关闭,并且该用户又回到了列表中,则该客户的商店对象应该被清除。

export interface CustomerState {
    customers: CustomerList[],
    customer: CustomerDetail
};

export interface CustomerList {
    customerId: number;
    name: string;
};

export interface CustomerDetail {
    customerId: number;
    firstName: string;
    lastName: string;
    addressList: CustomerDetailAddressList[];
    phoneList: CustomerDetailPhoneList[];
    emailList: CustomerDetailEmailList[];
    faxList: CustomerDetailFaxList[];
    /* ... */
};

如果用户现在,假设在特定客户详细信息页面上为客户创建新地址,新地址将发布到 api,并且在 api 成功响应后,商店再次从 api 重新获取新地址列表并放入店内顾客的addressList中。

有些人认为非标准化状态的最大缺点如下:

嵌套越深,存储内部用于设置或从存储中获取数据的映射就越长且越复杂。

其他人争辩说,如果我们对状态进行规范化,以便在客户对象内部没有嵌套,我们将如何从中受益,因为在我们的特殊情况下,客户列表对象和客户细节对象彼此不同,否则如果两个对象相同,我们只需将所选客户的 id 存储在商店中,并通过 id 从客户列表中选择数据。

他们在讨论中带来的另一个缺点是,如果用户离开客户详细信息页面并且状态被规范化,则不仅需要清除客户对象,还需要清除所有其他涉及客户的列表。

export interface CustomerState {
    customers: CustomerList[],
    customer: CustomerDetail,
    customerAddressList: CustomerDetailAddressList[];
    customerPhoneList: CustomerDetailPhoneList[];
    customerEmailList: CustomerDetailEmailList[];
    customerFaxList: CustomerDetailFaxList[];
};

tl;dr

你们怎么看/你们在这家商店的体验如何(正常化与否),我们如何才能两全其美?任何回应将不胜感激!

如果有些事情不太清楚或根本没有意义,请告诉我 - 我们仍在学习 - 非常感谢任何帮助和/或建议。

【问题讨论】:

    标签: angular typescript ngrx ngrx-store


    【解决方案1】:

    您已经在问题中很好地讨论了这两种方法的优缺点 - 这确实是两个选项之间的折腾 - 存储单个对象的好处意味着更新会在引用该对象的所有地方流动,但是它使你的减速器更加复杂。

    在我们复杂的应用程序中,我们选择(大部分)使用嵌套选项,但为了帮助保持 reducer 实现更简洁,我们编写了纯函数运算符来更新(例如)公司:

    function applyCompanyEdit(state: CompanyState, compId: number, compUpdate: (company: CompanyFull) => CompanyFull): CompanyState {
        let tab = state.openCompanies.find((aTab) => aTab.compId == compId);
        if (!tab) return state;
        let newCompany = compUpdate(tab.details);
        // Set company edited if the new company returned by compUpdate are not the exact same
        // object as the previous state.
        let compEdited = tab.edited || tab.details != newCompany;
    
        return {
            ...state,
            openCompanies: state.openCompanies.map((ct) => {
                if (ct.compId == compId) return {
                    ...ct,
                    edited: compEdited,
                    details: newCompany
                };
                return ct;
            })
        };
    }
    

    ...然后我们在多个 reducer 操作中使用它来更新单个 Company,如下所示:

    case CompanyStateService.UPDATE_SHORTNAME: 
        return applyCompanyEdit(state, action.payload.compId, (comp: CompanyFull) => {
            return {
                ...comp,
                shortName: action.payload.shortName
            }
        });
    case CompanyStateService.UPDATE_LONGNAME: 
        return applyCompanyEdit(state, action.payload.compId, (comp: CompanyFull) => {
            return {
                ...comp,
                longName: action.payload.longName
            }
        });
    

    这对我们来说非常有效,因为 reducer 操作保持非常简洁和易于理解,而且我们只需要编写笨拙的函数来查找要更新的正确对象。在这种情况下,嵌套相对较浅,但它可以扩展到 applyCompanyEdit 函数中的任意深度结构,将复杂性保持在一个地方。

    【讨论】:

    • 感谢马克的回答!很高兴听到我们不是唯一进行这次谈话的人。纯函数的东西看起来很有前途——我会和我的团队成员讨论这个!
    • @fen89 你好!我们在当前项目中遇到了同样的问题,您能分享一下您做出的决定吗?
    • @Massimo 抱歉回复晚了,不知怎的错过了!我们选择了“标准化和尽可能平坦的状态”解决方案(例如medium.com/@timdeschryver/ngrx-normalizing-state-d3960a86a3aa)。但最终还是不满意,但这是我们有点不喜欢的减速器的问题。我们遇到了来自 Net Basal 的 'Akita' (github.com/datorama/akita) 并完全爱上了它——我们现在几乎到处都在使用它,并且完全放弃了 ngrx!
    猜你喜欢
    • 1970-01-01
    • 2010-11-22
    • 2016-07-25
    • 1970-01-01
    • 2021-08-13
    • 1970-01-01
    • 2010-10-30
    • 2012-12-23
    • 1970-01-01
    相关资源
    最近更新 更多