发布于 2026-01-06 3 阅读
0

使用 NGRX 进行状态管理 - 简介

使用 NGRX 进行状态管理 - 简介

使用 NGRX 进行状态管理——简介

在本文中,我想向您介绍构成 NGRX 平台及其所有组成部分的概念,以便您充分了解它如何帮助我们创建更好的应用程序。

对于还不了解的人来说,NGRX是一个用于Angular的Redux。它可以帮助我们进行状态管理,而状态管理可以说是现代大型客户端应用程序中最难管理的部分。

与仅提供 store 层的Redux相比, NGRX具有相当多的强大功能。

注意:本文中的代码片段尚未更新至 NGRX 8 的最新更改。


这是共四篇系列文章的第一部分,内容将涵盖:

  • NGRX 的支柱
  • 如何使用 reducers 和实体来构建我们的 Store 架构
  • 管理和测试副作用
  • 将我们的用户界面连接到外观服务,其优缺点

为什么选择 NGRX 或其他任何状态管理解决方案

NGRX 和其他 Angular 状态管理库(NGXS、Akita)已成为复杂 Web 应用程序架构中的重要组成部分。

我的一个不太受欢迎的观点是,每个 Angular 应用程序都应该使用某种状态管理,无论是基于 RX 的服务、MobX 还是不同的 Redux 实现。

我发现大型项目(甚至小型项目)在组件中使用本地状态时存在一些陷阱,例如:

  • 路径间数据传输困难
  • 缓存已获取数据存在困难
  • 重复的逻辑和请求
  • 没有惯例

这份清单还可以更长,但这足以让我相信,某种形式的状态管理对于近期内避免重构新应用程序至关重要。

NGRX 支柱

让我们来看看NGRX平台的架构:

  • store — 它提供了一个中央存储库,我们可以从中选择Angular 依赖注入中每个组件和服务的状态。
  • effects 顾名思义,副作用是指在执行某个操作时产生的副作用。
  • entity — 一个实体框架,旨在帮助减少常见的样板文字

现在让我们更详细地了解一下接下来步骤中我们将要探讨的所有概念。

店铺

商店是我们存储数据的中央存储库。

这是我们的应用数据库,也是客户端唯一的数据源。从技术角度来说,它只是一个嵌套对象,我们用它来选择和存储数据。

由于Store服务可以通过 Angular DI 访问,因此我们应用程序中所有组件和服务都可以从任何地方访问我们 store 中的数据。

除非与应用程序的某个特定部分(例如表单、弹出窗口、瞬态状态)有关,否则任何与状态相关的信息都应该存储在 store 中。

行动

用 NGRX 的术语来说——action 是包含传递给 reducer 或触发副作用的信息的类。

