Angular 接入 NGRX 状态管理

注:图片来自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 项目

bash 复制代码
# 基于 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 进行整合使用;

安装命令:

bash 复制代码
npm install @ngrx/store --save
npm install @ngrx/store-devtools --save
npm install @ngrx/schematics --save-dev

更新 angular.json:

json 复制代码
{
    "cli": {
        "schematicCollections": ["@ngrx/schematics"]
    }
}

创建存储 State 的 Store:

选项介绍:

选项 作用
--root 目标模块为根模块时设置
--module 提供目标模块的路径
--state-path 提供 State 存储的路径
--state-interface 提供 State 接口名称

示例命令:

bash 复制代码
ng generate store State --root --module=app.module.ts --state-path=store --state-interface AppState

生成 app/store/index.ts 并更新了 app.module.ts

tsx 复制代码
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() ? [] : [];
tsx 复制代码
@NgModule({
  imports: [
    ...
    StoreModule.forRoot(reducers, { metaReducers }),
    StoreDevtoolsModule.instrument(),
  ],
	...
})
export class AppModule {}

创建用于添加和删除用户的 Action:

示例命令:

bash 复制代码
ng generate action store/actions/user

正生成的 app/store/actions/user.actions.ts 模版代码中作以下更改:

tsx 复制代码
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 跳过生成测试文件

示例命令:

bash 复制代码
ng generate reducer store/reducers/user --reducers=../index.ts --skip-tests

生成 app/store/reducers/user.reducer.ts 并更新 app/store/index.ts

tsx 复制代码
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,
);
tsx 复制代码
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

tsx 复制代码
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:

示例命令:

bash 复制代码
ng generate selector store/selectors/user --skip-tests

生成的 app/store/selectors/user.selectors.ts 仅包含导入模块的一行代码:

tsx 复制代码
import { createFeatureSelector, createSelector } from '@ngrx/store';

使用导入的函数创建适用于 User 的 Selector:

tsx 复制代码
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:
tsx 复制代码
import { Store } from '@ngrx/store';

export class AppComponent {

	// 注入 Store
  constructor(private store: Store) {}
}
  1. 让根组件实现 OnInit 接口,按模拟场景通过 store 触发 action:
tsx 复制代码
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 获取到用户数据状态:
tsx 复制代码
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:
html 复制代码
<div class="content">
    {{ user | async | json }}
</div>

接入副作用

通过接入副作用(effects)来完成异步获取网络数据更新状态。

安装 effects 核心模块:

html 复制代码
npm install @ngrx/effects --save

创建 User 的副作用:

选项介绍 :

选项 作用
--root 目标模块为根模块时设置
--module 提供目标模块的路径
--skip-tests 跳过生成测试文件

示例命令:

bash 复制代码
ng generate effect store/effects/user --root --module=app.module.ts --skip-tests

创建 app/store/effects/user.effects.ts 并更新 app.module.ts

tsx 复制代码
import { Injectable } from '@angular/core';
import { Actions, createEffect } from '@ngrx/effects';

@Injectable()
export class UserEffects {
  constructor(private actions$: Actions) {}
}
tsx 复制代码
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 服务:

tsx 复制代码
ng g service services/user --skip-tests

编写用来模拟网络获取用户数据的异步函数 updateApi :

tsx 复制代码
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,仅作为触发使用,更新用户数据在接下来的副作用编写中会体现:

tsx 复制代码
import { createActionGroup, emptyProps, props } from '@ngrx/store';

export const UserActions = createActionGroup({
  source: 'User',
  events: {
		...
    UpdateUser: emptyProps(),
  },
});

完成副作用编写:

UserEffects 中注入 UserService 后开始创建副作用,总共 4 步操作:

tsx 复制代码
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,来获取网络上的用户数据:

tsx 复制代码
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。

初始化项目:

创建新项目并安装依赖:

tsx 复制代码
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:

json 复制代码
{
    "cli": {
        "schematicCollections": ["@ngrx/schematics"]
    }
}

创建存储 State 的 Store:

json 复制代码
ng generate store State --root --module=app.module.ts --state-path=store --state-interface AppState 

创建实体:

选项介绍:

选项 作用
--reducers 执行reducers存放路径,约定路径为上一级的 index.ts,也是 store 创建的文件
--skip-tests 跳过生成测试文件

示例命令:

json 复制代码
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 文件中体现,下面是接入实体的核心部分,更多的适配器操作可以看文件中默认生成的模板代码:

tsx 复制代码
// 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:

tsx 复制代码
add() {
  this.store.dispatch(
    TodoActions.addTodo({
      todo: {
        id: uuidv4(),
        content: this.content,
      },
    })
  );
  this.content = '';
}

删除 action:

tsx 复制代码
del(todo: Todo) {
  this.store.dispatch(TodoActions.deleteTodo({ id: todo.id }));
}

清空 action:

tsx 复制代码
clears() {
  this.store.dispatch(TodoActions.clearTodos());
}

使用实体提供的 Selector 获取状态:

tsx 复制代码
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...

相关推荐
Nan_Shu_61416 分钟前
学习: Threejs (2)
前端·javascript·学习
G_G#24 分钟前
纯前端js插件实现同一浏览器控制只允许打开一个标签,处理session变更问题
前端·javascript·浏览器标签页通信·只允许一个标签页
@大迁世界40 分钟前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路1 小时前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug1 小时前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu121381 小时前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中1 小时前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路1 小时前
GDAL 实现矢量合并
前端
hxjhnct1 小时前
React useContext的缺陷
前端·react.js·前端框架
前端 贾公子2 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端