Skip to content

Commit 54d1662

Browse files
committed
add service + rxjs
1 parent 4f4ca0c commit 54d1662

4 files changed

+280
-0
lines changed

frontend/.DS_Store

0 Bytes
Binary file not shown.
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
## 实战演练
2+
3+
产品原型图以及前端组件结构如下所示:
4+
![产品原型图](https://ws1.sinaimg.cn/large/006tKfTcgy1g0ppsonpmpj31y40qbjwt.jpg)
5+
6+
经过分析后,数据状态流向示意图如下:
7+
![数据状态流向示意图](https://ws1.sinaimg.cn/large/006tKfTcgy1g0pptuszkuj30z108uwfr.jpg)
8+
9+
### 状态改变的几个节点
10+
11+
- 发起 Http 请求获取源数据:
12+
- SearchBar 组件点击查询按钮
13+
- DateTabs 组件选择新的日期
14+
- 筛选条件过滤处理:
15+
- 源数据发生改变时
16+
- 筛选条件发生改变时
17+
- Filter 组件数据发生变化
18+
- Sort 组件中的指定数据(仅显示有余票车次)发生变化 (不要问我为什么这里会有这么个条件,我们伟大的产品经理从用户体验的角度考虑的,一切服从产品经理的最高指挥!!!)
19+
- 排序条件进行排序
20+
- 过滤数据发生改变时
21+
- Sort 组件数据发生变化
22+
- 最终数据,也就是排序后的数据
23+
- List 组件订阅数据, 每次得到新数据,就重新渲染。
24+
25+
### Service 的核心结构
26+
27+
```ts
28+
// data.service.ts
29+
import { BehaviorSubject, Observable, combineLatest } from 'rxjs'
30+
import { map } from 'rxjs/operators';
31+
@Injectable({
32+
providedIn: 'root',
33+
})
34+
export class DataService {
35+
private searchParams$ = new BehaviorSubject<TrainSearchBo>() // 查询条件状态
36+
private filterParams$ = new Subject<FilterCondition>() // 过滤条件状态
37+
private sortType$ = new BehaviorSubject<SortTypeEnum>() // 排序条件状态
38+
private showTrains$: Observable<TrainInfoBo[]> // 最终数据状态
39+
private trainDate$ = new BehaviorSubject<Date | undefined>(undefined) // 出发日期,用于 SearchBar 和 DateTabs 通信
40+
41+
constructor (private httpService:TrainHttpService) {
42+
this.initShowTrains()
43+
}
44+
45+
private initShowTrains(): void {
46+
const trains$ = this.searchParams$.pipe(siwthMap(params => this.httpService.search(params)))
47+
48+
const filteredTrains$ = combineLatest(
49+
trains$,
50+
this.filterParams$,
51+
).pipe(
52+
map(([trains,filterParams]) => this.filterTrains(trains, filterParams))
53+
)
54+
55+
this.showTrains$ = combineLatest(
56+
filteredTrains$,
57+
this.sortType$,
58+
).pipe(
59+
map(([filteredTrains,sortType])=> this.sortTrains(filteredTrains, sortType)),
60+
)
61+
}
62+
63+
updateSearchParams(params: TrainSearchBo): void {
64+
this.searchParams$.next(params)
65+
}
66+
67+
updateFilterParams(params: FilterCondition): void {
68+
this.filterParams$.next(params)
69+
}
70+
71+
updateSortType(params: SortTypeEnum): void {
72+
this.sortType$.next(params)
73+
}
74+
75+
getShowTrains(): Observable<TrainInfoBo[]>{
76+
return this.showTrains$
77+
}
78+
79+
updateTrainDate(date: Date): void {
80+
this.trainDate$.next(date)
81+
}
82+
83+
getTrainDate(date: Date): Observable<Date> {
84+
return this.trainDate$.asObservable()
85+
}
86+
87+
private filterTrains() { /* xxx */ }
88+
private sortTrains() { /* xxx */ }
89+
}
90+
```
91+
92+
- 利用 [Subject](https://www.learnrxjs.io/subjects/subject.html) 的特点, 将**查询参数****过滤条件****排序条件**转换为3个可观察对象 (observable)。
93+
- 利用 [combineLatest](https://www.learnrxjs.io/operators/combination/combinelatest.html) 操作符,将上述的3个 observable 组合起来,等到每一个 observable 都发出一个值后,combineLatest 首次发出初始值。之后任意一个 observable 发出值,combineLatest 都会发出每个 observable 的最新值。
94+
- 利用 [siwthMap](https://www.learnrxjs.io/operators/transformation/switchmap.html) 操作符拿到参数发起 http 请求获取源数据。
95+
- 利用 [map](https://www.learnrxjs.io/operators/transformation/map.html) 操作符根据过滤条件和排序条件对源数据进行处理,发出最终数据。
96+
- 利用 [BehaviorSubject](https://www.learnrxjs.io/subjects/behaviorsubject.html) 的特点, 在保存数据的同时,还可以对外提供一个可订阅的对象用于获取数据,用于 SearchBar 和 DateTabs 进行通信。
97+
98+
### Components 的核心结构
99+
100+
```ts
101+
// search-bar.component.ts
102+
export class SearchBarComponent{
103+
form: FormGroup
104+
private unsubscribe$ = new Subject()
105+
106+
constructor(private dataService: DataService) {}
107+
108+
ngOnInit() {
109+
// 订阅联动日期
110+
this.dataService.getTrainDate()
111+
.pipe(takeUntil(this.unsubscribe$))
112+
.subscribe(
113+
value=>{
114+
if(value && value !== this.trainDate.value ){
115+
this.trainDate.setValue(value, { emitEvent: false })
116+
this.dataService.updateSearchParam(this.form.value)
117+
}
118+
}
119+
)
120+
121+
// 日期发生改变 更新 service 中的状态
122+
this.trainDate.valueChanges.subscribe(
123+
value=> this.dataService.updateTrainDate(value)
124+
)
125+
}
126+
127+
ngOnDestroy() {
128+
this.unsubscribe$.next()
129+
}
130+
131+
// 点击查询按钮 更新 trainDate 和 searchParam
132+
onSearchBtnClick() {
133+
const formValue = this.form.value
134+
this.dateService.updateTrainDate(formValue.trainDate)
135+
this.dataService.updateSearchParam(formValue)
136+
}
137+
138+
get trainDate(): FormControl {
139+
return this.form.get('trainDate')
140+
}
141+
}
142+
143+
// 其他几个组件就不声明了
144+
// DateTabsComponent 和 SearchBarComponent 类似
145+
// FilterComponent 和 SortComponent 更为简单,监听组件内数据发生变化后,
146+
// 调用 dataService 相应的update 方法即可,类似上面的 onSearchBtnClick
147+
148+
// list.component.ts
149+
export class ListComponent {
150+
showTrains$: Observable<TrainInfoBo[]>
151+
152+
constructor(private dataService: DataService) {}
153+
154+
ngOnInit() {
155+
this.showTrains$ = this.dataService.getShowTrains()
156+
}
157+
158+
// 使用 trackBy 优化 ngfor 指令
159+
trackByTrainNo = (_: number, train: TrainInfoBo) => train.trainNo
160+
}
161+
```
162+
163+
- 可以明显感觉到在各个 Component 内部的逻辑是比较简单的,只要注入 DataService ,然后当自身的状态发生变化时,调用 DataService 相应的 update 方法即可。
164+
- 如果有组件间的通信也是通过订阅 DataService 相应的 get 方法,然后更新自身的状态。
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
---
2+
layout: post
3+
title: 使用 Service + Rxjs 进行 Angular 的状态管理
4+
date: 2019-03-01
5+
author: Vm
6+
catalog: true
7+
8+
tags:
9+
- Angular
10+
- Rxjs
11+
---
12+
13+
# 使用 Service + Rxjs 进行 Angular 的状态管理
14+
15+
在 React 和 Vue 的应用中,状态管理库似乎是全家桶中必不可少的一环,其中以 Redux 和 Vuex 最为常见。
16+
虽然 Angular 的第三方库中也有一个类似的 Ngrx, 但是却处于一个可有可无的地位。
17+
18+
## 为什么在 Angular 中,状态管理库不是必须的呢?
19+
20+
因为 Angular 中内置了两大利器: Service 和 Rxjs
21+
22+
- 在 Angular 我们只要定义了一个 Service, 就可以通过依赖注入的方式在组件中使用它。
23+
- 这个 Service 通常都是单例的,我们把数据保存在 Service 中,那么组件间很轻松的就能共享数据。
24+
- 我们可以把与数据相关的处理定义在 Service 中,交给组件的数据就是组件最终渲染的数据。
25+
- 可以参考官方示例[英雄编辑器](https://stackblitz.com/angular/vkglbnmmbojm?file=src%2Fapp%2Fhero.service.ts) 中的 `HeroService`, 多个组件都注入了它,去获取同一份英雄列表的数据。
26+
27+
如果仅仅只有 Service 的话,那也是不够用的,举个栗子: 一个 input 输入框组件,根据输入内容请求接口,查询回数据后传递给一个 table 组件进行渲染。
28+
这个需求,我们需要小心点几点有:
29+
30+
- 防抖和确认输入内容是否发生改变,这是为了给服务端降低压力,我们需要减少没必要的请求。
31+
- 网络请求相应的不稳定性,我们需要保证最终 table 组件渲染在页面上的数据是最后一次请求响应的结果。
32+
- 如何把最终的数据传递给 table 组件。
33+
34+
针对上面3点我们可能需要做的事情有:
35+
36+
- 定义 防抖函数 或者使用第三方库,例如:underscore.debounce()
37+
- 比较前后两次value的函数:
38+
39+
```ts
40+
let preValue
41+
function isChange(value) {
42+
if(preValue !== value){
43+
preValue = value
44+
return true
45+
}
46+
return false
47+
}
48+
```
49+
50+
- 要保证请求结果的准确性的话,搜索到的几种解决方案都比较 hack,或者直接牺牲用户体验(在发起请求的时候,就不允许用户输入)。
51+
- 可以使用 @Output 到父组件接收,然后 table 组件通过 @Input 来获取。
52+
53+
上面的解决方案中的代码有这么几个缺点:
54+
55+
- 要实现保证请求结果的准确性的话,要么牺牲用户体验,要么实现复杂度较高
56+
- 需要定义一些易被污染的变量(preValue)
57+
- 耦合了父组件(input 和 table 必须在同一个父组件内)
58+
59+
那么如何优雅的解决上述的问题呢,这个时候我们的另一个利器是时候展现它真正的技术了。
60+
61+
```ts
62+
// data.service.ts
63+
import { BehaviorSubject, Observable } from 'rxjs'
64+
class DataService{
65+
private data$ = new BehaviorSubject<DataType>()
66+
updateData(data){
67+
this.data$.next(data)
68+
}
69+
getData():Observable<DataType>{
70+
return this.data$.asObservable()
71+
}
72+
}
73+
74+
// input.component.ts
75+
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
76+
import { DataService } from './data.service'
77+
class InputComponent{
78+
constructor(private dataService:DataService){}
79+
this.input.valueChanges.pipe(
80+
debounceTime(300), // 防抖
81+
distinctUntilChanged(), // 确定发生变化
82+
switchMap((value: string) => this.httpClient.post(api,value)),// 取消前一次的请求结果 发起一次新的请求
83+
)
84+
.subscribe(data => this.dataService.updateData(data)) // 拿到最终请求结果 通知data service 更新数据
85+
}
86+
87+
// table.component.ts
88+
import { DataService } from './data.service'
89+
class TableComponent{
90+
constructor(private dataService:DataService){}
91+
this.data$ = this.dataService.getData()
92+
}
93+
```
94+
95+
在上面的代码中:
96+
97+
- 定义了一个 Service ,利用 Rxjs 的 [BehaviorSubject](https://www.learnrxjs.io/subjects/behaviorsubject.html) 的特点, 在保存数据的同时,还可以对外提供一个可订阅的对象用于获取数据。
98+
- 在 InputComponent 中使用了(debounceTime,distinctUntilChanged,switchMap)等内置操作符来达到我们对请求的优化和保证数据的正确性。
99+
- debounceTime: 舍弃掉在两次输出之间小于指定时间的发出值
100+
- distinctUntilChanged: 当前值与之前最后一个值不同时才将其发出
101+
- switchMap: 映射成 observable,完成前一个内部 observable,发出值。
102+
- 可以取消上一次订阅,保证网络请求结果的准确性
103+
- 在 TableComponent 中只要注入一下 DataService 就可以轻易的拿到我们想要的数据。
104+
- 假设后续有另一个 ListComponent 也需要这一份数据,不用修改之前的任何代码,只需要新建在 ListComponent 中注入 DataService 然后获取数据即可。
105+
106+
代码简洁,链式调用,组件解耦,易于维护...总之,吹爆。
107+
这二者的结合基本满足了我们在实际开发过程中对于状态管理与组件通信的需求。
108+
109+
## 总结
110+
111+
在 Angular 应用中,通过 Service 来管理状态,把需要共享的数据通过 Observable 包装起来,提供相应的订阅接口即可。这样的状态流非常的简单清晰,易于维护。
112+
对于组件通信,在 Angular 中有多种方式,我建议的方式是:
113+
114+
- 对于父 => 子,使用 @Input()
115+
- 对于子 => 父,使用 @Output() EventEmitter。(其实 EventEmitter 就是一个 Observable 对象)
116+
- 对于其他情况,在没有特殊需求的条件下,尽可能的使用 Service + Rxjs 的方式通信,让组件解耦。

0 commit comments

Comments
 (0)