如何无需修改服务器,结合normalizr与ngrx/entity规范化深度嵌套数据
我太懂你这种困扰了——NgRx/Entity确实能大幅简化单实体的Reducer逻辑,但碰到Training -> Exercises -> Sets这种三级嵌套数据时,直接用经典模式写出来的代码会变得无比繁琐,改后端又要额外折腾。用Normalizr来做数据规范化,再配合NgRx/Entity的API,完全是完美的组合,不用动服务器就能搞定。下面是一套具体的落地步骤:
1. 定义Normalizr Schema
首先要为每个层级的实体定义Normalizr的Schema,明确它们的嵌套关系:
import { schema } from 'normalizr'; // 定义Set实体的schema export const setSchema = new schema.Entity('sets'); // 定义Exercise实体,包含sets数组(引用setSchema) export const exerciseSchema = new schema.Entity('exercises', { sets: [setSchema] }); // 定义Training实体,包含exercises数组(引用exerciseSchema) export const trainingSchema = new schema.Entity('trainings', { exercises: [exerciseSchema] });
如果你的实体ID字段不是默认的id(比如服务器返回的是trainingId),可以在schema里指定:
export const trainingSchema = new schema.Entity('trainings', { exercises: [exerciseSchema] }, { idAttribute: 'trainingId' });
2. 规范化API返回的嵌套数据
当你从服务器拿到嵌套的Training数据后,用Normalizr的normalize函数处理,就能得到扁平化的实体集合和关联ID引用:
import { normalize } from 'normalizr'; // 假设这是服务器返回的嵌套数据 const rawTrainingData = { id: 'training-1', name: 'Leg Day', exercises: [ { id: 'exercise-1', name: 'Squats', sets: [ { id: 'set-1', weight: 80, reps: 10 }, { id: 'set-2', weight: 90, reps: 8 } ] } ] }; // 规范化数据 const normalizedData = normalize(rawTrainingData, trainingSchema); // 规范化后的数据结构: // { // result: 'training-1', // entities: { // trainings: { 'training-1': { id: 'training-1', name: 'Leg Day', exercises: ['exercise-1'] } }, // exercises: { 'exercise-1': { id: 'exercise-1', name: 'Squats', sets: ['set-1', 'set-2'] } }, // sets: { // 'set-1': { id: 'set-1', weight: 80, reps: 10 }, // 'set-2': { id: 'set-2', weight: 90, reps: 8 } // } // } // }
3. 配置NgRx Entity状态
接下来在NgRx里为每个实体定义EntityState,然后整合到根状态中:
import { EntityState, createEntityAdapter } from '@ngrx/entity'; // Set实体的状态 export interface Set { id: string; weight: number; reps: number; } export interface SetState extends EntityState<Set> {} export const setAdapter = createEntityAdapter<Set>(); export const initialSetState = setAdapter.getInitialState(); // Exercise实体的状态 export interface Exercise { id: string; name: string; sets: string[]; // 现在是Set的ID数组,不是嵌套对象 } export interface ExerciseState extends EntityState<Exercise> {} export const exerciseAdapter = createEntityAdapter<Exercise>(); export const initialExerciseState = exerciseAdapter.getInitialState(); // Training实体的状态 export interface Training { id: string; name: string; exercises: string[]; // 现在是Exercise的ID数组 } export interface TrainingState extends EntityState<Training> {} export const trainingAdapter = createEntityAdapter<Training>(); export const initialTrainingState = trainingAdapter.getInitialState(); // 根状态 export interface AppState { trainings: TrainingState; exercises: ExerciseState; sets: SetState; }
4. 在Reducer中处理规范化数据
在对应的Reducer里,用EntityAdapter的方法(比如setAll、addOne)来更新状态,直接用Normalizr返回的entities数据即可:
import { createReducer, on } from '@ngrx/store'; import * as TrainingActions from './training.actions'; export const trainingReducer = createReducer( initialTrainingState, on(TrainingActions.loadTrainingSuccess, (state, { normalizedData }) => { // 更新Training实体 return trainingAdapter.setAll( [normalizedData.entities.trainings[normalizedData.result]], state ); }) ); // Exercise的Reducer同理 export const exerciseReducer = createReducer( initialExerciseState, on(TrainingActions.loadTrainingSuccess, (state, { normalizedData }) => { return exerciseAdapter.setAll( Object.values(normalizedData.entities.exercises), state ); }) ); // Set的Reducer export const setReducer = createReducer( initialSetState, on(TrainingActions.loadTrainingSuccess, (state, { normalizedData }) => { return setAdapter.setAll( Object.values(normalizedData.entities.sets), state ); }) );
如果是批量加载多个Training,直接用normalizedData.entities.trainings传入setAll即可,不用再手动处理嵌套。
5. 编写关联数据的选择器
用NgRx/Entity提供的getSelectors创建基础选择器,然后组合出能获取完整关联数据的选择器,方便组件使用:
import { createFeatureSelector, createSelector } from '@ngrx/store'; import { trainingAdapter, TrainingState } from './training.state'; import { exerciseAdapter } from './exercise.state'; import { setAdapter } from './set.state'; // 基础选择器 const selectTrainingState = createFeatureSelector<TrainingState>('trainings'); const { selectAll: selectAllTrainings, selectById: selectTrainingById } = trainingAdapter.getSelectors(selectTrainingState); const selectExerciseState = createFeatureSelector<ExerciseState>('exercises'); const { selectById: selectExerciseById } = exerciseAdapter.getSelectors(selectExerciseState); const selectSetState = createFeatureSelector<SetState>('sets'); const { selectById: selectSetById } = setAdapter.getSelectors(selectSetState); // 组合选择器:获取单个Training及其关联的Exercises和Sets export const selectTrainingWithDetails = (trainingId: string) => createSelector( selectTrainingById(trainingId), selectExerciseState, selectSetState, (training, exerciseState, setState) => { if (!training) return null; // 把Exercise ID转成完整的Exercise对象,同时关联Sets const exercisesWithSets = training.exercises.map(exerciseId => { const exercise = exerciseAdapter.getSelectors().selectById(exerciseState)(exerciseId); if (!exercise) return null; const sets = exercise.sets.map(setId => setAdapter.getSelectors().selectById(setState)(setId)); return { ...exercise, sets }; }).filter(Boolean); return { ...training, exercises: exercisesWithSets }; } );
这样在组件里,你只需要调用这个组合选择器,就能拿到包含完整嵌套关系的Training数据,同时底层状态还是扁平化的,完全享受NgRx/Entity的性能和便捷性。
一些额外的小提示
- 处理更新/删除操作时,只需要操作对应的实体即可,关联的ID引用已经存在状态里,不用手动维护嵌套关系;
- 如果服务器返回的是数组(比如多个Training),直接把schema改成
[trainingSchema]传入normalize函数即可; - 可以把Normalizr的规范化逻辑封装到NgRx的Effect里,这样组件和Reducer都不用处理原始数据,直接拿到规范化后的结果。
内容的提问来源于stack exchange,提问作者Street0

