项目场景(angular):
根据用户输入的内容对表格数据进行搜索显示
问题描述:
用户输入姓名对人员信息名单进行搜索,每输入一个字符,均会向后端进行请求,从数据库中获取符合条件的数据,当用户通过页面操作频繁请求该接口,而接口的不同参数响应时间差异较大时,易产生数据渲染混乱的问题。
说明:有一个姓名搜索框,用户可输入进行人员列表搜索,当用户快速的输入张三,前端会先调一个参数是“张”的列表接口,然后再掉一次参数是“张三”的相同列表接口,第一次调用响应时间是3秒,第二次调用响应时间是1秒,因此会先返回第二次调用的响应数据,然后再返回第一次响应数据,第一次的响应数据就会覆盖掉第二次响应数据,页面上输入框里的搜索内容是“张三”,而表格内显示的数据却是根据“张”搜索返回的数据,出现了页面的混乱
原因分析:
1、用户操作过于快速以及频繁,导致短时间内多次发送同一接口不同参数的请求
2、不同参数的查询操作导致后端接口响应时间不同
解决方案:
方案一:增加loading或禁用,适用于按钮操作
在点击按钮请求接口后,可启用loading或禁用按钮,禁止用户进行其他操作,直至响应数据返回后才可进行下步操作。
- 设置loading
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-test',
template: `
<div [loading]="loading">
<button nz-button (click)="getData(1)">1</button>
<button nz-button (click)="getData(2)">2</button>
<div>{{data}}</div>
</div>
`,
styleUrls: ['./test.component.scss']
})
export class TestComponent implements OnInit{
loading=false;
data;
constructor() {}
ngOnInit() {}
getData(param){
this.loading=true;
this.service.getData(param).pipe(finalize(() => {
this.loading = false;
})).subscribe((res) => {
if (res.code === '2000') {
this.data= res.data.data;
}
});
}
}
- 禁用按钮
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-test',
template: `
<div>
<button nz-button [disabled]="disabled" (click)="getData(1)">1</button>
<button nz-button [disabled]="disabled" (click)="getData(2)">2</button>
<div>{{data}}</div>
</div>
`,
styleUrls: ['./test.component.scss']
})
export class TestComponent implements OnInit{
disabled=false;
data;
constructor() {}
ngOnInit() {}
getData(param){
this.disabled=true;
this.service.getData(param).pipe(finalize(() => {
this.disabled= false;
})).subscribe((res) => {
if (res.code === '2000') {
this.data= res.data.data;
}
});
}
}
优点:简单
缺点:当响应时间越长,用户体验感越差,页面会更长时间处于无法操作的状态
方案二:利用定时器
根据用户行为可知,用户只需要最新一次操作的数据,我们只要发送最后一次请求即可。
方法:
1、设置500ms的定时器(定时时间可根据情况自定义)
2、每次用户操作此接口时先清除未执行的定时器,通过定时器延迟执行请求
结果:
用户在500ms内执行的相同操作时,未执行的定时器将被清理,始终只会保留最新的操作请求,若500ms内没有相同操作时,则会发送该请求
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-test',
template: `
<div>
<input nz-input [(ngModel)]="value" (ngModelChange)=getData(value)/>
<div>{{data}}</div>
</div>
`,
styleUrls: ['./test.component.scss']
})
export class TestComponent implements OnInit{
value=null;
timer;
data;
constructor() {}
ngOnInit() {}
getData(param){
if(this.timer) clearTimeout(this.timer);
this.timer = setTimeout(()=>{
this.service.getData(param).subscribe((res) => {
if (res.code === '2000') {
this.data= res.data.data;
}
});
},500);
}
}
优点:减少无用请求,节省一定性能,在一定程度上解决问题
缺点:无法完全避免问题,该方案是通过设置容差,可解决数据响应时间差在500ms内的问题,但当数据响应时间差大于设置的时间,此问题仍将存在
方案三(方案二基础上优化,需后端配合)
- 在方案二的基础上,做一个计时器,初始值为0
- 每一次发送请求前为计时器做一个递加操作,发送请求时将该值作为参数传给后台
- 接收响应数据时后台将该值再返回来
- 根据前端存储的计数器的值与后端返回来的值作比较
- 只有二者相等时说明返回的是最后一次用户操作的数据
- 若二者不相等,则返回的数据不是最后一次操作的数据。
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-test',
template: `
<div>
<input nz-input [(ngModel)]="value" (ngModelChange)=getData(value)/>
<div>{{data}}</div>
</div>
`,
styleUrls: ['./test.component.scss']
})
export class TestComponent implements OnInit{
value=null;
count = 0;
timer;
data;
constructor() {}
ngOnInit() {}
getData(param){
if(this.timer) clearTimeout(this.timer);
this.timer = setTimeout(()=>{
const payload = {
param:param,
count:count++
};
this.service.getData(payload).subscribe((res) => {
if (res.code === '2000' && res.data.count===this.count) {
this.data= res.data.data;
}
});
},500);
}
}
优点:结合方案二的优点,并在此基础上解决方案二的缺点,即使响应时间差超过的设定的时间,仍可保证页面渲染的数据是最新请求的数据
缺点:需后端配合,非纯前端处理
方案四(angualr http)【angular项目推荐使用】
<input nz-input [(ngModel)]="value" (ngModelChange)="search()"/>
<ul>
<li *ngFor="let package of packages$ | async">
<b>{{package.name}} v.{{package.version}}</b> -
<i>{{package.description}}</i>
</li>
</ul>
getList(payload): Observable<any> {
return this.http.post<any>(`/...`, payload);
}
value;
detailData = [];
private searchText$ = new Subject<string>();
constructor(
private service: TestService,
) {
}
ngOnInit(): void {
this.debounceList();
this.getList();
}
debounceList() {
this.searchText$.pipe(
debounceTime(500), // 等待,直到用户停止输入(500ms)
distinctUntilChanged(), // 等待,直到搜索内容发生了变化
switchMap(obj =>
this.service.getList(obj)) // 把搜索请求发送给服务
).subscribe(res => {
if (res.code === '2000') {
this.loading = false;
this.detailData = res.data.datalist;
} else {
this.loading = false;
}
}, error => {
this.loading = false;
});
}
getList(reset = false) {
this.loading = true;
const payload = {
value:this.value
};
this.searchText$.next(payload);
}
searchText$ 是一个序列,包含用户输入到搜索框中的所有值。 它定义成了 RxJS 的 Subject 对象,这表示它是一个多播 Observable,同时还可以自行调用 next(value) 来产生值。
除了把每个 searchText 的值都直接转发给 Service 之外,ngOnInit() 中的代码还通过下列三个操作符对这些搜索值进行管道处理,以便只有当它是一个新值并且用户已经停止输入时,要搜索的值才会抵达该服务。
- debounceTime(500)—等待用户停止输入(500毫秒)。
- distinctUntilChanged()—等待搜索文本发生变化。
- switchMap()—将搜索请求发送到服务。
switchMap() 操作符有三个重要的特征:
- 其参数是一个返回 Observable 的函数,service.getList会返回 Observable
- 若以前的搜索结果仍然是在途状态,会取消那个请求,并发起一次新的搜索。
- 会按照原始的请求顺序返回这些服务的响应,而不用关心服务器实际上是以乱序返回的请求
优点:减少无用请求,节省性能,能很好的解决当前问题,无需后端配合,不用修改原本接口
缺点:仅适用于angular项目
方案五(未实际操作过)
网上查找大量资料,大多数都提到了axios 中的一个属性‘cancelToken’
可参考