注:图片来自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 项目
bash
# 基于 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 进行整合使用;
安装命令:
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(),
},
});
- 增加用于添加用户的
AddUser
,并使用props
约束所接收的参数类型; - 增加用于删除用户的
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 体验它的作用。
- 在
app.component.ts
构造函数中注入 Store:
tsx
import { Store } from '@ngrx/store';
export class AppComponent {
// 注入 Store
constructor(private store: Store) {}
}
- 让根组件实现 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);
}
}
- 定义 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);
}
...
}
- 使用管道符在页面渲染 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.ts
、todo.model.ts
、todo.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...