我们应该规范化 angular 应用程序中的状态吗?

Should we normalize the state in an angular app?

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

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

Example: Let's say we have a feature store called customer. Inside this we store the customer-list and the selected customer. One point: The selected customer object has much more properties than the customer objects inside the customer-list.

We display the list of customers (model: CustomerList) from the store in a component, and if the user clicks on an entry he is redirected to the detail page where customer details (model: CustomerDetail) are displayed.

Inside the details page the user is able to create/edit/delete all the customer-sublists (like addresses, phones, faxes, etc.). If the detail page of a specific customer is closed and the user is back on the list, the store object of the customer should be cleared.

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 中重新获取新的地址列表,并将其放入商店内客户的地址列表中。

有人认为非规范化状态的最大缺点是:

The deeper the nesting, the longer and more complex are the mappings inside the store to set or get data to/from the store.

其他人争辩说,如果我们对状态进行某种规范化,以便在客户对象内部没有嵌套,我们将如何从中受益,因为在我们的特殊情况下,customerList 对象和 customerDetail 对象不同于彼此,否则如果两个对象相同,我们可以简单地将 selected 客户的 ID 存储在商店内,并通过 id select 客户列表中的数据。

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

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

tl;dr

你们有什么看法/您对这家商店的体验如何(标准化与否),我们如何才能两全其美?如有任何回复,我们将不胜感激!

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

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

在我们的复杂应用程序中,我们选择(主要)使用嵌套选项,但为了帮助保持 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 操作中使用它来更新单个公司,如下所示:

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 函数内任意深度的结构,将复杂性保持在一个地方。