写点什么

使用 RxJs 实现一个支持 infinite scroll 的 Angular Component

作者:Jerry Wang
  • 2022 年 10 月 03 日
    四川
  • 本文字数:2929 字

    阅读完需:约 10 分钟

使用 RxJs 实现一个支持 infinite scroll 的 Angular Component

首先看看我这个支持 infinite scroll 的 Angular 应用的运行时效果:


https://jerry-infinite-scroller.stackblitz.io/


滚动鼠标中键,向下滚动,可以触发 list 不断向后台发起请求,加载新的数据:



下面是具体的开发步骤。


(1) app.component.html 的源代码:


<div>  <h2>{{ title }}</h2>  <ul    id="infinite-scroller"    appInfiniteScroller    scrollPerecnt="70"    [immediateCallback]="true"    [scrollCallback]="scrollCallback"  >    <li *ngFor="let item of news">{{ item.title }}</li>  </ul></div>
复制代码


这里我们给列表元素 ul 施加了一个自定义指令 appInfiniteScroller,从而为它赋予了支持 infinite scroll 的功能。


[scrollCallback]="scrollCallback" 这行语句,前者是自定义执行的 input 属性,后者是 app Component 定义的一个函数,用于指定当 list 的 scroll 事件发生时,应该执行什么样的业务逻辑。


app component 里有一个类型为集合的属性 news,被 structure 指令 ngFor 展开,作为列表行项目显示。


(2) app Component 的实现:


import { Component } from '@angular/core';import { HackerNewsService } from './hacker-news.service';
import { tap } from 'rxjs/operators';
@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'],})export class AppComponent { currentPage: number = 1; title = ''; news: Array<any> = [];
scrollCallback;
constructor(private hackerNewsSerivce: HackerNewsService) { this.scrollCallback = this.getStories.bind(this); }
getStories() { return this.hackerNewsSerivce .getLatestStories(this.currentPage) .pipe(tap(this.processData)); // .do(this.processData); }
private processData = (news) => { this.currentPage++; this.news = this.news.concat(news); };}
复制代码



把函数 getStories 绑定到属性 scrollCallback 上去,这样当 list scroll 事件发生时,调用 getStories 函数,读取新一页的 stories 数据,将结果合并到数组属性 this.news 里。读取 Stories 的逻辑位于 hackerNewsService 里完成。


(3) hackerNewsService 通过依赖注入的方式被 app Component 消费。


import { Injectable } from '@angular/core';import { HttpClient } from '@angular/common/http';
const BASE_URL = 'https://node-hnapi.herokuapp.com';
@Injectable()export class HackerNewsService { constructor(private http: HttpClient) {}
getLatestStories(page: number = 1) { return this.http.get(`${BASE_URL}/news?page=${page}`); }}
复制代码


(4) 最核心的部分就是自定义指令。


import { Directive, AfterViewInit, ElementRef, Input } from '@angular/core';
import { fromEvent } from 'rxjs';
import { pairwise, map, exhaustMap, filter, startWith } from 'rxjs/operators';
interface ScrollPosition { sH: number; sT: number; cH: number;}
const DEFAULT_SCROLL_POSITION: ScrollPosition = { sH: 0, sT: 0, cH: 0,};
@Directive({ selector: '[appInfiniteScroller]',})export class InfiniteScrollerDirective implements AfterViewInit { private scrollEvent$;
private userScrolledDown$;
// private requestStream$;
private requestOnScroll$;
@Input() scrollCallback;
@Input() immediateCallback;
@Input() scrollPercent = 70;
constructor(private elm: ElementRef) {}
ngAfterViewInit() { this.registerScrollEvent();
this.streamScrollEvents();
this.requestCallbackOnScroll(); }
private registerScrollEvent() { this.scrollEvent$ = fromEvent(this.elm.nativeElement, 'scroll'); }
private streamScrollEvents() { this.userScrolledDown$ = this.scrollEvent$.pipe( map( (e: any): ScrollPosition => ({ sH: e.target.scrollHeight, sT: e.target.scrollTop, cH: e.target.clientHeight, }) ), pairwise(), filter( (positions) => this.isUserScrollingDown(positions) && this.isScrollExpectedPercent(positions[1]) ) ); }
private requestCallbackOnScroll() { this.requestOnScroll$ = this.userScrolledDown$;
if (this.immediateCallback) { this.requestOnScroll$ = this.requestOnScroll$.pipe( startWith([DEFAULT_SCROLL_POSITION, DEFAULT_SCROLL_POSITION]) ); }
this.requestOnScroll$ .pipe( exhaustMap(() => { return this.scrollCallback(); }) ) .subscribe(() => {}); }
private isUserScrollingDown = (positions) => { return positions[0].sT < positions[1].sT; };
private isScrollExpectedPercent = (position) => { return (position.sT + position.cH) / position.sH > this.scrollPercent / 100; };}
复制代码


首先定义一个 ScrollPosition 接口,包含三个字段 sH, sT 和 cH,分别维护 scroll 事件对象的三个字段:scrollHeight,scrollTop 和 clientHeight.


我们从施加了自定义指令的 dom 元素的 scroll 事件,构造一个 scrollEvent 会自动 emit 出事件对象。



因为这个事件对象的绝大多数属性信息,我们都不感兴趣,因此使用 map 将 scroll 事件对象映射成我们只感兴趣的三个字段:scrollHeight, scrollTop 和 clientHeight:



但是仅仅有这三个点的数据,我们还无法判定当前 list 的 scroll 方向。



所以使用 pairwise 这个 rxjs 提供的操作符,将每两次点击生成的坐标放到一个数组里,然后使用函数 this.isUserScrollingDown 来判断,当前用户 scroll 的方向。


如果后一个元素的 scrollTop 比前一个元素大,说明是在向下 scroll:


  private isUserScrollingDown = (positions) => {    return positions[0].sT < positions[1].sT;  };
复制代码


我们并不是检测到当前用户向下 scroll,就立即触发 HTTP 请求加载下一页的数据,而是得超过一个阀值才行。


这个阀值的实现逻辑如下:


private isScrollExpectedPercent = (position) => {    console.log('Jerry position: ', position);    const reachThreshold =      (position.sT + position.cH) / position.sH > this.scrollPercent / 100;    const percent = ((position.sT + position.cH) * 100) / position.sH;    console.log('reach threshold: ', reachThreshold, ' percent: ', percent);    return reachThreshold;  };
复制代码


如下图所示:当阀值到达 70 的时候,返回 true:



总结

本文介绍了如何使用 Angular 前端框架,基于 Rxjs 工具库开发一个支持 infinite scroll 的延迟加载列表的详细步骤。


发布于: 刚刚阅读数: 3
用户头像

Jerry Wang

关注

🏆InfoQ写作平台-签约作者🏆 2017.12.03 加入

SAP成都研究院开发专家,SAP社区导师,SAP中国技术大使。2007 年从电子科技大学计算机专业硕士毕业后加入 SAP 成都研究院工作至今。工作中使用 ABAP, Java, JavaScript 和 TypeScript 进行开发。

评论

发布
暂无评论
使用 RxJs 实现一个支持 infinite scroll 的 Angular Component_前端开发_Jerry Wang_InfoQ写作社区