如何在Angular中使用NGXS进行状态管理 - 第2部分

670 阅读6分钟

TL;DR在Angular中的状态管理与NGXS系列的第一部分,我们了解了NGXS的基础知识,它是如何工作的,以及如何使用它来管理我们应用程序的状态。在第二部分,我将向你展示如何使用NGXS的Auth0来管理与使用有关的状态。


用户状态管理

用户存储的工作方式将类似于菜单状态管理的工作方式。我不会详细说明状态的每一部分是如何工作的,而会更多地关注Auth0和NGXS的整合。

启动程序直接使用Auth0的SDK,并通过RolesService 管理角色。本教程部分将指导你通过NGXS迁移到使用Auth0的SDK,并通过使用NGXS的Select 功能管理角色。

创建用户模型

让我们先来定义用户状态对象的接口。打开 user.model.ts并添加以下代码👇

// src/app/core/state/user/user.model.ts

import { User as Auth0User } from "@auth0/auth0-spa-js";

export interface UserStateModel {
  userDetails: Auth0User | undefined;
}

用下面的代码为user 目录创建一个桶状输出👇

// src/app/core/state/user/index.ts

export * from "./user.model";

为了进一步简化我们的导入,将用户文件夹添加到state 文件夹的桶状导出中,并添加以下代码👇

// src/app/core/state/index.ts

export * from "./menus";

// ✨ New 👇
export * from "./user";

创建用户行动

我们有三个与用户有关的动作,我们需要为我们的用户商店。登录、注销和用户变更Action,以保持我们Store中的用户细节与Auth0的SDK同步。

由于我们有几个用户动作将来自于Navbar组件,你可以把它们分组在AllNavbarAction ,以确保你不会在应用程序的不同部分重复使用这些动作(遵循良好的动作卫生模式)。

因为UserChangedFromAuth0SDK Action源于Auth0的SDK,我们把Action类型的源部分命名为 Auth0 SDK.

打开 user.actions.ts并添加下面的代码👇

// src/app/core/state/user/user.actions.ts

import { User as Auth0User } from "@auth0/auth0-spa-js";

export namespace User {
  export namespace AllNavbarActions {
    export class LoginFlowInitiated {
      static readonly type = "[Navbar] Login Flow Initiated";
    }

    export class LogoutFlowInitiated {
      static readonly type = "[Navbar] Logout Flow Initiated";
    }
  }
  export class UserChangedFromAuth0SDK {
    static readonly type = "[Auth0 SDK] User Changed";
    constructor(public payload: { user: Auth0User | undefined }) {}
  }
}

添加到 user.actions到桶的导出。打开 index.ts并添加下面的代码👇

// src/app/core/state/user/index.ts

export * from "./user.model";

// ✨ New 👇
export * from "./user.actions";

更新应用程序以使用用户动作

就像你在文章的第一部分对菜单相关功能所做的那样,让我们更新应用程序的用户相关功能以使用NGXS的Actions。你可以通过注入Store类,用你在上一节定义的Action名称调用dispatch 函数来使用Action。

打开 nav-bar.component.ts并添加下面的代码👇

// src/app/shared/components/nav-bar/nav-bar.component.ts

import { Component } from "@angular/core";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { faHome, faUser, faUtensils } from "@fortawesome/free-solid-svg-icons";
import { AuthService } from "@auth0/auth0-angular";

// ✨ New 👇
import { Store } from "@ngxs/store";
import { User } from "src/app/core";

export interface INavBarMenuLinkProps {
  to: string;
  icon: IconDefinition;
  label: string;
}

@Component({
  selector: "app-nav-bar",
  templateUrl: "./nav-bar.component.html",
  styleUrls: ["./nav-bar.component.scss"],
})
export class NavBarComponent {
  faUser = faUser;
  isAuthenticated$ = this.authService.isAuthenticated$;
  user$ = this.authService.user$;

  navOptions: INavBarMenuLinkProps[] = [
    { to: "/home", label: "Home", icon: faHome },
    { to: "/menu", label: "Menu", icon: faUtensils },
  ];

  constructor(
    private authService: AuthService,
    // ✨ New 👇
    private store: Store
  ) {}

  loginWithRedirect(): void {
    // ✨ New 👇
    this.store.dispatch(new User.AllNavbarActions.LoginFlowInitiated());
  }

  logout(): void {
    // ✨ New 👇
    this.store.dispatch(new User.AllNavbarActions.LogoutFlowInitiated());
  }
}

创建用户状态

在为用户状态的片断创建单独的选择器之前,让我们先创建使用这个功能所需的模板。NGXS使用一个带有额外的State 装饰器的Injectable 类。创建 user.state.ts并添加以下代码👇

// src/app/core/state/user/user.state.ts

