深入探索 ngrx:状态管理与开发实践
1. 引入 ngrx
在开发应用时,我们可以使用 ngrx 来管理应用状态。下面将通过一个具体的应用开发过程,介绍 ngrx 的核心概念和使用方法,包括 ngrx 存储、带负载的动作、reducer、效果和选择器。
2. 使用 ngrx 重构中介应用
我们要重构一个之前的应用,该应用有一个搜索字段和两个链接(eBay 和 Amazon)。重构时,将使用 ngrx 存储替换原来维护应用状态的 SearchService,并添加 ProductService 来生成搜索结果。
新应用将使用以下 ngrx 构建块:
- 存储(Store):用于存储和检索应用状态、搜索查询和结果。
- 减速器(Reducer):处理 SEARCH 和 SEARCH_SUCCESS 类型的动作。
- 效果(Effects):处理 SEARCH 和 SEARCH_SUCCESS 类型的动作。
- 选择器(Selectors):检索整个状态对象、搜索查询或搜索结果。
应用的状态对象包含两个属性:搜索查询和搜索结果。以下是状态对象的类型声明:
export interface State {
searchQuery: string;
searchResults: string[];
}
3. 动作(Actions)
在这个应用中,动作可以携带负载。例如,SEARCH 动作可以携带搜索查询作为负载,SEARCH_SUCCESS 动作可以携带搜索结果作为负载。因此,我们将每个动作封装到一个带有构造函数的类中,构造函数接受负载作为参数。动作的声明如下:
import {Action} from '@ngrx/store';
export const SEARCH = '[Product] search';
export const SEARCH_SUCCESS = '[Product] search success';
export class SearchAction implements Action {
readonly type = SEARCH;
constructor(public payload: {searchQuery: string}) {}
}
export class SearchSuccessAction implements Action {
readonly type = SEARCH_SUCCESS;
constructor(public payload: {searchResults: string[]}) {}
}
export type SearchActions = SearchAction | SearchSuccessAction;
在动作定义中,使用 [Product] 作为前缀,为动作创建了命名空间,使代码更易读。
4. 搜索组件作为动作创建者
SearchComponent 负责在用户输入搜索条件后创建并分发 SEARCH 动作。以下是 SearchComponent 的代码:
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Store } from '@ngrx/store';
import { SearchAction } from './actions';
@Component({
selector: 'app-search',
template: `
<h2>Search component</h2>
<input type="text" placeholder="Enter product" [formControl]="searchInput">
`,
styles: ['.main {background: yellow}']
})
export class SearchComponent {
searchInput: FormControl;
constructor(private store: Store<any>) {
this.searchInput = new FormControl('');
this.searchInput.valueChanges
.pipe(
debounceTime(300),
tap(value => console.log(`The user entered ${value}`))
)
.subscribe(searchValue => {
this.store.dispatch(new SearchAction({ searchQuery: searchValue }));
});
}
}
分发的动作将被 reducer 捕获,reducer 会更新状态对象中的搜索查询属性。
5. 减速器(Reducer)
reducer 负责根据动作类型更新状态对象。以下是 reducer 的代码:
import {SearchActions, SEARCH, SEARCH_SUCCESS} from './actions';
export interface State {
searchQuery: string;
searchResults: string[];
}
const initialState: State = {
searchQuery: '',
searchResults: []
};
export function reducer(state = initialState, action: SearchActions): State {
switch (action.type) {
case SEARCH: {
return {
...state,
searchQuery: action.payload.searchQuery,
searchResults: []
};
}
case SEARCH_SUCCESS: {
return {
...state,
searchResults: action.payload.searchResults
};
}
default: {
return state;
}
}
}
在 reducer 中,使用 spread 运算符克隆状态对象并更新其属性。
6. 效果(Effects)
在这个应用中,我们有一个效果,使用 ProductService 来获取产品。ProductService 会生成并返回一个包含五个产品的可观察对象,并使用 RxJS 的 delay 运算符模拟一秒的延迟。以下是 ProductService 的代码:
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
@Injectable()
export class ProductService {
static counter = 0;
getProducts(searchQuery: string): Observable<string[]> {
const productGenerator = () =>
`Product ${searchQuery}${ProductService.counter++}`;
const products = Array.from({length: 5}, productGenerator);
return of(products).pipe(delay(1000));
}
}
SearchEffects 类定义了一个效果 loadProducts$ ,用于在接收到 SEARCH 动作后获取产品并分发 SEARCH_SUCCESS 动作。以下是 SearchEffects 的代码:
import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
import { Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { ProductService } from './product.service';
import { SearchAction, SearchSuccessAction, SEARCH } from './actions';
@Injectable()
export class SearchEffects {
@Effect()
loadProducts$: Observable<any> = this.actions$
.ofType(SEARCH)
.pipe(
map((action: SearchAction) => action.payload),
switchMap(({searchQuery}) => this.productService.getProducts(searchQuery)),
map(searchResults => new SearchSuccessAction({searchResults}))
);
constructor(private actions$: Actions, private productService: ProductService) {}
}
在这个效果中,使用 ofType(SEARCH) 过滤出 SEARCH 动作,提取其负载并传递给 ProductService 的 getProducts 方法,最后分发 SEARCH_SUCCESS 动作。
7. 选择器(Selectors)
在实际应用中,状态对象可能是一个嵌套属性的树结构,我们可以使用选择器来获取特定的状态切片。以下是选择器的代码:
import {createFeatureSelector, createSelector} from '@ngrx/store';
import {State} from './reducers';
export const getState = createFeatureSelector<State>('myReducer');
export const getSearchQuery = createSelector(getState, state => state.searchQuery);
export const getSearchResults = createSelector(getState, state => state.searchResults);
通过选择器,我们可以方便地获取状态对象的特定属性。
8. 总结已完成的工作
- 声明了代表 SEARCH 和 SEARCH_RESULTS 类型动作的类。
- SearchComponent 可以分发 SEARCH 类型的动作。
- reducer 可以处理这两种动作类型。
- 声明了可以获取产品并分发 SEARCH_RESULTS 类型动作的效果。
- 声明了用于获取应用状态切片的选择器。
9. 使用选择器渲染搜索结果
在 eBay 和 Amazon 组件中,使用选择器来渲染搜索条件和检索到的产品。以下是 EbayComponent 的代码:
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { State, getSearchQuery, getSearchResults } from './reducers';
@Component({
selector: 'app-ebay',
template: `
<div class="ebay">
<h2>eBay component</h2>
Search criteria: {{searchFor$ | async}}
<ul>
<li *ngFor="let p of searchResults$ | async ">{{ p }}</li>
</ul>
</div>
`,
styles: ['.ebay {background: cyan}']
})
export class EbayComponent {
searchFor$ = this.store.select(getSearchQuery);
searchResults$ = this.store.select(getSearchResults);
constructor(private store: Store<State>) {}
}
10. 注册存储和效果
最后,需要在应用模块中注册存储和效果。以下是应用模块的代码:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { environment } from '../environments/environment';
import { AppComponent } from './app.component';
import { EbayComponent } from './ebay.component';
import { AmazonComponent } from './amazon.component';
import { SearchComponent } from './search.component';
import { reducer } from './reducers';
import { SearchEffects } from './effects';
import { ProductService } from './product.service';
import { HashLocationStrategy, LocationStrategy } from '@angular/common';
@NgModule({
imports: [
BrowserModule,
CommonModule,
ReactiveFormsModule,
RouterModule.forRoot([
{path: '', component: EbayComponent},
{path: 'amazon', component: AmazonComponent}
]),
StoreModule.forRoot({myReducer: reducer}),
EffectsModule.forRoot([SearchEffects]),
StoreDevtoolsModule.instrument({
logOnly: environment.production
})
],
declarations: [AppComponent, EbayComponent, AmazonComponent, SearchComponent],
providers: [
ProductService,
{provide: LocationStrategy, useClass: HashLocationStrategy}
],
bootstrap: [AppComponent]
})
export class AppModule {}
以下是整个应用的流程 mermaid 流程图:
graph LR
A[用户输入搜索条件] --> B[SearchComponent 分发 SEARCH 动作]
B --> C[Reducer 更新搜索查询]
C --> D[SearchEffects 监听 SEARCH 动作]
D --> E[ProductService 获取产品]
E --> F[SearchEffects 分发 SEARCH_SUCCESS 动作]
F --> G[Reducer 更新搜索结果]
G --> H[EbayComponent 和 AmazonComponent 使用选择器获取结果并渲染]
为了更清晰地展示各部分的关系,我们可以用表格总结:
| 组件/模块 | 功能 |
| ---- | ---- |
| SearchComponent | 接收用户输入,分发 SEARCH 动作 |
| Reducer | 根据动作类型更新状态对象 |
| SearchEffects | 监听 SEARCH 动作,调用 ProductService 获取产品,分发 SEARCH_SUCCESS 动作 |
| ProductService | 生成并返回产品的可观察对象 |
| EbayComponent 和 AmazonComponent | 使用选择器获取搜索结果并渲染 |
| AppModule | 注册存储、效果和路由等 |
深入探索 ngrx:状态管理与开发实践
11. ngrx 其他可用功能
除了上述使用的 @ngrx/store 和 @ngrx/effects 包,ngrx 还有其他一些有用的包:
- @ngrx/entity :是一个实体状态适配器,用于管理记录集合。
- @ngrx/schematics :是一个脚手架库,提供生成与 ngrx 相关代码的蓝图。
12. 使用 Redux DevTools 监控状态
由于将状态管理操作委托给了 ngrx,因此需要一个工具来在运行时监控状态变化。可以使用浏览器扩展 Redux DevTools 以及 @ngrx/store-devtools 包来对应用状态进行检测。具体操作步骤如下:
1. 安装 @ngrx/store-devtools :
npm install @ngrx/store-devtools
- 添加 Chrome 扩展 Redux DevTools :在 Chrome 浏览器的扩展商店中搜索并添加该扩展,Firefox 浏览器也有相应的扩展。
- 在应用模块中添加检测代码 :
- 默认配置 :在
@NgModule装饰器的imports部分添加以下代码:
- 默认配置 :在
StoreDevtoolsModule.instrument()
- **生产环境优化配置**:为了在生产环境中减少性能开销,可以使用环境变量:
StoreDevtoolsModule.instrument({
logOnly: environment.production
})
在生产环境中,将 logOnly 标志设置为 true ,这样可以关闭一些会引入明显性能开销的功能,如分发和重新排序动作、在页面重新加载之间持久化状态和动作历史等。完整的禁用功能列表可查看 http://mng.bz/cOwC 。
- **自定义配置示例**:以下代码允许监控最多 25 个最近的动作,并在生产环境中以仅日志模式工作:
StoreDevtoolsModule.instrument({
maxAge: 25,
logOnly: environment.production
})
还可以通过为 instrument() 方法提供 features 参数来限制 Chrome Redux 扩展的某些功能。更多关于配置 ngrx 检测和支持的 API 的详细信息可查看 http://mng.bz/3AXe 。
13. Redux DevTools 功能展示
- 初始状态 :启动应用后,Chrome Redux 面板可能会显示一个黑色窗口并提示 “No store found”,此时刷新页面即可。初始状态下,会有两个由
@ngrx/store和@ngrx/effects内部分发的init动作。选择@ngrx/store/init动作和右上角的State按钮,可以看到状态属性searchQuery和searchResults为空。 - 查看动作信息 :点击右上角的
Action按钮,可以看到最新动作的类型和负载。例如,最新动作可能是[Product] search success,动作负载存储在状态属性searchResults中。 - 查看状态信息 :点击
State按钮,可以看到状态变量searchQuery和searchResults的当前值。 - 查看状态差异 :如果
State标签显示整个状态对象,点击Diff按钮可以显示特定动作导致的状态变化。如果没有选择动作,Diff标签将显示最新动作引起的状态变化。 - 时间旅行调试 :在调试应用时,开发人员经常需要重新创建应用的特定状态。使用 Redux DevTools 可以在不刷新页面的情况下进行时间旅行,重新创建特定状态。选择一个动作后,会看到
Jump和Skip按钮。点击Skip会划掉所选动作,运行中的应用会反映此更改;点击Sweep按钮会将该动作从列表中移除;点击Jump按钮会跳转到所选动作对应的应用状态,Redux DevTools 会显示该时刻的状态属性,应用的 UI 也会相应地重新渲染。
下面用 mermaid 流程图展示使用 Redux DevTools 的流程:
graph LR
A[安装 @ngrx/store-devtools] --> B[添加 Chrome 扩展 Redux DevTools]
B --> C[在应用模块添加检测代码]
C --> D[启动应用并刷新页面]
D --> E[查看初始状态]
E --> F[点击 Action 按钮查看动作信息]
F --> G[点击 State 按钮查看状态信息]
G --> H[点击 Diff 按钮查看状态差异]
H --> I[选择动作进行时间旅行调试]
为了更清晰地展示 Redux DevTools 的操作和功能,我们用表格总结:
| 操作 | 功能 |
| ---- | ---- |
| 安装 @ngrx/store-devtools | 为应用添加状态检测功能 |
| 添加 Chrome 扩展 Redux DevTools | 提供可视化界面监控状态 |
| 在应用模块添加检测代码 | 配置检测功能 |
| 刷新页面 | 解决 “No store found” 问题 |
| 点击 Action 按钮 | 查看最新动作的类型和负载 |
| 点击 State 按钮 | 查看状态变量的当前值 |
| 点击 Diff 按钮 | 查看特定动作导致的状态变化 |
| 选择动作后点击 Skip | 划掉所选动作,应用反映更改 |
| 选择动作后点击 Sweep | 将动作从列表中移除 |
| 选择动作后点击 Jump | 跳转到所选动作对应的应用状态 |
通过以上对 ngrx 的介绍和实践,我们可以看到 ngrx 为 Angular 应用的状态管理提供了强大而灵活的解决方案,结合 Redux DevTools 可以更方便地进行调试和监控。希望这些内容能帮助你更好地使用 ngrx 来管理应用状态。
超级会员免费看
324

被折叠的 条评论
为什么被折叠?



