[译] Angular: 使用 RxJS Observables 来实现简易版的无限滚动加载指令

原文链接: codeburst.io/angular-2-s…

本文为 RxJS 中文社区 翻译文章,如需转载,请注明出处,谢谢合作!

如果你也想和我们一起,翻译更多优质的 RxJS 文章以奉献给大家,请点击【这里】

这篇文章是我上篇文章 使用响应式编程来实现简易版的无限滚动加载 的延续。在本文中,我们将创建一个 Angular 指令来实现无限滚动加载功能。我们还将继续使用 HackerNews 的非官方 API 来获取数据以填充到页面中。

我使用 angular-cli 来搭建项目。

ng new infinite-scroller-poc --style=scss
复制代码

项目生成好后,进入 infinite-scroller-poc 目录下。

Angular CLI 提供了一堆命令用来生成组件、指令、服务和模块。

我们来生成一个服务和一个指令。

ng g service hacker-news
ng g directive infinite-scroller
复制代码

注意: Angular CLI 会自动在 app.module.ts 里注册指令,但不会将服务添加到 providers 数组中。你需要手动添加。app.module.ts 如下所示。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';
import { InfiniteScrollerDirective } from './infinite-scroller.directive';
import { HackerNewsService } from './hacker-news.service';
@NgModule({
  declarations: [
    AppComponent,
    InfiniteScrollerDirective
  ],
  imports: [
    BrowserModule,
    HttpModule
  ],
  providers: [HackerNewsService],
  bootstrap: [AppComponent]
})
export class AppModule { }
复制代码

接下来,我们在服务中添加 HackerNews 的 API 调用。下面是 hacker-news.service.ts,它只有一个函数 getLatestStories

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';

const BASE_URL = 'http://node-hnapi.herokuapp.com';

@Injectable()
export class HackerNewsService {
  
  constructor(private http: Http) { }

  getLatestStories(page: number = 1) {
    return this.http.get(`${BASE_URL}/news?page=${page}`);
  }
}
复制代码

现在来构建我们的无限滚动加载指令。下面是指令的完整代码,别担心代码太长,我们会分解来看。

import { Directive, AfterViewInit, ElementRef, Input } from '@angular/core';

import { Observable, Subscription } from 'rxjs/Rx';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/pairwise';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/exhaustMap';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/startWith';

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$ = Observable.fromEvent(this.elm.nativeElement, 'scroll');

  }

  private streamScrollEvents() {
    this.userScrolledDown$ = this.scrollEvent$
      .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$
        .startWith([DEFAULT_SCROLL_POSITION, DEFAULT_SCROLL_POSITION]);
    }

    this.requestOnScroll$
      .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);
  }

}
复制代码

指令接收3个输入值:

  1. scrollPercent - 用户需要滚动到容器的百分比,达到后方可调用 scrollCallback
  2. scrollCallback - 返回 observable 的回调函数。
  3. immediateCallback - 布尔值,如果为 true 则指令初始化后会立即调用 scrollCallback

Angular 为组件和指令提供了4个生命周期钩子。

对于这个指令,我们想要进入 ngAfterViewInit 生命周期钩子以注册和处理滚动事件。在 constructor 中,我们注入了 ElementRef,它可以让我们引用应用了指令的元素,即滚动容器。

constructor(private elm: ElementRef) { }

ngAfterViewInit() {

    this.registerScrollEvent();  

    this.streamScrollEvents();

    this.requestCallbackOnScroll();

}
复制代码

ngAfterViewInit 生命周期钩子中,我们执行了3个函数:

  1. registerScrollEvent - 使用 Observable.fromEvent 来监听元素的滚动事件。
  2. streamScrollEvents - 根据我们的需求来处理传入的滚动事件流,当滚动到给定的容器高度百分比时发起 API 请求。
  3. requestCallbackOnScroll - 一旦达到我们设定的条件后,调用 scrollCallback 来发起 API 请求。

还有一个可选的输入条件 immediateCallback,如果设置为 true 的话,我们会将 DEFAULT_SCROLL_POSITION 作为流的起始数据,它会触发 scrollCallback 而无需用户滚动页面。这样的话会调用一次 API 以获取初始数据展示在页面中。上述所有函数的作用都与我的上篇文章中是一样的,上篇文章中已经详细地解释了 RxJS Observable 各个操作符的用法,这里就不赘述了。

