You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何无需修改服务器,结合normalizr与ngrx/entity规范化深度嵌套数据

结合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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 07:35:10