import { Injectable } from "@angular/core";
import { State } from "@ngxs/store";
import { UserStateModel } from "./user.model";

@State<UserStateModel>({
  name: "user",
  defaults: {
    userDetails: undefined,
  },
})
@Injectable()
export class UserState {}

接下来,让我们为user,isLoggedIn,userRoles, 和isAdmin 添加一些实用的选择器,以方便让你的组件访问这些属性。打开 user.state.ts并用下面的代码更新它👇

// src/app/core/state/user/user.state.ts

import { Injectable } from "@angular/core";
import { UserStateModel } from "./user.model";

// ✨ New 👇
import { State, Selector } from "@ngxs/store";

// ✨ New 👇
import { environment } from "src/environments/environment";

// ✨ New 👇
import { USER_ROLES } from "../../services";

@State<UserStateModel>({
  name: "user",
  defaults: {
    userDetails: undefined,
  },
})
@Injectable()
export class UserState {
  // ✨ New 👇
  @Selector()
  static user(state: UserStateModel) {
    return state.userDetails;
  }

  // ✨ New 👇
  @Selector()
  static isLoggedIn(state: UserStateModel) {
    return !!state.userDetails;
  }

  // ✨ New 👇
  @Selector()
  static userRoles(state: UserStateModel) {
    return (
      state.userDetails?.[`${environment.auth.audience}/roles`] || undefined
    );
  }

  // ✨ New 👇
  @Selector()
  static isAdmin(state: UserStateModel) {
    return state.userDetails?.[`${environment.auth.audience}/roles`]?.includes(
      USER_ROLES.MENU_ADMIN
    );
  }
}

添加 user.state到出口列表中的 index.ts👇

// src/app/core/state/user/index.ts

export * from "./user.model";
export * from "./user.actions";

// ✨ New 👇
export * from "./user.state";

更新应用程序以使用用户选择器

用户角色路由防护以前使用的是来自RolesService 的角色值。由于你现在有一个用户角色选择器,你可以用上一节中创建的选择器替换当前的实现。打开 user-role.guard.ts并添加以下代码👇

// src/app/core/guards/user-role.guard.ts

import { Injectable } from "@angular/core";
import {
  CanActivate,
  Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
} from "@angular/router";
import { Observable, of } from "rxjs";
import { catchError, map } from "rxjs/operators";

// ✨ New 👇
import { Store } from "@ngxs/store";
import { UserState } from "..";

@Injectable({ providedIn: "root" })
export class UserRoleGuard implements CanActivate {
  constructor(
    private router: Router,
    // ✨ New 👇
    private store: Store
  ) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> {
    // ✨ Update this 👇
    return this.store.select(UserState.userRoles).pipe(
      map((roles) => {
        if (roles && roles.includes(route?.data?.role)) {
          return true;
        }

        // redirect the user to home
        this.router.navigate(["/home"]);
        return false;
      }),
      catchError((err) => {
        // redirect the user to home
        this.router.navigate(["/home"]);
        return of(false);
      })
    );
  }
}

我们使用用户相关信息的另一个地方是导航栏。我们显示用户信息,并根据用户的isAuthenticated 状态,有条件地显示登录和注销按钮。打开 nav-bar.component.ts并添加下面的代码👇

// src/app/shared/components/nav-bar/nav-bar.component.ts

import { Component } from "@angular/core";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { faHome, faUser, faUtensils } from "@fortawesome/free-solid-svg-icons";
import { Store } from "@ngxs/store";

// ✨ New 👇
import { User, UserState } from "src/app/core";

export interface INavBarMenuLinkProps {
  to: string;
  icon: IconDefinition;
  label: string;
}

@Component({
  selector: "app-nav-bar",
  templateUrl: "./nav-bar.component.html",
  styleUrls: ["./nav-bar.component.scss"],
})
export class NavBarComponent {
  faUser = faUser;

  // ✨ New 👇
  isAuthenticated$ = this.store.select(UserState.isLoggedIn);

  // ✨ New 👇
  user$ = this.store.select(UserState.user);

  navOptions: INavBarMenuLinkProps[] = [
    { to: "/home", label: "Home", icon: faHome },
    { to: "/menu", label: "Menu", icon: faUtensils },
  ];

  constructor(private store: Store) {}

  loginWithRedirect(): void {
    this.store.dispatch(new User.AllNavbarActions.LoginFlowInitiated());
  }

  logout(): void {
    this.store.dispatch(new User.AllNavbarActions.LogoutFlowInitiated());
  }
}

档案页显示一些关于用户的信息,比如姓名和照片。由于这是作为用户存储的一部分,我们也来更新一下,以使用用户选择器。打开 profile.component.ts并添加以下代码👇

// src/app/features/profile/profile.component.ts