接下来将无限滚动指令添加到 AppComponent 中。下面是 app.component.ts 的完整代码。

import { Component } from '@angular/core';
import { HackerNewsService } from './hacker-news.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  currentPage: number = 1;

  news: Array<any> = [];

  scrollCallback;

  constructor(private hackerNewsSerivce: HackerNewsService) {

    this.scrollCallback = this.getStories.bind(this);

   }

  getStories() {
    return this.hackerNewsSerivce.getLatestStories(this.currentPage).do(this.processData);
  }

  private processData = (news) => {
    this.currentPage++;
    this.news = this.news.concat(news.json());
  }

}
复制代码

getStories - 调用 hackerNewsService 并处理返回数据。

注意 constructor 中的 this.scrollCallback 写法

this.scrollCallback = this.getStories.bind(this);
复制代码

我们将 this.getStories 函数赋值给 scrollCallback 并将其上下文绑定为 this 。这样可以确保当回调函数在无限滚动指令里执行时,它的上下文是 AppComponent 而不是 InfiniteScrollerDirective 。更多关于 .bind 的用法,可以参考这里

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

html 想当简单,ul 作为 appInfiniteScroller 指令的容器,同时还传入了参数 scrollPercentimmediateCallbackscrollCallback。每个 li 表示一条新闻,并只显示新闻的标题。

为容器设置基础样式。

#infinite-scroller {
  height: 500px;
  width: 700px;
  border: 1px solid #f5ad7c;
  overflow: scroll;
  padding: 0;
  list-style: none;

  li {
    padding : 10px 5px;
    line-height: 1.5;
    &:nth-child(odd) {
      background : #ffe8d8;
    }
    &:nth-child(even) {
      background : #f5ad7c;
    }
  }
}
复制代码

下面的示例是使用了 Angular 指令的无限滚动加载,注意观察右边的滚动条。

在线示例: ashwin-sureshkumar.github.io/angular-inf…

我无法将 gif 图片上传到此处。这是 gif 图片的链接: giphy.com/gifs/xTiN0F…

如果你喜欢本文的话,欢迎分享、评论及点 ? 。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
参数说明 listLoadingMore({id:"",id2:function(){},action:function(){},pageNum:10,getNew:undefined,funcArg:undefined,func:function(){},loadingMustTime:0,loadingDom:function(){},loadedDom:function(){},nullDataFunc:function(){},endFunc:function(){},errorFunc:function(){}}); id:滚动条id,不可以是body; id2:滚动列表的id,通过function自行根据情况返回; action:数据来源的ajax地址,插件会自动加上page和length(分别表示请求页数和当前已有记录数),返回格式统一是[{},{}...],此类jsonArray; ajaxType:ajax提交方式,将更改传递数据的方式,默认post; pageNum:每页加载数量,将会根据这个数量判断是否全部加载完成; getNew:自定义获取数据方法; funcArg:自定义获取数据时传递的参数,类型为函数,返回所需参数; childrenTag:列表子项的标签,默认LI; func:非自定义获取数据时,创建每行数据的方法; loadingMustTime:强制最小加载时间,默认0; loadingDom:加载中动画自定义,返回Dom或者HTML代码; loadedDom:完全加载完成动画自定义,返回Dom或者HTML代码; nullDataFunc:数据列表为空时执行的方法; endFunc:每页数据加载完成时执行的方法; errorFunc:ajax获取数据失败时调用的方法。 回调方法 调用本方法初始化后将会得到一个专属的方法集: Object {getNew:function,isFull:function,isRunning:function,isStop:function,loading:function,show:function}; getNew():忽略判断条件,强制执行加载操作。此时数据传递中,page值可能出错,但length值一定正确,请谨慎判断后在执行该方法; isFull(boolean):修改列表加载完成标志。true为全部加载完成,false则相反; isRunning(boolean):修改列表当前是否正在加载中标志。几乎没有使用的机会; isStop(boolean):设置是否停止加载,设置为true时将会阻止未来的所有加载操作; loading():根据当前滚动条的位置,自动判断是否需要加载新数据; show(boolean,boolean):强制设置加载中和加载完成dom的显示或隐藏,true表示显示,false相对。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值