在Angular + Redux应用程序中处理副作用

146 阅读4分钟

简介

随着Redux在前端生态系统中的广泛流行,Angular和其他领先的前端框架已经将其作为他们可靠的状态管理库之一。

但不幸的是,Redux架构并没有提供任何内置的功能来处理Redux状态树的异步数据变化(也被称为副作用)。因此,当这些异步操作完成后,Redux状态树将受到影响。

本文将介绍@ngrx/effects库,一个用于处理NgRx应用程序中的副作用的特殊包,以及如何使用它来处理NgRx应用程序中的副作用。

前提条件

  1. 对Angular的了解
  2. 对NgRx的了解
  3. 对TypeScript的了解

什么是副作用?

副作用是指诸如从远程服务器获取数据、访问本地存储、记录分析事件以及访问通常在未来某个时间完成的文件等操作。

知道了什么是副作用,假设我们想向一个API端点发出请求,以获取我们应用程序中的用户列表。考虑到这种操作将是异步的,以下情况将被考虑。

  1. FETCHING_USERS
  2. USERS_FETCH_SUCCESSFUL
  3. ERROR_FETCHING_USERS

让我们通过一些实践来弄脏我们的手。

环境设置

用下面的命令创建一个新的Angular项目。

ng new side-effects

运行下面的命令来安装本练习所需的依赖项。

npm install --save @ngrx/effects @ngrx/store rxjs

接下来,我们将运行下面的命令,为用户创建一个功能模块。

ng generate module user

然后,我们将创建一个constants.ts 文件,以容纳FETCHING USERSUSERS 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

如果您有任何问题或建议,请在下面的评论区提出。

处理Angular + Redux应用程序中的副作用一文出现在LogRocket博客上。