import { Component } from "@angular/core";

// ✨ New 👇
import { Store } from "@ngxs/store";

// ✨ New 👇
import { UserState } from "src/app/core";

@Component({
  selector: "app-profile",
  templateUrl: "./profile.component.html",
  styleUrls: ["./profile.component.scss"],
})
export class ProfileComponent {
  // ✨ New 👇
  user$ = this.store.select(UserState.user);

  // ✨ New 👇
  constructor(private store: Store) {}
}

当登录的用户是菜单项页面上的管理员用户时,我们显示一个Add 按钮。你可以在这篇博文中阅读更多关于在Auth0上设置管理用户的内容。打开 menu-items.component.ts并添加以下代码👇

// src/app/features/menu/menu-items/menu-items.component.ts

import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { Store } from "@ngxs/store";

// ✨ New 👇
import { MenusState, UserState } from "src/app/core";

@Component({
  selector: "app-menu-items",
  templateUrl: "./menu-items.component.html",
  styles: [
    `
      :host {
        width: 100%;
        height: 100%;
      }
    `,
  ],
})
export class MenuItemsComponent {
  menuItems$ = this.store.select(MenusState.menuItems);

  // ✨ New 👇
  isAdmin$ = this.store.select(UserState.isAdmin);

  constructor(
    private router: Router,
    private activatedRoute: ActivatedRoute,
    private store: Store
  ) {}

  addMenuItem(): void {
    this.router.navigate(["add"], { relativeTo: this.activatedRoute });
  }
}

最后,当登录的用户是菜单项页面上的管理员时,我们会显示EditDelete 按钮。打开 menu-item.component.ts并添加下面的代码👇

// src/app/features/menu/menu-item/menu-item.component.ts

import { Component } from "@angular/core";
import { Location } from "@angular/common";
import { ActivatedRoute, Router } from "@angular/router";
import { map, switchMap } from "rxjs/operators";
import { Store } from "@ngxs/store";

// ✨ New 👇
import { MenusState, UserState } from "src/app/core";

@Component({
  selector: "app-menu-item",
  templateUrl: "./menu-item.component.html",
  styleUrls: ["./menu-item.component.scss"],
})
export class MenuItemComponent {
  menuItemId$ = this.activatedRoute.params.pipe(map((params) => params.id));

  menuItem$ = this.menuItemId$.pipe(
    switchMap((id) => this.store.select(MenusState.menuItem(id)))
  );

  // ✨ New 👇
  isAdmin$ = this.store.select(UserState.isAdmin);

  constructor(
    private activatedRoute: ActivatedRoute,
    private router: Router,
    private location: Location,
    private store: Store
  ) {}

  back(): void {
    this.location.back();
  }

  navigateTo(url: string): void {
    this.router.navigateByUrl(`${this.router.url}/${url}`);
  }
}

创建用户动作处理程序

像菜单动作处理程序一样,用户状态将使用Action 装饰器来处理与用户有关的动作。对于用户状态,动作处理程序将是应用程序与Auth0的SDK交互的地方--登录、注销,以及用Auth0的用户对象更新状态。

让我们从登录和注销的动作处理程序开始,在Auth0的SDK中触发各自的流程。打开 user.state.ts并添加下面的代码👇

// src/app/core/state/user/user.state.ts

import { Injectable } from "@angular/core";

// ✨ New 👇
import { State, Action, StateContext, Store, Selector } from "@ngxs/store";
// ✨ New 👇
import { AuthService } from "@auth0/auth0-angular";

import { environment } from "src/environments/environment";
import { UserStateModel } from "./user.model";
import { User } from "./user.actions";
import { USER_ROLES } from "../../services";

@State<UserStateModel>({
  name: "user",
  defaults: {
    userDetails: undefined,
  },
})
@Injectable()
export class UserState {
  constructor(
    private store: Store,
    // ✨ New 👇
    private authService: AuthService
  ) {}

  // ✨ New 👇
  @Action(User.AllNavbarActions.LoginFlowInitiated)
  login() {
    this.authService.loginWithRedirect();
  }

  // ✨ New 👇
  @Action(User.AllNavbarActions.LogoutFlowInitiated)
  logout() {
    this.authService.logout();
  }

  @Selector()
  static user(state: UserStateModel) {
    return state.userDetails;
  }

  @Selector()
  static isLoggedIn(state: UserStateModel) {
    return !!state.userDetails;
  }

  @Selector()
  static userRoles(state: UserStateModel) {
    return (
      state.userDetails?.[`${environment.auth.audience}/roles`] || undefined
    );
  }

  @Selector()
  static isAdmin(state: UserStateModel) {
    return state.userDetails?.[`${environment.auth.audience}/roles`]?.includes(
      USER_ROLES.MENU_ADMIN
    );
  }
}

