Skip to content
 

NgRx: Angular的状态管理利器

更新: 9/4/2025字数: 0 字 时长: 0 分钟

NgRx 是 Angular 应用中基于 Redux 模式的状态管理解决方案,它提供了一套完整的工具集来管理应用状态。

一、NgRx 核心概念

1. 基本架构

NgRx 遵循 Redux 的三大原则:

  • 1. 单一数据源:整个应用状态存储在一个 Store 中
  • 2. 状态只读:只能通过 Action 改变状态
  • 纯函数修改:使用 Reducer 处理状态变更

2. 核心组成部分

概念作用
Store应用状态的容器,提供状态访问和派发 Action 的接口
Action描述状态变更的事件,包含 type 和 payload
Reducer纯函数,接收当前状态和 Action,返回新状态
Selector用于从 Store 中提取特定状态片段
Effect处理副作用(如 API 调用),基于 RxJS

二、基本实现步骤

1. 安装 NgRx

bash
ng add @ngrx/store
ng add @ngrx/effects
ng add @ngrx/store-devtools

2. 定义状态和 Action

typescript
// products.actions.ts
import { createAction, props } from "@ngrx/store";

export const loadProducts = createAction("[Products] Load Products");
export const loadProductsSuccess = createAction("[Products] Load Products Success", props<{ products: Product[] }>());
export const loadProductsFailure = createAction("[Products] Load Products Failure", props<{ error: string }>());

3. 创建 Reducer

typescript
// products.reducer.ts
import { createReducer, on } from "@ngrx/store";
import * as ProductsActions from "./products.actions";

export interface State {
  products: Product[];
  loading: boolean;
  error: string | null;
}

const initialState: State = {
  products: [],
  loading: false,
  error: null
};

export const productsReducer = createReducer(
  initialState,
  on(ProductsActions.loadProducts, (state) => ({
    ...state,
    loading: true,
    error: null
  })),
  on(ProductsActions.loadProductsSuccess, (state, { products }) => ({
    ...state,
    products,
    loading: false
  })),
  on(ProductsActions.loadProductsFailure, (state, { error }) => ({
    ...state,
    loading: false,
    error
  }))
);

4. 注册 Store

typescript
// app.module.ts
import { StoreModule } from "@ngrx/store";
import { EffectsModule } from "@ngrx/effects";

@NgModule({
  imports: [StoreModule.forRoot({ products: productsReducer }), EffectsModule.forRoot([ProductsEffects]), StoreDevtoolsModule.instrument()]
})
export class AppModule {}

三、高级功能实现

1. 使用 Effects 处理异步

typescript
// products.effects.ts
import { Injectable } from "@angular/core";
import { Actions, createEffect, ofType } from "@ngrx/effects";
import { mergeMap, map, catchError } from "rxjs/operators";
import * as ProductsActions from "./products.actions";
import { ProductsService } from "../services/products.service";

@Injectable()
export class ProductsEffects {
  loadProducts$ = createEffect(() =>
    this.actions$.pipe(
      ofType(ProductsActions.loadProducts),
      mergeMap(() =>
        this.productsService.getProducts().pipe(
          map((products) => ProductsActions.loadProductsSuccess({ products })),
          catchError((error) => of(ProductsActions.loadProductsFailure({ error })))
        )
      )
    )
  );

  constructor(private actions$: Actions, private productsService: ProductsService) {}
}

2. 使用 Selectors 查询状态

typescript
// products.selectors.ts
import { createFeatureSelector, createSelector } from "@ngrx/store";
import { State } from "./products.reducer";

const getProductsFeatureState = createFeatureSelector<State>("products");

export const getProducts = createSelector(getProductsFeatureState, (state) => state.products);

export const getLoading = createSelector(getProductsFeatureState, (state) => state.loading);

export const getError = createSelector(getProductsFeatureState, (state) => state.error);

3. 在组件中使用

typescript
// products.component.ts
import { Store } from "@ngrx/store";
import * as fromProducts from "./state/products.selectors";
import * as ProductsActions from "./state/products.actions";

@Component({
  selector: "app-products",
  templateUrl: "./products.component.html"
})
export class ProductsComponent {
  products$ = this.store.select(fromProducts.getProducts);
  loading$ = this.store.select(fromProducts.getLoading);
  error$ = this.store.select(fromProducts.getError);

  constructor(private store: Store) {}

  ngOnInit() {
    this.store.dispatch(ProductsActions.loadProducts());
  }
}

四、最佳实践

1. 状态设计原则

  • 扁平化结构:避免嵌套过深的状态
  • 领域分离:按功能模块划分状态
  • 不可变性:始终返回新对象而非修改原状态

2. 性能优化

typescript
// 使用memoized selectors
export const getFeaturedProducts = createSelector(
  getProducts,
  (products) => products.filter(p => p.featured)
);

// 组件中使用OnPush变更检测策略
@Component({
  // ...
  changeDetection: ChangeDetectionStrategy.OnPush
})

3. 测试策略

typescript
describe("Products Reducer", () => {
  it("should handle loadProducts", () => {
    const action = ProductsActions.loadProducts();
    const state = productsReducer(initialState, action);

    expect(state).toEqual({
      ...initialState,
      loading: true,
      error: null
    });
  });
});

describe("Products Effects", () => {
  let actions$: Observable<any>;
  let effects: ProductsEffects;
  let productsService: MockProductsService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [ProductsEffects, provideMockActions(() => actions$), { provide: ProductsService, useClass: MockProductsService }]
    });

    effects = TestBed.inject(ProductsEffects);
    productsService = TestBed.inject(ProductsService);
  });

  it("should dispatch loadProductsSuccess", () => {
    const products = [{ id: 1, name: "Product" }];
    actions$ = of(ProductsActions.loadProducts());

    effects.loadProducts$.subscribe((action) => {
      expect(action).toEqual(ProductsActions.loadProductsSuccess({ products }));
    });
  });
});

五、常见问题解答

Q1: 什么时候应该使用 NgRx?

  • 应用中存在大量共享状态
  • 状态变更逻辑复杂
  • 需要跟踪状态变化历史
  • 多个组件需要响应同一状态变化

Q2: NgRx 会不会增加太多样板代码?

  • 可以使用@ngrx/entity 简化常见 CRUD 操作
  • 通过代码生成工具减少手动编写
  • 对于简单应用可以考虑轻量级方案如 NgRx Component Store

Q3: 如何调试 NgRx 应用?

  • 使用 Redux DevTools 浏览器插件
  • 利用@ngrx/store-devtools
  • 添加自定义 meta-reducers 记录状态变化

六、实战案例:购物车功能

typescript
// cart.actions.ts
export const addToCart = createAction("[Cart] Add Item", props<{ product: Product }>());

export const removeFromCart = createAction("[Cart] Remove Item", props<{ productId: number }>());

// cart.reducer.ts
export interface CartState {
  items: CartItem[];
  total: number;
}

export const cartReducer = createReducer(
  initialState,
  on(CartActions.addToCart, (state, { product }) => {
    const existingItem = state.items.find((item) => item.product.id === product.id);
    return {
      items: existingItem
        ? state.items.map((item) => (item.product.id === product.id ? { ...item, quantity: item.quantity + 1 } : item))
        : [...state.items, { product, quantity: 1 }],
      total: state.total + product.price
    };
  })
);

// cart.selectors.ts
export const selectCartItems = createSelector(selectCartState, (state) => state.items);

export const selectCartTotal = createSelector(selectCartState, (state) => state.total);

我见青山多妩媚,料青山见我应如是。