Angular 接入 NGRX 状态管理
注:图片来自ngrx.io/guide/store
NGRX 是 Angular 实现响应式状态管理的应用框架。
NGRX 状态管理生命周期图中包含了以下元素:
- Store:集中的状态存储;
- Action:根据用户所触的不同事件执行不同的 Action ;
- Reducer:根据不同的 Action 对 Store 中存储的状态做出相应的改变;
- Selector:用于获取存储状态切片的纯函数;
- Effects:基于流实现的副作用的处理,以减少基于外部交互的状态。
NGRX 状态管理中包含了两条变更状态的主线:
- 同步变更状态:
用户
=>Action
=>Reducer
=>Store(State)
; - 异步变更状态:
用户
=>Action
=>Effects
=>Service
=>Effects
=>Action
=>Reducer
=>Store(State)
;
快速开始
创建 Angular 项目:
安装并执行 CLI 创建 Angular 项目
# 基于 Angular 17 版本演示 # 注意要将 Nodejs 版本切换至 18.13+ npm install -g @angular/cli # 创建为 standalone 类型的项目 ng new angular-ngrx --standalone=false
安装 NGRX 核心模块:
- @ngrx/store:状态管理核心模块,包含了状态存储、Actions、Reducers、Selectors;
- @ngrx/store-devtools:调试的工具,需要配合github.com/reduxjs/red… 使用;
- @ngrx/schematics:提供使用 NGRX 的 CLI 命令,需要与 Angular 进行整合使用;
安装命令:
npm install @ngrx/store --save npm install @ngrx/store-devtools --save npm install @ngrx/schematics --save-dev
更新 angular.json:
{ "cli": { "schematicCollections": ["@ngrx/schematics"] } }
创建存储 State 的 Store:
选项介绍:
选项 | 作用 |
---|---|
--root | 目标模块为根模块时设置 |
--module | 提供目标模块的路径 |
--state-path | 提供 State 存储的路径 |
--state-interface | 提供 State 接口名称 |
示例命令:
ng generate store State --root --module=app.module.ts --state-path=store --state-interface AppState
生成 app/store/index.ts
并更新了 app.module.ts
:
import { isDevMode } from '@angular/core'; import { ActionReducer, ActionReducerMap, createFeatureSelector, createSelector, MetaReducer } from '@ngrx/store'; export interface AppState {} export const reducers: ActionReducerMap<AppState> = {}; export const metaReducers: MetaReducer<AppState>[] = isDevMode() ? [] : [];
@NgModule({ imports: [ ... StoreModule.forRoot(reducers, { metaReducers }), StoreDevtoolsModule.instrument(), ], ... }) export class AppModule {}
创建用于添加和删除用户的 Action:
示例命令:
ng generate action store/actions/user
正生成的 app/store/actions/user.actions.ts
模版代码中作以下更改:
import { createActionGroup, emptyProps, props } from '@ngrx/store'; export const UserActions = createActionGroup({ source: 'User', events: { AddUser: props<{ name: string; age: number; gender: string }>(), DelUser: emptyProps(), }, });
- 增加用于添加用户的
AddUser
,并使用props
约束所接收的参数类型; - 增加用于删除用户的
DelUser
,并使用emptyProps
表示不传递任何参数(仅存储一位用户);
创建根据 Action 来更新状态的 Reducer:
选项介绍:
选项 | 作用 |
---|---|
--reducers | 执行reducers存放路径,约定路径为上一级的 index.ts,也是 store 创建的文件 |
--skip-tests | 跳过生成测试文件 |
示例命令:
ng generate reducer store/reducers/user --reducers=../index.ts --skip-tests
生成 app/store/reducers/user.reducer.ts
并更新 app/store/index.ts
:
import { createReducer, on } from '@ngrx/store'; import { UserActions } from './user.actions'; export const userFeatureKey = 'user'; export interface State {} export const initialState: State = {}; export const reducer = createReducer( initialState, );
import { isDevMode } from '@angular/core'; import { ActionReducer, ActionReducerMap, createFeatureSelector, createSelector, MetaReducer } from '@ngrx/store'; import * as fromUser from './reducers/user.reducer'; export interface AppState { [fromUser.userFeatureKey]: fromUser.State; } export const reducers: ActionReducerMap<AppState> = { [fromUser.userFeatureKey]: fromUser.reducer, }; export const metaReducers: MetaReducer<AppState>[] = isDevMode() ? [] : [];
添加核心更改状态的代码到 app/store/reducers/user.reducer.ts
:
import { createReducer, on } from '@ngrx/store'; import { UserActions } from '../actions/user.actions'; export const userFeatureKey = 'user'; // 定义 State 接口 export interface State { id: string; name: string; age: number; gender: string; } // 申明 State 的初始状态 export const initialState: State = { id: '', name: '', age: 0, gender: '', }; export const reducer = createReducer( initialState, // 监听 UserActions 中的 addUser 事件并更新状态 on(UserActions.addUser, (state, action) => ({ id: '', name: action.name, age: action.age, gender: action.gender, })), // 监听 UserActions 中的 delUser 事件并更新状态 on(UserActions.delUser, (state, action) => ({ id: '', name: '', age: 0, gender: '', })) );
创建获取状态的使用的 Selector:
示例命令:
ng generate selector store/selectors/user --skip-tests
生成的 app/store/selectors/user.selectors.ts
仅包含导入模块的一行代码:
import { createFeatureSelector, createSelector } from '@ngrx/store';
使用导入的函数创建适用于 User 的 Selector:
import { createFeatureSelector, createSelector } from '@ngrx/store'; import { State, userFeatureKey } from '../reducers/user.reducer'; /** * 用于获取 User */ export const selectUser = createFeatureSelector<State>(userFeatureKey); /** * 用于获取 User 的 name */ export const selectUserName = createSelector( selectUser, (state: State) => state.name );
进入模拟场景:
模拟这样一个场景:在组件加载完成后首先执行添加 User 的 Action,在 5 秒之后执行删除 User 的 Action,用来模拟 User 数据状态的变化,并将 User 绑定到页面用来观察,最后切换不用的 Selector 体验它的作用。
- 在
app.component.ts
构造函数中注入 Store:
import { Store } from '@ngrx/store'; export class AppComponent { // 注入 Store constructor(private store: Store) {} }
- 让根组件实现 OnInit 接口,按模拟场景通过 store 触发 action:
export class AppComponent implements OnInit { title = 'angular-ngrx'; constructor(private store: Store) {} ngOnInit(): void { // 添加用户 this.store.dispatch( UserActions.addUser({ name: 'xiao zhang', age: 18, gender: 'male', }) ); // 删除用户 setTimeout(() => { this.store.dispatch(UserActions.delUser()); }, 5000); } }
- 定义 User (Observable类型)属性,并通过 selectUser 获取到用户数据状态:
export class AppComponent implements OnInit { title = 'angular-ngrx'; user: Observable<{ id: string; name: string; age: number; gender: string; }>; constructor(private store: Store) { this.user = this.store.select(selectUser); } ... }
- 使用管道符在页面渲染 Observable 类型 User:
<div class="content"> {{ user | async | json }} </div>
接入副作用
通过接入副作用(effects)来完成异步获取网络数据更新状态。
安装 effects 核心模块:
npm install @ngrx/effects --save
创建 User 的副作用:
选项介绍 :
选项 | 作用 |
---|---|
--root | 目标模块为根模块时设置 |
--module | 提供目标模块的路径 |
--skip-tests | 跳过生成测试文件 |
示例命令:
ng generate effect store/effects/user --root --module=app.module.ts --skip-tests
创建 app/store/effects/user.effects.ts
并更新 app.module.ts
:
import { Injectable } from '@angular/core'; import { Actions, createEffect } from '@ngrx/effects'; @Injectable() export class UserEffects { constructor(private actions$: Actions) {} }
import { EffectsModule } from '@ngrx/effects'; import { UserEffects } from './store/effects/user.effects'; @NgModule({ ... imports: [ ... EffectsModule.forRoot([UserEffects]), ], }) export class AppModule {}
编写 Test User Api:
执行 ng 命令生成 User 服务:
ng g service services/user --skip-tests
编写用来模拟网络获取用户数据的异步函数 updateApi :
import { Injectable } from '@angular/core'; import { Observable, map, timer } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class UserService { constructor() {} updateApi(): Observable<{ name: string; age: number; gender: string; }> { return timer(3000).pipe( map(() => ({ name: 'xiao li', age: 23, gender: 'male', })) ); } }
添加新的 Actions:
这里的 UpdateUser 同样是 emptyProps,仅作为触发使用,更新用户数据在接下来的副作用编写中会体现:
import { createActionGroup, emptyProps, props } from '@ngrx/store'; export const UserActions = createActionGroup({ source: 'User', events: { ... UpdateUser: emptyProps(), }, });
完成副作用编写:
在 UserEffects
中注入 UserService
后开始创建副作用,总共 4 步操作:
import { UserService } from './../../services/user.service'; import { Injectable } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { UserActions } from '../actions/user.actions'; import { exhaustMap, map } from 'rxjs'; @Injectable() export class UserEffects { updateUser$ = createEffect(() => { return this.actions$.pipe( // 设置副作用所关联的 Action ofType(UserActions.updateUser), // 处理副作用 exhaustMap(() => { // 调用服务,获取用户数据 return this.userService.updateApi().pipe( map((user) => { // 将得到的用户数据通过 AddUser Action 发送出去 return UserActions.addUser(user); }) ); }) ); }); constructor(private actions$: Actions, private userService: UserService) {} }
进入模拟场景:
在组件加载完的 5 秒后,用户数据的状态被清空,紧接着就执行 UpdateUser Action,来获取网络上的用户数据:
export class AppComponent implements OnInit { ... ngOnInit(): void { // 添加用户 this.store.dispatch( UserActions.addUser({ name: 'xiao zhang', age: 18, gender: 'male', }) ); // 删除用户 setTimeout(() => { this.store.dispatch(UserActions.delUser()); this.store.dispatch(UserActions.updateUser()); }, 5000); } }
PS:以上案例完整代码可访问 github.com/OSpoon/angu…
接入实体
实体的引入对应单个用户状态的管理来说起到的效果并不明显,所以你可以将代码回退到最初的状态,实现一个接入实体更加贴切的案例 — TodoList。
初始化项目:
创建新项目并安装依赖:
ng new angular-ngrx-todolist --standalone=false npm install @ngrx/store @ngrx/store-devtools --save npm install @ngrx/schematics --save-dev # 安装接入实体的依赖 npm install @ngrx/entity --save # 实现 uuid 生成 npm install uuid --save npm install @types/uuid --save-dev
更新 angular.json:
{ "cli": { "schematicCollections": ["@ngrx/schematics"] } }
创建存储 State 的 Store:
ng generate store State --root --module=app.module.ts --state-path=store --state-interface AppState
创建实体:
选项介绍:
选项 | 作用 |
---|---|
--reducers | 执行reducers存放路径,约定路径为上一级的 index.ts,也是 store 创建的文件 |
--skip-tests | 跳过生成测试文件 |
示例命令:
ng generate entity store/todo/todo --reducers=../index.ts --skip-tests
PS:生成的模版代码包括了todo.actions.ts
、todo.model.ts
、todo.reducer.ts
,同时也更新了 app/store/index.ts
:
接入实体的代码在 todo.reducer.ts
文件中体现,下面是接入实体的核心部分,更多的适配器操作可以看文件中默认生成的模板代码:
// 1. 将 State 集成自 EntityState export interface State extends EntityState<Todo> { // additional entities state properties } // 2. 创建后续对象操作的适配器 export const adapter: EntityAdapter<Todo> = createEntityAdapter<Todo>(); // 3. 使用创建好的适配器初始化 initialState export const initialState: State = adapter.getInitialState({ // additional entity state properties });
完善 TodoList 功能:
增加 action:
add() { this.store.dispatch( TodoActions.addTodo({ todo: { id: uuidv4(), content: this.content, }, }) ); this.content = ''; }
删除 action:
del(todo: Todo) { this.store.dispatch(TodoActions.deleteTodo({ id: todo.id })); }
清空 action:
clears() { this.store.dispatch(TodoActions.clearTodos()); }
使用实体提供的 Selector 获取状态:
export class AppComponent { todos: Observable<Todo[]>; total: Observable<number>; constructor(private store: Store) { this.todos = this.store.select(selectAll); this.total = this.store.select(selectTotal); } ... }
小结:通过接入实体,可以使用其内置的适配器对 Todo 进行添加、更新、删除、批量添加、批量更新、批量删除、清空等操作,还可以通过其内置的 Selector 方便的获取 Todos 数据,数据的长度等等信息,可以简化一大部分的开发时间。
PS:以上案例使用 Zorro 组件库,完整代码可访问 github.com/OSpoon/angu…
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)