ngrx

ngrx/store
本文档会持续更新。
Store
Strore是Angular基于Rxjs的状态管理,保存了Redux的核心概念,并使用RxJs扩展的Redux实现。使用Observable来简化监听事件和订阅等操作。在看这篇文章之前,已经假设你已了解rxjs和redux。官方文档 有条件的话,请查看官方文档进行学习理解。
安装
npm install @ngrx/store
Tutorial
下面这个Tutorial将会像你展示如何管理一个计数器的状态和如何查询以及将它显示在Angular的Component上。你可以通过StackBlitz来在线测试。
1.创建actions
src/app/counter.actions.ts

import {Action} from ‘@ngrx/store’;

export enum ActionTypes {
Increment = ‘[Counter Component] Increment’,
Decrement = ‘[Counter Component] Decrement’,
Reset = ‘[Counter Component] Reset’,
}

export class Increment implements Action {
readonly type = ActionTyoes.Increment;
}

export class Decrement implements Action {
readonly type = ActionTypes.Decrement;
}

export class Reset implements Action {
readonly tyoe = Actiontypes.Reset;
}
2.定义一个reducer通过所提供的action来处理计数器state的变化。
src/app/counter.reducer.ts

import {Action} from ‘@ngrx/store’;
import {ActionTypes} from ‘./conter.actions’;

export const initailState = 0;

export function conterReducer(state = initialState, action: Action) {
switch(action.type) {
case ActionTypes.Increment:
return state + 1;
case ActionTypes.Decrement:
return state – 1;
case ActionTypes.Reset:
return 0;
default:
return state;
}
}
3.在src/app/app.module.ts中导入 StoreModule from @ngrx/store 和 counter.reducer
import {StroeModule} from ‘@ngrx/store’;
import {counterReducer} from ‘./counter.reducer’;
4.在你的AppModule的imports array添加StoreModule.forRoot,并在StoreModule.forRoot中添加count 和 countReducer对象。StoreModule.forRoot()函数会注册一个用于访问store的全局变量。
scr/app/app.module.ts

import { BrowserModule } from ‘@angular/platform-browser’;
import { NgModule } from ‘@angular/core’;

import { AppComponent } from ‘./app.component’;

import { StoreModule } from ‘@ngrx/store’;
import { counterReducer } from ‘./counter.reducer’;

@NgModule({
declaration: [AppComponent],
imports: [
BrowserModule,
StoreModule.forRoot({count: countReducer})
],
provoders: [],
bootstrap: [AppComponent]
})
export class AppModule {}
5.在app文件夹下新创建一个叫my-counter的Component,注入Store service到你的component的constructor函数中,并使用select操作符在state查询数据。更新MyCounterComponent template,添加添加、减少和重设操作,分别调用increment,decrement,reset方法。并使用async管道来订阅count$ Observable。
src/app/my-counter/my-counter.component.html

<button (click)=”increment()”>Increment</button>

<div>Current Count: {{ count$ | async }}</div>

<button (click)=”decrement()”>Decrement</button>

<button (click)=”reset()”>Reset Counter</button>
更新MyCounterComponent类,创建函数并分发(dispatch)Increment,Decrement和Reset actions.
import { Component } from ‘@angular/core’;
import { Store, select } from ‘@ngrx/store’;
import { Observable } from ‘rxjs’;
import { Increment, Decrement, Reset } from ‘../counter.actions’;

