美文网首页js css html
使用 RxJs 实现一个支持 infinite scroll 的

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

作者: _扫地僧_ | 来源:发表于2022-01-24 10:37 被阅读0次

    首先看看我这个支持 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 事件,构造一个 scrollEventObservable 对象。这样,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:

    更多Jerry的原创文章,尽在:"汪子熙":


    相关文章

      网友评论

        本文标题:使用 RxJs 实现一个支持 infinite scroll 的

        本文链接:https://www.haomeiwen.com/subject/vdoacrtx.html