记录--Loading 用户体验 - 加载时避免闪烁
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
在切换详情页中有这么一个场景,点击上一条,会显示上一条的详情页,同理,点击下一条,会显示下一条的详情页。
伪代码如下所示:
我们定义了一个 switcher
模版, 用户点击上一条、下一条时调用 goToPreOrNext
方法。该页面通过 loadingDone
状态判断是否展示加载效果。
// html <thy-loading [thyDone]="loadingDone"></thy-loading> <ng-container *ngIf="loadingDone"> <styx-pivot-detail> ... <thy-arrow-switcher ... (thyPrevious)="goToPreOrNext($event)" (thyNext)="goToPreOrNext($event)" ></thy-arrow-switcher> ... </styx-pivot-detail> </ng-container>
在 goToPreOrNext
方法中,当调用该方法时,通过 goToPreOrNextResolve
接口返回下一条的详情 id
,通过该 id
请求详情数据。
// 请求下一条 id fetchPreOrNext(event: ThyArrowSwitcherEvent) { ... this.goToPreOrNextResolve(event.index, topicId).subscribe( (id: string) => { this.getDetail(id); } ... ) } // 请求详情数据 getDetail(postId: string) { this.loadingDone = false; this.store .fetchPost(postId) .pipe( finalize(() => { this.loadingDone = true; }) ) .subscribe(); }
这看起来好像没有什么问题,应该一般都是这么干的,我们运行看看。
🌠 如何切换时候不闪?
与最上面的相比,有没有发现每次切换时,都会闪一下,用户体验很不好,有没有办法可以解决它?
这个问题就是 loadingDone
的状态切换导致的,我们把 loadingDone
干掉是不是就可以了?
如下代码所示:
// 请求详情数据 getDetail(postId: string) { // 注释掉这一行 // this.loadingDone = false; this.store .fetchPost(postId) .pipe( finalize(() => { this.loadingDone = true; }) ) .subscribe(); }
好像方案可行?
但是把网络调成低速 3G 后,会发现,我们的加载效果没了,页面像卡住了一样,这当然不行。
有没有更好的方案?
⏰ setTimeout 方案
把先前 loadingDone
状态不放到 getDetail
方法中,而是单独拿出来紧跟 this.getDetail(id)
后面。
代码如下:
// 定义一个 timer **private timer = null;** // 请求下一条 id fetchPreOrNext(event: ThyArrowSwitcherEvent) { ... this.goToPreOrNextResolve(event.index, topicId).subscribe( (id: string) => { this.getDetail(id); **this.timer = setTimeout(() => { this.loadingDone = false; }, 500);** } ... ) } // 请求详情数据 getDetail(postId: string) { // 删除掉该行loadingDone 代码 **// this.loadingDone = false;** this.store .fetchPost(postId) .pipe( finalize(() => { this.loadingDone = true; // 记得清除 **clearTimeout(this.timer);** }) ) .subscribe(); }
这么做的含义就是,我们给到 loadingDone
500ms 的缓冲时间,如果 500ms 内返回数据了,则没有 loading
的效果,如果没有加载回来,在会显示加载中。
一般情况如下所示:
低速网络下的效果:
这确实是一种方案,但是总感觉哪里怪怪的。
这里是个定时任务并且 500ms 后触发。试想一种结果,当我快速点击下一条并且在 300ms 获取到了数据并把 loadingDone 状态置为 true, 但 500ms时,loadingDone 状态置为 false,造成假死的情况,显然这不是我们想要的。
那这该如何解决?
💎 RxJS 大法
抛去使用 setTimeout
的方案,我们对 getDetail
代码改成如下的形式。
大致的思路是,将请求的 loading 状态与数据获取的状态分离,并定义了两个流 result$
和 showLoadingIndicator$
。
result$
流请求到数据之后,之后之后的一些操作, showLoadingIndicator$
流则负责 loading
状态的推送。
来看看怎么一步一步实现的:
首先我们定义一个请求的流。
const fetchPost$ = () => this.store.fetchPost(postId);
然后分别定义了两个流 result$
和 showLoadingIndicator$
。这里的 share()
函数是因为会有两个订阅它的地方。
const result$ = fetchPost$().pipe(share()); const showLoadingIndicator$;
然后我们来处理 showLoadingIndicator$
流。
我们期望在 500ms 内请求到的数据,则不应该展示 loading,否则,应该展示 loading 状态。
const showLoadingIndicator$ = timer(500).pipe(mapTo(true), takeUntil(result$))
如果在 500ms 后很快请求到了数据,为了避免闪屏,我们需要让 loading
至少显示 1s。然后使用 merge()
合并这两种结果。
const showLoadingIndicator$ = merge( timer(500).pipe(mapTo(true), takeUntil(result$)), combineLatest(result$, timer(1000)).pipe(mapTo(false)) ).pipe(startWith(false), distinctUntilChanged());
最后订阅它们。
result$.subscribe( result => { // 请求到结果后的操作 }, error => { // TODO } ); showLoadingIndicator$.subscribe(isLoading => { // 更新 loadingDone 状态 this.loadingDone = !isLoading; });
完整的代码如下:
// 请求下一条 id fetchPreOrNext(event: ThyArrowSwitcherEvent) { ... this.goToPreOrNextResolve(event.index, topicId).subscribe( (id: string) => { this.getDetail(id); } ... ) } // 请求详情数据 getDetail(postId: string) { const fetchPost$ = () => this.store.fetchPost(postId); const result$ = fetchPost$().pipe(share()); const showLoadingIndicator$ = merge( timer(500).pipe(mapTo(true), takeUntil(result$)), combineLatest(result$, timer(1000)).pipe(mapTo(false)) ).pipe(startWith(false), distinctUntilChanged()); result$.subscribe( result => { // TODO }, error => { // TODO } ); showLoadingIndicator$.subscribe(isLoading => { this.loadingDone = !isLoading; }); }
如果想更细致知道如何实现的,参考下面这篇文档:
Loading indication with a delay and anti-flickering in RxJS