@Component({
selector: ‘app-my-counter’,
templateUrl: ‘./my-counter.component.html’,
styleUrls: [‘./my-counter.component.css’],
})
export class MyCounterComponent (
count$: Observable<number>;

constructor(private store: Stare<{count: number}>) {
this.count$ = store.pipe(select(‘count’));
}

increment() {
this.store.dispatch(new Increment());
}

decrement() {
this.store.dispatch(new Decrement());
}

reset() {
this.store.dispatch(new Reset());
}
)
6.添加MyCounter component到AppComponent template中
<app-my-counter></app-my-counter>
Actions
Actions是NgRx的核心模块之一。Action表示在整个应用中发生的独特的事件。从用户与页面的交互,与外部的网络请求的交互和直接与设备的api交互,这些和更多的事件通过actions来描述。
介绍
在NgRx的许多地方都使用了actions。Actions是NgRx许多系统的输入和输出。Action帮助你理解如何在你的应用中处理事件。
Action接口(Action interface)
NgRx通过简单的interface来组成Action:
interface Action {
type: string;
}
这个interface只有一个属性:type,string类型。这个type属性将描述你的应用调度的action。这个类型的值以[Source]的形式出现和使用,用于提供它是什么类型的操作的上下文和action在哪里被调度(dispatched)。您可以向actions添加属性,以便为操作提供其他上下文或元数据。最常见的属性就是payload,它会添加action所需的所有数据。下面列出的是作为普通javascript对象编写的操作的示例:
{
type: ‘[Auth API] Login Success’
}
这个action描述了调用后端API成功认证的时间触发。
{
type: ‘[Login Page]’,
payload: {
username: string;
password: string;
}
}
这个action描述了用户在登录页面点击登录按钮尝试认证用户的时间触发。payload包含了登录页面提供的用户名和密码。
编写 actions
有一些编写actions的好习惯:

前期——在开始开发功能之前编写编写action,以便理解功能和知识点
分类——基于事件资源对actions进行分类
编写更多——action的编写容易,所以你可以编写更多的actions,来更好的表达应用流程
事件-驱动——捕获事件而不是命令,因为你要分离事件的描述和事件的处理
描述——提供针对唯一事件的上下文,其中包含可用于帮助开发人员进行调试的更详细信息

遵循这些指南可帮助您了解这些actions在整个应用程序中的流程。下面是一个启动登陆请求的action示例:
import {} from ‘@ngrx/store’;

export class Login Implements Action {
readonly type = ‘[Login Page] Login’

constructor(public: payload: {username: string, password: string}){}
}
action编写成类,以便在dispatched操作时提供类型安全的方法来构造action。Login action 实现(implements) Action interface。在示例中,payload是一个包含username和password的object,这是处理action所需的其他元数据.在dispatch时,新实例化一个实例。
login-page.component.ts
click(username: string, password: string) {
store.dispatch(new Login({username:username, password: password}))
}
Login action 有关于action来自于哪里和事件发生了什么的独特上线文。

action的类型包含在[]内
类别用于对形状区域的action进行分组,无论他是组件页面,后端api或浏览器api
类别后面的Login文本是关于action发生了什么的描述。在这个例子中,用户点击登录页面上的登录按钮来通过用户名密码来尝试认证。

创建action unions
actions的消费者,无论是reducers(纯函数)或是effects(带副作用的函数)都使用actions的type来确定是否要执行这个action。在feature区域,多个actions组合在一起,但是每个action都需要提供自己的type信息。看上一个Login action 例子,你将为action定义一些额外的信息。
import {Action} from ‘@ngrx/store’;

export enum ActionTypes {
Login = ‘[Login Page] Login’;
}

export class Login Implememts Action {
readonly type = ActionTypes.Login;

constructor(public paylad: {username: string, password: string})
}

export type Union = Login;
将action type string放在enum中而不是直接放在class内。此外,还会使用Union类去导出Loginclass.
Reducers
NgRx中的Reducers负责处理应用程序中从一个状态到下一个状态的转换。Reducer函数从action的类型来确定如何处理状态。
介绍
Reducer函数是一个纯函数,函数为相同的输入返回相同的输出。它们没有副作用,可以同步处理每个状态转化。每个reducer都会调用最新的action,当前状态(state)和确定是返回最新修改的state还是原始state。这个指南将会向你展示如何去编写一个reducer函数,并在你的store中注册它,并组成独特的state。
关于reducer函数
每一个由state管理的reducer都有一些共同点:

接口和类型定义了state的形状
参数包含了初始state或是当前state、当前action
switch语句

下面这个例子是state的一组action,和相对应的reducer函数。首先,定义一些与state交互的actions。
scoreboard-page.actions.ts

import {Action} from ‘@ngrx/store’;

export enum Actiontypes {
IncrementHome = ‘[Scoreboard Page] Home Score’,
IncrementAway = ‘[Scoreboard Page] Away Score’,
Reset = ‘[Scoreboard Page] Score Reset’,
}

export class IncrementHome implements Action {
readonly type = ActionTypes.IncrementHome;
}

export class IncrementAway implements Action {
readonly type = ActionTypes.IncrementAway;
}

