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 });
}
}
最后,当登录的用户是菜单项页面上的管理员时,我们会显示Edit 和Delete 按钮。打开 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来处理与用户有关的功能。