为了使来自Auth0的SDK的用户观察器和我们的用户状态保持同步,创建一个订阅来监听AuthService'suser$ 属性,每当观察器发出一个新的值时就派发UserChangedFromAuth0SDK 。然后UserChangedFromAuth0SDK 将用来自Auth0的新用户数据更新用户状态。打开 user.state.ts并添加以下代码👇

// src/app/core/state/user/user.state.ts

import { Injectable } from "@angular/core";
import { State, Action, StateContext, Store, Selector } from "@ngxs/store";
import { AuthService } from "@auth0/auth0-angular";
import { environment } from "src/environments/environment";
import { USER_ROLES } from "../../services";
import { UserStateModel } from "./user.model";
import { User } from "./user.actions";

@State<UserStateModel>({
  name: "user",
  defaults: {
    userDetails: undefined,
  },
})
@Injectable()
export class UserState {
  constructor(private store: Store, private authService: AuthService) {
    // ✨ New 👇
    this.listenToUserChange();
  }

  // ✨ New 👇
  @Action(User.UserChangedFromAuth0SDK)
  userChangedFromAuth0SDK(
    ctx: StateContext<UserStateModel>,
    actions: User.UserChangedFromAuth0SDK
  ) {
    const state = ctx.getState();
    ctx.setState({
      ...state,
      userDetails: actions.payload.user,
    });
  }

  @Action(User.AllNavbarActions.LoginFlowInitiated)
  login() {
    this.authService.loginWithRedirect();
  }

  @Action(User.AllNavbarActions.LogoutFlowInitiated)
  logout() {
    this.authService.logout();
  }

  // ✨ New 👇
  private listenToUserChange(): void {
    this.authService.user$.subscribe((user) => {
      this.store.dispatch(
        new User.UserChangedFromAuth0SDK({ user: user || undefined })
      );
    });
  }

  @Selector()
  static user(state: UserStateModel) {
    return state.userDetails;
  }

  @Selector()
  static isLoggedIn(state: UserStateModel) {
    return !!state.userDetails;
  }

  @Selector()
  static userRoles(state: UserStateModel) {
    return (
      state.userDetails?.[`${environment.auth.audience}/roles`] || undefined
    );
  }

  @Selector()
  static isAdmin(state: UserStateModel) {
    return state.userDetails?.[`${environment.auth.audience}/roles`]?.includes(
      USER_ROLES.MENU_ADMIN
    );
  }
}

更新应用程序模块

然后你需要把UserState 加入到NgxsModule 的初始化中。打开 app.module.ts并添加下面的代码。

// src/app/app.module.ts

import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { AuthHttpInterceptor, AuthModule } from "@auth0/auth0-angular";

import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { NavBarModule } from "./shared";
import { environment } from "src/environments/environment";
import { HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http";
import { NgxsModule } from "@ngxs/store";
import { NgxsReduxDevtoolsPluginModule } from "@ngxs/devtools-plugin";

// ✨ Update this 👇
import { MenusState, UserState } from "./core";

@NgModule({
  imports: [
    BrowserModule,
    HttpClientModule,
    AuthModule.forRoot({
      ...environment.auth,
      cacheLocation: "localstorage",
      httpInterceptor: {
        allowedList: [
          `${environment.serverUrl}/api/menu/items`,
          `${environment.serverUrl}/api/menu/items/*`,
        ],
      },
    }),
    AppRoutingModule,
    NavBarModule,
    // ✨ Update this 👇
    NgxsModule.forRoot([MenusState, UserState], { developmentMode: true }),
    NgxsReduxDevtoolsPluginModule.forRoot(),
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthHttpInterceptor,
      multi: true,
    },
  ],
})
export class AppModule {}

检查点:在这部分没有添加任何可见的功能变化。应用程序的当前状态和之前的检查点之间的区别在于其底层实现。该应用现在使用NGXS而不是BehaviorSubject 来管理用户相关的状态。当用户未被认证时,应用程序应显示带有空的仪表盘的登录按钮,当用户被认证时,显示带有登录用户姓名的注销按钮以及仪表盘上的菜单项目。点击 "登录 "和 "注销 "按钮应该使用Auth0的SDK触发各自的流程,并更新你的应用程序的状态。如果你在浏览器中打开Redux Devtools,你应该在每次执行任何与用户有关的操作时看到用户状态和操作。

结论

状态管理是构建应用程序时的一个关键组成部分。你在我们的演示应用程序中添加了两个Store来管理两个不同的状态--菜单和用户。这是一个相对较小的演示应用程序,有一些商店、动作和选择器,以展示你如何使用NGXS来管理你的应用程序的状态,并使用NGXS与Auth0的SDK来处理与用户有关的功能。