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}));
}