Angular 接入 NGRX 状态管理

2024-01-26 09:28:00 浏览数 (3)

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

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

安装命令:

代码语言:javascript复制
npm install @ngrx/store --save
npm install @ngrx/store-devtools --save
npm install @ngrx/schematics --save-dev

更新 angular.json:

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

创建存储 State 的 Store:

选项介绍:

选项

作用

--root

目标模块为根模块时设置

--module

提供目标模块的路径

--state-path

提供 State 存储的路径

--state-interface

提供 State 接口名称

示例命令:

代码语言:javascript复制
ng generate store State --root --module=app.module.ts --state-path=store --state-interface AppState

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

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

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

示例命令:

代码语言:javascript复制
ng generate action store/actions/user

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

代码语言:javascript复制
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

跳过生成测试文件

示例命令:

代码语言:javascript复制
ng generate reducer store/reducers/user --reducers=../index.ts --skip-tests

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

代码语言:javascript复制
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,
);
代码语言:javascript复制
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

代码语言:javascript复制
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:

示例命令:

代码语言:javascript复制
ng generate selector store/selectors/user --skip-tests

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

代码语言:javascript复制
import { createFeatureSelector, createSelector } from '@ngrx/store';

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

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

export class AppComponent {

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

接入副作用

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

安装 effects 核心模块:

代码语言:javascript复制
npm install @ngrx/effects --save

创建 User 的副作用:

选项介绍 :

选项

作用

--root

目标模块为根模块时设置

--module

提供目标模块的路径

--skip-tests

跳过生成测试文件

示例命令:

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

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

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

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

代码语言:javascript复制
ng g service services/user --skip-tests

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

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

代码语言:javascript复制
import { createActionGroup, emptyProps, props } from '@ngrx/store';

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

完成副作用编写:

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

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

代码语言:javascript复制
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。

初始化项目:

创建新项目并安装依赖:

代码语言:javascript复制
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:

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

创建存储 State 的 Store:

代码语言:javascript复制
ng generate store State --root --module=app.module.ts --state-path=store --state-interface AppState 

创建实体:

选项介绍:

选项

作用

--reducers

执行reducers存放路径,约定路径为上一级的 index.ts,也是 store 创建的文件

--skip-tests

跳过生成测试文件

示例命令:

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

代码语言:javascript复制
// 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:

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

删除 action:

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

清空 action:

代码语言:javascript复制
clears() {
  this.store.dispatch(TodoActions.clearTodos());
}

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

代码语言:javascript复制
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…

0 人点赞