简介
随着Redux在前端生态系统中的广泛流行,Angular和其他领先的前端框架已经将其作为他们可靠的状态管理库之一。
但不幸的是,Redux架构并没有提供任何内置的功能来处理Redux状态树的异步数据变化(也被称为副作用)。因此,当这些异步操作完成后,Redux状态树将受到影响。
本文将介绍@ngrx/effects库,一个用于处理NgRx应用程序中的副作用的特殊包,以及如何使用它来处理NgRx应用程序中的副作用。
前提条件
- 对Angular的了解
- 对NgRx的了解
- 对TypeScript的了解
什么是副作用?
副作用是指诸如从远程服务器获取数据、访问本地存储、记录分析事件以及访问通常在未来某个时间完成的文件等操作。
知道了什么是副作用,假设我们想向一个API端点发出请求,以获取我们应用程序中的用户列表。考虑到这种操作将是异步的,以下情况将被考虑。
FETCHING_USERSUSERS_FETCH_SUCCESSFULERROR_FETCHING_USERS
让我们通过一些实践来弄脏我们的手。
环境设置
用下面的命令创建一个新的Angular项目。
ng new side-effects
运行下面的命令来安装本练习所需的依赖项。
npm install --save @ngrx/effects @ngrx/store rxjs
接下来,我们将运行下面的命令,为用户创建一个功能模块。
ng generate module user
然后,我们将创建一个constants.ts 文件,以容纳FETCHING USERS ,USERS FETCH SUCCESSFUL ,和ERROR FETCHING USERS ,如下所示。
//src/app/user/user.constants.ts
export const FETCHING_USERS = "FETCHING_USERS";
export const USERS_FETCH_SUCCESSFUL = "USERS_FETCH_SUCCESSFUL";
export const ERROR_FETCHING_USERS = "ERROR_FETCHING_USERS";
动作创建者
动作创建者是创建和返回动作的辅助函数。知道了这些,让我们创建一个,如下所示。
//src/app/user/user.actions.ts
import {
USERS_FETCH_SUCCESSFUL,
ERROR_FETCHING_USERS,
FETCHING_USERS
} from "./user.constants";
export const usersFetchSuccessful = users => ({
type: USERS_FETCH_SUCCESSFUL,
payload: users
});
export const fetchError = error => ({
type: ERROR_FETCHING_USERS,
payload: error
});
export const fetchingUsers = () => ({ type: FETCHING_USERS });
在这里我们导出usersFetchSuccessful,fetchError, 和fetchingUsers, 这些都是组件中需要的,以便与NgRx商店交互。
如果发生错误,fetchError() 动作创建器将被调用,一旦数据从端点成功返回,usersFetchSuccessful() 动作创建器将被调用,一旦API请求被启动,fetchingUsers() 动作创建器将被调用。
创建还原器
还原器是纯函数,不改变状态。相反,它们产生一个新的状态。一个还原器指定应用程序的状态如何响应所触发的动作而变化。
让我们来创建我们的还原器,如下所示。
//src/app/user/user.reducers.ts
import {
USERS_FETCH_SUCCESSFUL,
ERROR_FETCHING_USERS,
FETCHING_USERS
} from "./user.constants";
import { User } from "./user.model";
import { ActionReducerMap } from "@ngrx/store/src/models";
const initialState = {
loading: false,
list: [],
error: void 0
};
export interface UserState {
loading: boolean;
list: Array<User>;
error: string;
}
export interface FeatureUsers {
users: UserState;
}
export const UserReducers: ActionReducerMap<FeatureUsers> = {
users: UserReducer
};
export function userReducer(state = initialState, action) {
switch (action.type) {
case USERS_FETCH_SUCCESSFUL:
return { ...state, list: action.payload, loading: false };
case ERROR_FETCHING_USERS:
return { ...state, error: action.payload, loading: false };
case FETCHING_USERS:
return { ...state, loading: true };
default:
return state;
}
}
每当一个动作从连接到商店的任何组件中被派发出来时,还原器就会接收该动作,并针对这些情况测试该动作的类型属性。如果测试不符合其中任何一种情况,它将返回当前状态。
创建效果
效果允许我们执行一个指定的任务,然后在任务完成后分派一个动作。
知道了这些,让我们来创建我们的效果,它将处理发送请求、接收请求的整个过程,同时在请求失败时接收错误响应。
//src/app/user/user.effect.ts
import { Actions, Effect, ofType } from "@ngrx/effects";
import { HttpClient } from "@angular/common/http";
import { FETCHING_USERS } from "./product.constants";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import { delay, map, catchError, switchMap } from "rxjs/operators";
import { usersFetchSuccessful, fetchError } from "./user.actions";
import { Action } from "@ngrx/store";
import { of } from "rxjs/observable/of";
@Injectable()
export class UserEffects {
@Effect()
users$: Observable<Action> = this.actions$.pipe(
ofType(FETCHING_USERS),
switchMap(action =>
this.http
.get("https://jsonplaceholder.typicode.com/users")
.pipe(
delay(3000),
map(usersFetchSuccessful),
catchError(err => of(fetchError(err)))
)
)
);
constructor(private actions$: Actions<Action>, private http: HttpClient) {
console.log("user effects initialized");
}
}
这里,@Injectable 装饰器被用来装饰Effect 类。
ofType() 方法允许我们监听一个特定的派发动作,在我们的案例中,FETCHING_USERS ,同时触发switchMap() 方法,允许我们将当前的观察器转换为AJAX服务。delay() 方法允许我们在一段时间内显示加载指标。map() 方法允许我们在AJAX响应成功时派发一个动作。
注册效果
有两种注册效果的方法。我们可以在根模块或功能模块中这样做。前一种方法使效果在整个应用程序中可被全局访问,而后一种方法将其限制在特定的模块中。出于代码可重用性的考虑,后者是首选。
//src/app/user/user.module.ts
import { NgModule } from "@angular/core";
import { UserComponent } from "./user.component";
import { BrowserModule } from "@angular/platform-browser";
import { UserEffects } from "./user.effect";
import { EffectsModule } from "@ngrx/effects";
import { StoreModule, Action } from "@ngrx/store";
import { UserReducers } from "./user.reducers";
import { HttpClientModule } from "@angular/common/http";
@NgModule({
imports: [
BrowserModule,
StoreModule.forFeature("featureUsers", UserReducers),
EffectsModule.forFeature([UserEffects]),
HttpClientModule
],
exports: [UserComponent],
declarations: [UserComponent],
providers: []
})
export class UserModule {}
对于一个功能模块,我们将使用forFeature() 方法在EffectsModule 。
现在我们已经完成了效果的创建和注册,让我们从我们的组件中访问这个效果。
创建选择器
如果你以前使用过Vuex,你会熟悉getters,它与NgRx选择器相似。选择器是用来从存储状态中获取计算信息的。我们可以在我们的动作和组件中多次调用getters。
知道了这些,让我们来创建我们的选择器。
//src/app/user/user.selector.ts
import { AppState } from "../app-state";
export const getList = (state: AppState) => state.featureUsers.users.list;
export const getError = (state: AppState) =>
state.featureUsers.users.error;
export const isLoading = (state: AppState) =>
state.featureUsers.users.loading;
在等待AJAX请求完成时,我们需要一个组件来显示一个加载指示器。如果请求成功,该组件将显示我们的数据,如果不成功,则显示一个错误信息。
创建组件
让我们创建一个组件,在等待AJAX请求时显示我们的数据,以及一个加载指示器。
//src/app/user/user.component.ts
import { Component, OnInit } from "@angular/core";
import { AppState } from "../app-state";
import { Store } from "@ngrx/store";
import { fetchingUsers } from "./user.actions";
import { getList, isLoading, getError } from "./user.selector";
@Component({
selector: "users",
template: `
Users:
<div *ngFor="let user of users$ | async">
{{ user.email }}
</div>
<div *ngIf="loading$ | async; let loading">
<div *ngIf="loading">
fetching users...
</div>
</div>
<div *ngIf="error$ | async; let error" >
<div *ngIf="error">{{ error }}</div>
</div>
`
})
export class UserComponent implements OnInit {
users$;
loading$;
error$;
constructor(private store: Store<AppState>) {
this.users$ = this.store.select(getList);
this.loading$ = this.store.select(isLoading);
this.error$ = this.store.select(getError);
}
ngOnInit() {
this.store.dispatch(fetchingUsers());
}
}
在这里,商店是通过构造函数注入的,所以我们可以通过商店的select() 方法从商店访问状态对象的属性。商店的select 方法返回一个可观察变量,该变量使用异步管道在模板中呈现。
有了这个,我们来更新AppState 。
//src/app/app-state.ts
import { FeatureUsers } from "./user/user.reducer";
export interface AppState {
featureUsers: FeatureUsers;
}
因为AppState 现在知道了所产生的用户对象的结构,我们的组件就能够触发store.select() 方法。
另外,让我们更新一下appModule 。
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { EffectsModule } from "@ngrx/effects";
import { AppComponent } from "./app.component";
import { StoreModule } from "@ngrx/store";
import { UserModule } from "./user/user.module";
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
EffectsModule,
StoreModule.forRoot({}),
EffectsModule.forRoot([]),
UserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
让我们用下面的命令看看我们到目前为止在浏览器上建立的东西。
npm start
结语
在这篇文章中,我们展示了如何使用@ngrx/effects 库在我们的NgRx应用程序中处理副作用,同时建立了一些Redux概念,如行动、减速器和常量。此外,我们还创建了处理未决请求、AJAX请求中的错误和成功的AJAX请求的效果。
关于NgRx的更多信息,你可以在这里查看NgRx的官方文档。这里是本教程的GitHub repo。
如果您有任何问题或建议,请在下面的评论区提出。