我们应该规范化 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
函数内任意深度的结构,将复杂性保持在一个地方。
在我们公司,我们正在开展我们的第一个 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
函数内任意深度的结构,将复杂性保持在一个地方。