Angular 接入 NGRX 状态管理

Untitled.png

注:图片来自ngrx.io/guide/store

NGRX 是 Angular 实现响应式状态管理的应用框架。

NGRX 状态管理生命周期图中包含了以下元素:

  1. Store:集中的状态存储;
  2. Action:根据用户所触的不同事件执行不同的 Action ;
  3. Reducer:根据不同的 Action 对 Store 中存储的状态做出相应的改变;
  4. Selector:用于获取存储状态切片的纯函数;
  5. Effects:基于流实现的副作用的处理,以减少基于外部交互的状态。

NGRX 状态管理中包含了两条变更状态的主线:

  1. 同步变更状态:用户 => Action => Reducer => Store(State)
  2. 异步变更状态:用户 => 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 核心模块:

  1. @ngrx/store:状态管理核心模块,包含了状态存储、Actions、Reducers、Selectors;
  2. @ngrx/store-devtools:调试的工具,需要配合github.com/reduxjs/red… 使用;
  3. @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(),
},
});
  1. 增加用于添加用户的AddUser ,并使用 props 约束所接收的参数类型;
  2. 增加用于删除用户的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 体验它的作用。

  1. app.component.ts 构造函数中注入 Store:
import { Store } from '@ngrx/store';
export class AppComponent {
// 注入 Store
constructor(private store: Store) {}
}
  1. 让根组件实现 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);
}
}
  1. 定义 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);
}
...
}
  1. 使用管道符在页面渲染 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.tstodo.model.tstodo.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…

posted @   前端小鑫同学  阅读(254)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示