动作有两个参数:

  • 我们命名的唯一标识符type(请确保将其标记为readonly
  • 一个可选payload属性,表示传递给操作的数据
export enum LoginActionTypes {  
    LoginButtonClicked = '[Login Button] LOGIN_BUTTON_CLICKED',  
    LoginRequestStarted = '[Login API] LOGIN_REQUEST_STARTED'  
}

export class LoginButtonClicked {  
   public readonly type = LoginActionTypes.LoginButtonClicked;

   constructor(public payload: LoginRequestPayload) {}  
}

export class LoginRequestStarted {  
   public readonly type = LoginActionTypes.LoginRequestStarted;

   constructor(public payload: LoginRequestPayload) {}  
}

export type UserActions = LoginButtonClicked | LoginRequestStarted;
Enter fullscreen mode Exit fullscreen mode

参数命名约定type

  • 你通常会看到这种文字是用这种格式书写的。[prefix] NAME
  • 根据 NGRX 团队的建议,前缀可用于声明请求的来源。

💡专业提示:编写多个细粒度的操作,并始终注明它们的来源。即使重写一些功能相同的操作也无关紧要。 

减速器

Reducer 只是负责以不可变方式更新状态对象的纯函数。 

reducer 是一个函数,它接收两个参数:当前状态对象和动作类,并返回新的状态作为输出。 

新状态始终是一个新构建的对象我们永远不会改变该状态

export function loginReducer(  
    state: UserState = {},  
    action: UserActions  
): UserState {  
    switch (action.type) {  
        case UserActionTypes.LoginSuccess:  
            return action.payload;  
        default:  
            return state;  
}
Enter fullscreen mode Exit fullscreen mode

这是一个极其简单的 reducer,如果没有匹配到 action,则返回当前状态;否则,返回 action 的有效负载作为下一个状态。在实际应用中,你的 reducer 会比示例中的 reducer 复杂得多。 

有很多库可以简化 reducer 的使用,但对我来说,它们很少值得使用。

对于更复杂的 reducer,我建议创建函数,并保持 reducer 函数简单小巧

事实上,我们可以通过使用对象并将操作类型与对象的键进行匹配来重构 switch 语句。

让我们重写一下:

interface LoginReducerActions {   
    [key: UserActionTypes]: (  
        state: UserState,   
        action: UserActions  
    ): UserState;  
};

const loginReducerActions: LoginReducerActions = {   
    [UserActionTypes.LoginSuccess]: (  
       state: UserState,   
       action: LoginSuccess  
    ) => action.payload  
};

export function loginReducer(  
    state: UserState = {},  
    action: UserActions  
): UserState {  
    if (loginReducerActions.hasOwnProperty(action.type)) {  
       return loginReducerActions[action.type](state, action);  
    }

    return state;  
}
Enter fullscreen mode Exit fullscreen mode

选择器

选择器是我们定义的用于从商店对象中选择信息的函数。 

在介绍选择器之前,让我们先来看看在服务中通常是如何从数据存储中选择数据的:

 

interface DashboardState {  
   widgets: Widget[];  
}

export class DashboardRepository {  
    widgets$ = this.store.select((state: DashboardState) => {  
        return state.widgets;  
    });

    constructor(private store: Store<DashboardState>) {}  
}
Enter fullscreen mode Exit fullscreen mode

为什么这种方法并不理想?

  • 它不干
  • 如果商店的结构要改变(相信我,它肯定会改变),我们就需要对所有商品进行相应的调整。
  • 服务本身了解商店的结构。
  • 不缓存 

让我们来介绍一下受 React 库启发而提供的@ngrx/store名为的实用程序。 createSelectorreselect

为了简单起见,我将保持代码片段的统一性,但您应该假设选择器是在单独的文件中创建的,并且它们会被导出。

// selectors  
import { createSelector, createFeatureSelector } from '@ngrx/store';

const selectDashboardState = createFeatureSelector('dashboard');

export const selectAllWidgets = createSelector(  
    selectDashboardState,   
    (state: DashboardState) => state.widgets  
);

// service  
export class DashboardRepository {  
    widgets$ = this.store.select(selectAllWidgets);

    constructor(private store: Store<DashboardState>) {}  
}
Enter fullscreen mode Exit fullscreen mode

💡专业提示:选择器非常有用,务必编写细粒度的选择器,并尽量将逻辑封装在选择器中,而不是封装在服务或组件中。

实体

实体由软件包添加@ngrx/entity 。

如果您曾经使用过 Redux,您可能已经发现,常见 CRUD 操作的样板代码既耗时又冗余。

NGRX Entity 提供了一组开箱即用的常用操作和选择器,帮助我们减少 reducer 的大小。

使用实体框架后,我们的状态会是什么样子?

interface EntityState<V> {    
  ids: string[] | number[];     
  entities: {   
      [id: string | id: number]: V   
  };
}
Enter fullscreen mode Exit fullscreen mode

我通常先创建一个适配器,放在一个单独的文件中,这样我们就可以从不同的文件(例如 reducer 文件和 selectors 文件)中导入它。

export const adapter: EntityAdapter = createEntityAdapter();

让我们在 reducer 中使用适配器。适配器是如何与 reducer 交互的呢?

  • 它创建了一个初始状态(参见上面的接口EntityState
  • 它为我们提供了一系列CRUD操作方法,以便我们可以动态编写 reducer。
const initialState: DashboardState = adapter.getInitialState();

export const dashboardReducer(  
    state = initialState,  
    action: DashboardActions  
): DashboardState {  
   switch (action.type) {  
       case DashbordActionTypes.AddWidget:  
          const widget: Widget = action.payload;    
          return adapter.addOne(action.payload, state);  
   }

   // more ...   
   }
}
Enter fullscreen mode Exit fullscreen mode

💡专业提示查看 NGRX 实体中所有可用的方法。

实体适配器还允许我们启动一组选择器来查询存储。

以下示例展示了如何选择仪表板状态中的所有组件:

const { selectAll } = adapter.getSelectors();

export const selectDashboardState = createFeatureSelector<DashboardState>('dashboard');

export const selectAllWidgets = createSelector(  
  selectDashboardState,  
  selectAll  
);
Enter fullscreen mode Exit fullscreen mode

💡专业提示查看 NGRX Entity 中所有可用的选择器。

效果

最后,我最喜欢的NGRX功能:特效

顾名思义,我们使用 effects 来管理应用程序中的副作用。NGRX 将 effects 实现为由 action 发出的流,这些流通常会返回新的 action。

让我们来看一下下图:

  • 应用程序中的某个地方会发出一个操作(例如:用户界面、WebSocket、定时器等)。
  • 效果会拦截已定义副作用的动作。副作用会被执行。
  • 副作用(除特殊情况外)会返回一个新的操作。
  • 该操作会经过一个 reducer 并更新 store。

正如我之前提到的,并非所有副作用都会返回新的 action。我们可以配置副作用,使其在不需要时不分发任何 action,但重要的是你要明白,在大多数情况下,我们确实需要分发新的 action。 

NGRX中效果最实际的应用场景是发出HTTP请求:

export class WidgetsEffects {  
    constructor(  
        private actions$: Actions,  
        private api: WidgetApiService  
    ) {}

    @Effect()  
    createWidget$: Observable<AddWidgetAction> =   
        this.actions$.pipe(  
            ofType(WidgetsActionTypes.CreateWidgetRequestStarted),  
            mergeMap((action: CreateWidgetAction) => {  
                return this.api.createWidget(action.payload);   
            }),  
            map((widget: Widget) => new AddWidgetAction(widget))  
         );

    @Effect({ dispatch: false })  
    exportWidgets$: Observable<void> =   
        this.actions$.pipe(  
            ofType(WidgetsActionTypes.ExportWidgets),  
            tap((action: ExportWidgets) => {  
                return this.api.exportWidgets();   
            }),  
         );  
}
Enter fullscreen mode Exit fullscreen mode

让我们来分析一下上面的代码片段。

  • 我们创建了一个名为WidgetsEffects
  • Actions我们导入了两个提供商:WidgetsApiService
  • Actions是一个动作流。我们使用运算符ofType来帮助我们过滤动作,只保留我们想要监听的动作。
  • 我们在类上创建一个属性,并用以下方式装饰它:Effect
  • CREATE_WIDGET_REQUEST当一个名为 `action` 的动作被派发时,就会调用此效果。
  • 我们从操作中获取有效负载,并使用我们的 API 服务执行调用。
  • 一旦成功执行,我们就将其映射AddWidgetAction到 reducer 可以获取的 action,并更新我们的 store。
  • 在第二个名为 `<effect>` 的效果中exportWidgets$ ,我们接收到一个 `<action>` ExportWidgets ,我们使用tap`<operator>` 操作符来执行一个副作用,然后……嗯,什么也没发生!由于我们传递了配置,{ dispatch: false }因此无需返回任何 `<action>`。

要点总结

  • 状态管理解决方案(无论是库还是您自己的实现)始终应优先于本地状态。 
  • 单一数据源(例如数据存储)有助于我们管理应用程序的状态,但也有一些例外情况,例如状态处于瞬态时。
  • 我们简要地探讨了 store、actions、reducers、entities、selectors 和 effects 的概念,但在接下来的步骤中,我们将通过一些更高级的示例详细讲解每一个概念。

下一篇文章,我们将进入应用程序的存储和状态实体。


如果您需要任何澄清,或者您认为有什么不清楚或错误的地方,请务必留言!

希望您喜欢这篇文章!如果您喜欢,请在 MediumTwitter上关注我 获取更多关于前端、Angular、RxJS、TypeScript 等主题的文章!

文章来源:https://dev.to/gc_psk/state-management-with-ngrx-introduction-1g2m