export class Reset implements Action {
readonly type = ActionTypes.Reset;

constructor(public payload: {home: number, away: number}) {}
}

export type ActionsUnion = IncrementHome | IncrementAway | Reset;
接下来,创建reducer文件,导入actions,并定义这个state的形状。
定义state的形状
每个reducer函数都会监听actions,上面定义的scorebnoard actions描述了reducer处理的可能转化。导入多组actions以处理reducer其他的state转化。
scoreboard.reducer.ts

import * as Scoreboard from ‘../actions/scoreboard-page.actions’;

export interface State {
home: number;
away: number;
}
根据你捕获的内容来定义state的形状,它是单一的类型,如number,还是一个含有多个属性的object。
设置初始state
初始state给state提供了初始值,或是在当前state是undefined时提供值。您可以使用所需state属性的默认值设置初始state。创建并导出变量以使用一个或多个默认值捕获初始state。
scoreboard.reducer.ts

export const initialState: Satate = {
home: 0,
away: 0,
};
创建reducer函数
reducer函数的职责是以不可变的方式处理state的更变。定义reducer函数来处理actions来管理state。
scoreboard.reducer.ts

export function reducer {
satate = initialState,
action: Scoreboard.ActionsUnion
}: State {
switch(action.type) {
case Scoreboard.ActionTypes.IncrementHome: {
return {
…state,
home: state.home + 1,
}
}
case Scoreboard.ActionTypes.IncrementAway: {
return {
…state,
away: state.away + 1,
}
}
case Scoreboard.ActionTypes.Reset: {
return action.payload;
}
default: {
return state;
}
}
}
Reducers将switch语句与TypeScript在您的actions中定义的区分联合组合使用,以便在reducer中提供类型安全的操作处理。Switch语句使用type union来确定每种情况下正在使用的actions的正确形状。action的types定在你的action在你的reducer函数的case语句。type union 也约束你的reducer的可用操作。在这个例子中,reducer函数处理3个actions:IncrementHome,IncrementAway,Reset。每个action都有一个基于ActionUnion提供的强类型。每个action都可以不可逆的处理state。这意味着state更变不会修改源state,而是使用spread操作返回一个更变后的新的state。spread语法从当前state拷贝属性,并创建一个新的返回。这确保每次更变都会有新的state,保证了函数的纯度。这也促进了引用完整性,保证在发生状态更改时丢弃旧引用
注意:spread操作只执行浅复制,不处理深层嵌套对象。您需要复制对象中的每个级别以确保不变性。有些库可以处理深度复制,包括lodash和immer。
当action被调度时,所有注册过的reducers都会接收到这个action。通过switch语句确定是否处理这个action。因为这个原因,每个switch语句中总是包含default case,当这个reducer不处理action时,返回提供的state。
注册root state
state在你的应用中定义为一个large object。注册reducer函数。注册reducer函数来管理state的各个部分中具有关联值的键。使用StoreModule.forRoot()函数和键值对来定义你的state,来在你的应用中注册一个全局的Store。StoreModule.forRoot()在你的应用中注册一个全局的providers,将包含这个调度state的action和select的Store服务注入到你的component和service中。
app.module.ts

import {NgModule} from ‘@angular/core’;
import {StoreModule} form ‘@ngrx/store’;
import {scoreboardReducer} from ‘./reducers/scoreboard.resucer’;

@NgModule({
imports: [StoreModule.forRoot({game: scoreboardReducer})],
})

export class AppModule {}
使用StoreModule.forRoot()注册states可以在应用启动时定义状态。通常,您注册的state始终需要立即用于应用的所有区域。
注册形状state
形状states的行为和root state相同,但是你在你的应用中需要定义具体的形状区域。你的state是一个large object,形状state会在这个object中以键值对的形式注册。下面这个state object的例子,你将看到形状state如何以递增的方式构建你的state。让我们从一个空的state开始。
app.module.ts

@NgModule({
imports: [StoreModule.forRoot({})],
})
export class AppModule {}
这里在你的应用中创建了一个空的state
{
}
现在使用scoreboardreducer和名称为ScoreboarModule的形状NgModule注册一个额外的state。
scoreboard.module.ts

import { NgModule } from ‘@angular/core’;
import { StoreModule } from ‘@ngrx/store’;
import { scoreboardReducer } from ‘./reducers/scoreboard.reducer’;

