首先看看我这个反对 infinite scroll 的 Angular 利用的运行时成果:

https://jerry-infinite-scroll...

滚动鼠标中键,向下滚动,能够触发 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$ Observable 对象。这样,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的原创文章,尽在:"汪子熙":