Angular: 使用 RxJS Observables 来实现简易版的无限滚动加载指令
我使用 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个输入值:
scrollPercent- 用户需要滚动到容器的百分比,达到后方可调用scrollCallback。scrollCallback- 返回 observable 的回调函数。immediateCallback- 布尔值,如果为 true 则指令初始化后会立即调用scrollCallback。
Angular 为组件和指令提供了4个生命周期钩子。
对于这个指令,我们想要进入 ngAfterViewInit 生命周期钩子以注册和处理滚动事件。在 constructor 中,我们注入了 ElementRef,它可以让我们引用应用了指令的元素,即滚动容器。
constructor(private elm: ElementRef) { }
ngAfterViewInit() {
    this.registerScrollEvent();  
    this.streamScrollEvents();
    this.requestCallbackOnScroll();
}
在 ngAfterViewInit 生命周期钩子中,我们执行了3个函数:
registerScrollEvent- 使用Observable.fromEvent来监听元素的滚动事件。streamScrollEvents- 根据我们的需求来处理传入的滚动事件流,当滚动到给定的容器高度百分比时发起 API 请求。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 指令的容器,同时还传入了参数 scrollPercent、immediateCallback 和 scrollCallback。每个 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 指令的无限滚动加载,注意观察右边的滚动条。
Angular: 使用 RxJS Observables 来实现简易版的无限滚动加载指令的更多相关文章
- 使用 Angular 和 RxJS 实现的无限滚动加载
		
无限滚动加载应该是怎样的? 无限滚动加载列表在用户将页面滚动到指定位置后会异步加载数据.这是避免寻主动加载(每次都需要用户去点击)的好方法,而且它能真正保持应用的性能.同时它还是降低带宽和增强用户体验 ...
 - 简易数据分析 10 | Web Scraper 翻页——抓取「滚动加载」类型网页
		
这是简易数据分析系列的第 10 篇文章. 友情提示:这一篇文章的内容较多,信息量比较大,希望大家学习的时候多看几遍. 我们在刷朋友圈刷微博的时候,总会强调一个『刷』字,因为看动态的时候,当把内容拉到屏 ...
 - Angular ui-roter 和AngularJS 通过 ocLazyLoad 实现动态(懒)加载模块和依赖
		
什么是ui-router ui-router是AngularUI库最有用的组件之一(AngularUI库由AngularJS社区构建).它是一个第三方路由框架,允许通过状态机制组织接口,而不是简单的U ...
 - win7(旗舰版)下,OleLoadPicture 加载内存中的图片(MagickGetImageBlob),返回值 < 0
		
昨天去三哥家,想把拍好的照片缩小一下,我用很久前写的一个软件进行缩小,然后进行一次效果预览,这个时候弹出: Call OleLoadPicture Fail - loadPictureFromMW 奇 ...
 - WebGL简易教程(十五):加载gltf模型
		
目录 1. 概述 2. 实例 2.1. 数据 2.2. 程序 2.2.1. 文件读取 2.2.2. glTF格式解析 2.2.3. 初始化顶点缓冲区 2.2.4. 其他 3. 结果 4. 参考 5. ...
 - angular源码分析:angular的整个加载流程
		
在前面,我们讲了angular的目录结构.JQLite以及依赖注入的实现,在这一期中我们将重点分析angular的整个框架的加载流程. 一.从源代码的编译顺序开始 下面是我们在目录结构哪一期理出的an ...
 - .NET Core的文件系统[5]:扩展文件系统构建一个简易版“云盘”
		
FileProvider构建了一个抽象文件系统,作为它的两个具体实现,PhysicalFileProvider和EmbeddedFileProvider则分别为我们构建了一个物理文件系统和程序集内嵌文 ...
 - MVC 验证码实现( 简易版)
		
现在网站上越来越多的验证码,使用场景也是越来越多,登陆.注册.上传.下载...等等地方,都有可能大量使用到验证码,那么制作验证码到底有多简单呢?我们一起来看下最简易版的验证码实现过程- 验证码的基本步 ...
 - 简易版自定义BaseServlet
		
这几天在学Java Web,一直在思考Servlet重用的问题,就用java的反射机制实现自定义的简易版BaseServlet; 该方式有点像struts2 利用映射获取前端的参数.有兴趣的同学可以自 ...
 
随机推荐
- .Net Core IFormFile 始终为空的问题
			
之前获取上传文件都是使用Request.Form.Files获取,直到这次改成定义形参 IFormFile时才遇到这个问题. // POST api/values [HttpPost] public ...
 - 虚拟化 - VMware
			
和VirtualBox一样,也需要关掉Hyper-V才能启动虚拟机,否则会报Guard的错误. 网络 [转]VMware网络连接模式-桥接.NAT以及仅主机模式的详细介绍和区别 桥接 就好像在局域网中 ...
 - kolla-ansible安装openstack(Ocata)
			
基本功能部署 基础环境 角色 操作系统 硬件配置 Depoly CentOS 7 Server 磁盘:40GB 内存:8GB 网卡:ens3(内网) ens4(外网) Sched CentOS 7 S ...
 - Metasploit域渗透测试全程实录(终结篇)
			
本文作者:i春秋签约作家——shuteer 前言 内网渗透测试资料基本上都是很多大牛的文章告诉我们思路如何,但是对于我等小菜一直是云里雾里.于是使用什么样的工具才内网才能畅通无阻,成了大家一直以来的渴 ...
 - 网站架构:PHP针对并发访问如何优化?
			
1.拆表:大表拆小表(垂直拆,水平拆:分表,分区partition,分片sharding),可以在应用层实现,也可以在数据库层面实现一部分:提高系统性能. 2.分库:把表放到不同的数据库,这也是分布式 ...
 - redis cluster 的ERR max number of clients reached 问题排查
			
早上发现微服务连不上redis cluster了,看来下日志如下 [root@win-jrh378d7scu 7005]# bin/redis-cli -c -h 15.31.213.183 -p 7 ...
 - 【Hadoop & Ecilpse】Exception in thread "main" org.apache.hadoop.security.AccessControlException: Permission denied: user=bruce, access=WRITE, inode="/out2/_temporary/0":atguigu:supergroup:drwxr-xr-x
			
问题再现: 使用本机 Ecilpse (Windows环境) 去访问远程 hadoop 集群出现以下异常: 问题原因: 因为远程提交的情况下如果没有 hadoop 的系统环境变量,就会读取当前主机的 ...
 - python中的字符串和编码
			
了解编码之前首先说下这几个词的概率: 位.字节.字符.字符串 1.位(bit)也称为比特 这个其实很简单,因为计算机都是二进制存储数据,也就是0和1,一个0或者1就表示一位.这是计算机存储的最小单位. ...
 - HDU - 3085 Nightmare Ⅱ
			
HDU - 3085 Nightmare Ⅱ 双向BFS,建立两个队列,让男孩女孩一起走 鬼的位置用曼哈顿距离判断一下,如果该位置与鬼的曼哈顿距离小于等于当前轮数的两倍,则已经被鬼覆盖 #includ ...
 - P5038 [SCOI2012]奇怪的游戏
			
题目链接 题意分析 首先我们需要求的是统一以后的值\(x\) 并且一般的棋盘操作我们都需要黑白染色 那么对于棋盘格子是偶数的情况的话 答案是存在单调性的 因为如果统一之后 两两搭配还是可以再加一个的 ...