首先看看我这个反对 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 的原创文章,尽在:” 汪子熙 ”: