主题
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-devtools2. 定义状态和 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);