@NgModule({
imports: [StoreModule.forFeature(‘game’, scoreboardReducer)],
})
export class ScoreboardModule {}
添加ScoreboardModule到APPModule。
app.module.ts

import { NgModule } from ‘@angular/core’;
import { StoreModule } from ‘@ngrx/store’;
import { ScoreboardModule } from ‘./scoreboard/scoreboard.module’;

@NgModule({
imports: [StoreModule.forRoot({}), ScoreboardModule],
})
export class AppModule {}
每一次ScoreboardModule被加载,这个game将会变为这个object的一个属性,并被管理在state中。
{
game: { home: 0, away: 0}
}
形状state的加载是eagerly还是lazlly的,取决于你的应用。可以使用形状状态随时间和不同形状区域构建状态对象。
select
Selector是一个获得store state的切片的纯函数。@ngrx/store提供了一些辅助函数来简化selection。selector提供了很多对state的切片功能。

轻便的
记忆化
组成的
可测试的
类型安全的

当使用createSelector和createFeatureSelector函数时,@ngrx/store会跟踪调用选择器函数的最新参数。因为选择器是纯函数,所以当参数匹配时可以返回最后的结果而不重新调用选择器函数。这可以提供性能优势,特别是对于执行昂贵计算的选择器。这种做法称为memoization。
使用selector切片state
index.ts

import {createSelector} from ‘@ngrx/store’;

export interface FeatureState {
counter: number;
}

export interface AppSatte {
feature: FeatureState;
}

export const selectFeature = (state: AppState) => state.feature;

export const selectFeatureCount = createSelector(
selectFeature,
(state: FeatrureState) => state.counter
)
使用selectors处理多切片
createSelector能够从基于同样一个state的几个切片state中获取一些数据。createSelector最多能够接受8个selector函数,以获得更加完整的state selections。在下面这个例子中,想象一下你有selectUser object 在你的state中,你还有book object的allBooks数组。你想要显示你当前用户的所有书。你能够使用createSelector来实现这些。如果你在allBooks中更新他们,你的可见的书将永远是最新的。如果选择了一本书,它们将始终显示属于您用户的书籍,并且在没有选择用户时显示所有书籍。结果将会是你从你的state中过滤一部分,并且他永远是最新的。
import {createSelecotr} from ‘@ngrx/store’;

export interface User {
id: number;
name: string;
}

export interface Book {
id: number;
userId: number;
name: string;
}

export interface AppState {
selectoredUser: User;
allBooks: Book[];
}

export const selectUser = (state: AppSate) => state.selectedUser;
export const SelectAllBooks = (state: AppState) => state.allBooks;

export const SelectVisibleBooks = createSelector(
selectUser,
selectAllBooks,
(selectedUser: User, allBooks: Books[]) => {
if(selectedUser && allBooks) {
return allBooks.filter((book: Book) => book.UserId === selectedUser.id);
}else {
return allBooks;
}
}
)
使用selecotr props
当store中没有一个适合的select来获取切片state,你可以通过selector函数的props。在下面的例子中,我们有计数器,并希望他乘以一个值,我们可以添加乘数并命名为prop:
index.ts

export const getCount = createSelector(
getCounterValue,
(counter, props) => counter * props.multiply
);
在这个组件内部,我们定义了一个props。
ngOnInit() {
this.counter = this.store.pipe(select(formRoot.getCount, {multiply: 2}));
}
记住,selector只将之前的输入参数保存在了缓存中,如果你用另一个乘数来重新使用这个selector,selector总会去重新计算它的值,这是因为他在接收两个乘数。为了正确地记忆selector,将selector包装在工厂函数中以创建选择器的不同实例
index.ts

export const getCount = () => {
createSelector(
(state, props) => state.counter[props.id],
(counter, props) => counter * props* multiply
);
}
组件的selector现在调用工厂函数来创建不同的选择器实例:
ngOnInit() {
this.counter2 = this.store.pipe(select(fromRoot.getCount(), { id: ‘counter2’, multiply: 2 }));
this.counter4 = this.store.pipe(select(fromRoot.getCount(), { id: ‘counter4’, multiply: 4 }));
this.counter6 = this.store.pipe(select(fromRoot.getCount(), { id: ‘counter6’, multiply: 6 }));
}

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理