[譯] Angular: 使用 RxJS Observables 來實現簡易版的無限滾動載入指令

SangKa發表於2018-03-15

原文連結: 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個生命週期鉤子。

[譯] Angular: 使用 RxJS Observables 來實現簡易版的無限滾動載入指令

對於這個指令,我們想要進入 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…

如果你喜歡本文的話,歡迎分享、評論及點 ? 。

相關文章