第三课:列表页

上一节课我们做好了所有的准备工作,我们现在就要开始进行实际创作了。本课我们将聚焦于Home页来把它改造成用来展示GIF回馈的列表页。

Reddit提供者

我们将要创建的布局严重的依赖从reddit拉取的数据,我们决定了创建一个提供者来为我们做这些操作。我们现在不会进入reddit提供者的实现细节,但是我们现在得设置好我们最终将要用到的函数,这样可以在本课中用上他们。
> 修改 src/providers/reddit.ts为如下:

  1. import { Injectable } from '@angular/core';
  2. import { Http } from '@angular/http';
  3. import 'rxjs/add/operator/map';
  4. @Injectable()
  5. export class Reddit {
  6. settings: any;
  7. loading: boolean = false;
  8. posts: any = [];
  9. subreddit: string = 'gifs';
  10. page: number = 1;
  11. perPage: number = 15;
  12. after: string;
  13. stopIndex: number;
  14. sort: string = 'hot'
  15. moreCount: number = 0;
  16. constructor(public http: Http) {
  17. }
  18. fetchData(): void {
  19. }
  20. }

这几本书就是这个提供者的股价,他定义了大量的成员变量和fetchData函数用于从reddit获取数据。成员函数的作用是什么现在可能不是很明显,我们还是(按顺序)了解一下吧:

  • 用户提供的设置settings
  • 是否加载了当前新的GIF
  • 当前加载完成的所有的GIF的入口
  • 当前的subreddit
  • 当前页(即,用户点击“Load More”的次数)
  • 每页展示的GIF的数量
  • 最后一个从Reddit拉取的帖子的引用(这样我们就知道下次从哪个页面开始)
  • 用于存储帖子数组的长度
  • GIF排列依据
  • 应用会持续向reddit拉取数据直到够一页的数据,moreCount告诉它加载更多是具体多少(即,如果在请求API20次还不够的话)

我们希望尽快完成这个提供者,但是现在我们还是先完成home页面布局。

布局

在开始制作这个布局之前,也就是home.html,我们先看一下效果图:
Giflist 2

这是一个很简单的布局,顶部一个导航条,包含一个搜索条一个设置按钮(用于启动Settings页面)。在他下面是一系列的从Reddit返回的GIF列表。还有截屏里面没有显示的列表底部的‘Load More’按钮,用户点击这个按钮的时候将会加载下一页的GIF。
首先我们看一下整个模板,然后分成各个小块来详细讲解。
> 修改 src/pages/home/home.html为如下:

  1. <ion-header>
  2. <ion-navbar color="secondary">
  3. <ion-title>
  4. <ion-searchbar color="primary" placeholder="enter subreddit name..."></ion-searchbar>
  5. </ion-title>
  6. <ion-buttons end>
  7. <button ion-button icon-only (click)="openSettings()"><ion-icon name="settings"></ion-icon></button>
  8. </ion-buttons>
  9. </ion-navbar>
  10. </ion-header>
  11. <ion-content>
  12. <ion-list>
  13. <ion-item no-lines>
  14. GIF GOES HERE
  15. </ion-item>
  16. <ion-list-header>
  17. TITLE GOES HERE
  18. </ion-list-header>
  19. <ion-item *ngIf="redditService.loading" no-lines style="text-align:center;">
  20. <img src="assets/images/loader.gif" style="width: 50px" />
  21. </ion-item>
  22. </ion-list>
  23. <button ion-button full color="light" (click)="loadMore()">Load More...</button>
  24. </ion-content>

第一部分设置了导航条:

  1. <ion-navbar color="secondary">
  2. <ion-title>
  3. <ion-searchbar color="primary" placeholder="enter subreddit name..."></ion-searchbar>
  4. </ion-title>
  5. <ion-buttons end>
  6. <button ion-button icon-only (click)="openSettings()"><ion-icon name="settings"></ion-icon></button>
  7. </ion-buttons>
  8. </ion-navbar>

我们给添加了secondary是属性这样后续会将他样式调整到使用secondary颜色。
我们用了,这个组件通常是用来在navbar中展示标题的,这里我们用来将搜索条调整到navbar的居中位置。通常每个搜索条会有一个单独的工具栏这样它会占用整个空间,但是我不想让屏幕看起来很乱所以这里用它来稍微调整一下。为了能够让他恰当的去工作,我们后续会添加一些自定义样式。我们也给搜索条提供了primary属性这样他会用primary颜色。
然后,我们使用来防止我们的设置按钮,这个按钮会启动设置页面。使用end指令可以将按钮放到右边,如果想把按钮放到左边的话那么就用start指令。我们也给按钮添加了一个(click)监听器这样在点击按钮的时候会调用openSettings函数。我们现在没有创建这个函数所以现在点击的时候什么都不会发生,但是我们稍后会在home.ts中定义。
接下来我们定义了主题内容区域:

  1. <ion-list>
  2. <ion-item no-lines>
  3. GIF GOES HERE
  4. </ion-item>
  5. <ion-list-header>
  6. TITLE GOES HERE
  7. </ion-list-header>
  8. <ion-item *ngIf="loading" no-lines style="text-align: center;">
  9. <img src="assets/images/loader.gif" style="width: 50px" />
  10. </ion-item>
  11. </ion-list>

列表是移动应用中使用最多的组件之一。Ionic中你可以通过创建一个然后给其中添加任意数量的来创建一个列表。目前我们只有一个项,后续将改为自动循环每个需要显示的GIF。同时我们也用到了来创建一个头来展示GIF的标题,同时也给项添加了no-lines属性这样列表项不会有边缘显示了。
跟GIF项一样,我们将在列表的底部添加一个额外的项,其中包含了一个加载动画。他将用于在获取新的GIF的过程中显示一个旋转动画,但是由于他只会在发生加载的时候显示,我们使用了 *ngIf 指令来控制他的显示时机。本案例中,加载动画只会在loading为true的时候才显示(这个值会后面在类中定义,然后在加载前和加载完成后对他进行更改)
模板中剩下的代码是一个加载更多按钮:

  1. <button ion-button full color="light" (click)="loadMore()">Load More...</button>

这里没什么惊心动魄的东西,给这个组件提供了一个light指令以改变他的色值,有一个(click)函数设置点击的时候调用类定义中的loadMore()函数。

类定义

在模板定义里我们解决了我们的“视图”,现在现在需要去制作类定义来处理列表页的逻辑。这里用于定义模板里面引用到的所有函数,以及其他一些页面运行的代码。
同理,我们先贴出所有代码然后一点一点的来解释。
> 修改 src/pages/home/home.ts 为如下:

  1. import { Component } from '@angular/core';
  2. import { ModalController, Platform } from 'ionic-angular';
  3. import { Keyboard } from 'ionic-native';
  4. import { SettingsPage } from '../settings/settings';
  5. import { Data } from '../../providers/data';
  6. import { Reddit } from '../../providers/reddit';
  7. import { FormControl } from '@angular/forms';
  8. import 'rxjs/add/operator/map';
  9. import 'rxjs/add/operator/debounceTime';
  10. import 'rxjs/add/operator/distinctUntilChanged';
  11. @Component({
  12. selector: 'page-home',
  13. templateUrl: 'home.html'
  14. })
  15. export class HomePage {
  16. subredditValue: string;
  17. constructor(public dataService: Data, public redditService: Reddit, public modalCtrl: ModalController, public platform: Platform) {
  18. }
  19. ionViewDidLoad(){
  20. this.platform.ready().then(() => {
  21. this.loadSettings();
  22. });
  23. }
  24. loadSettings(): void {
  25. console.log("TODO: Implement loadSettings()");
  26. }
  27. showComments(post): void {
  28. console.log("TODO: Implement showComments()");
  29. }
  30. openSettings(): void {
  31. console.log("TODO: Implement openSettings()");
  32. }
  33. playVideo(e, post): void {
  34. console.log("TODO: Implement playVideo()");
  35. }
  36. changeSubreddit(): void {
  37. console.log("TODO: Implement changeSubreddit()");
  38. }
  39. loadMore(): void {
  40. console.log("TODO: Implement loadMore()");
  41. }
  42. }

显然新加的代码不少,即使只是基本类定义看起来也挺复杂的。我们先走一遍。
首先是import语句:

  1. import { Component } from '@angular/core';
  2. import { ModalController, Platform } from 'ionic-angular';
  3. import { Keyboard } from 'ionic-native';
  4. import { SettingsPage } from '../settings/settings';
  5. import { Data } from '../../providers/data';
  6. import { Reddit } from '../../providers/reddit';
  7. import { FormControl } from '@angular/forms';
  8. import 'rxjs/add/operator/map';
  9. import 'rxjs/add/operator/debounceTime';
  10. import 'rxjs/add/operator/distinctUntilChanged';

这里有很多导入,因为我们把后续会用到的所有东西都导入进来了,但是对于后面大部分案例来讲,这算是少的了。
前面的非常基础,ModalController允许我们创建一个模态页展示到当前页面的顶部(译者:不是页面的顶部,是覆盖整个页面的上面),Platform允许我们设备加载完成之后执行一些操作。之后,我们导入了Keyboard插件(稍后使用)。
我们也导入了之前创建的SettingsPage(目前还没完成),导入了Data提供者(当前也没完善),还有未完成的Reddit提供者。FormControl允许我们为输入框创建一个“FormControl”(让我们可以访问Observable)。rxjs导入的是RxJS库 — 令人烦恼的是,你需要自己导入Observable的操作符,所以接下来导入了。操作符的使用在使用的时候再讨论。
接下来的是构造器constructor函数和ionViewDidLoad函数。构造器函数的类的重要组成部分,因为他是类实例化的时候第一个执行的函数。在其中我们可以注入和设置需要用到的组件或者服务,也是想要立即执行一些函数的最佳点。始终记住,最佳体验是不要在构造器里面做太多的“工作”,这样一些代码你可以放到ionViewDidLoad()周期函数里面去(这个函数在页面加载完成之后第一个执行)。

  1. subredditValue: string;
  2. constructor(public dataService: Data, public redditService: Reddit, public modalCtrl: ModalController, public platform: Platform) {
  3. }
  4. ionViewDidLoad(){
  5. this.platform.ready().then(() => {
  6. this.loadSettings();
  7. });
  8. }

ionViewDidLoad中我们设置了注入服务的引用(我们的Data和Reddit),在构造器的顶部我们设置了一个成员变量用于绑定到模块中的搜索条的输入框。我们也调用了loadSettings()函数将会从存储中加载用户设置。重点是我们是在平台准备好之后再调用的。
我们来看看剩下的代码:

  1. loadSettings(): void {
  2. console.log("TODO: Implement loadSettings()");
  3. }
  4. showComments(post): void {
  5. console.log("TODO: Implement showComments()");
  6. }
  7. openSettings(): void {
  8. console.log("TODO: Implement openSettings()");
  9. }
  10. playVideo(e, post): void {
  11. console.log("TODO: Implement playVideo()");
  12. }
  13. changeSubreddit(): void {
  14. console.log("TODO: Implement changeSubreddit()");
  15. }
  16. loadMore(): void {
  17. console.log("TODO: Implement loadMore()");
  18. }

剩下的只是一大堆的函数。这些方法要么是模板调用的,要么是里面其他地方调用的(构造器或者其他方法)。很明显他们现在都是白板,后面会对他们进行扩展。

使用Observable来控制搜索

我们接下来用Observable。我们已经在基础部分详细探讨过什么是Observable,如果你忘得差不多了的话,建议现在回去读一遍获取数据,Observable和Promise部分。
大部分Observable的使用就是简单的订阅Observable的返回值,例如使用Http服务获取数据(这个应用的下一部分就会用到了)。所以基本你不用自己创建一个Observable。但是如果你想要的话,还是可以用得上他提供的一些其他的方法。
我们将用到之前导入的FormControl服务来创建一个“FormControl”这样就会给咱们提供一个Observable。FormControl跟[(ngModel)]提供的创想数据绑定的工作方式很像,他们都是将类定义中的变量和模板中的输入域捆绑到一起。接下来我们做一些变更。
> 修改 src/pages/home/home.html 为如下:

  1. <ion-searchbar color="primary" placeholder="enter subreddit name..." [(ngModel)]="subredditValue" [formControl]="subredditControl" value=""></ion-searchbar>

我们这里所作只是添加了一个[formControl],他将提供给稍后创建的Control使用。接下来,我们来对类的构造器做一些改动。
> 修改 src/pages/home/home.ts 的constructor和ionViewDidLoad为如下:

  1. subredditValue: string;
  2. subredditControl: FormControl;
  3. constructor(public dataService: Data, public redditService: Reddit, public modalCtrl: ModalController, public platform: Platform) {
  4. this.subredditControl = new FormControl();
  5. }
  6. ionViewDidLoad(){
  7. this.subredditControl.valueChanges.debounceTime(1500)
  8. .distinctUntilChanged().subscribe(subreddit => {
  9. if(subreddit != '' && subreddit){
  10. this.redditService.subreddit = subreddit;
  11. this.changeSubreddit();
  12. Keyboard.close();
  13. }
  14. });
  15. this.platform.ready().then(() => {
  16. this.loadSettings();
  17. });
  18. }

首先我们创建了一个新的FormControl,然后订阅了他提供的valueChangesObservable。如果你看过基础部分的Observable的话,那么这里看起来没什么奇怪的。我们订阅subscribe了observable这样每次他提交新的值的时候都会运行这段代码。这里比较奇怪的地方是valueChanges.debounceTime(1500).distinctUntilChanged()。基本上,我们可以任意链接需要数量的操作符(因为每个函数都是返回的Observable,所以还是可以订阅)他们每一个的做的事情都不一样。例如,当我们这么做的话:

  1. this.subredditControl.valueChanges.debounceTime(1500).subscribe

我们只有在输入发生变更且1.5秒内没有新的输入的话才运行代码。这防止我们频繁提交无意义的请求到API。例如,当用户输入‘chemicalreactiongifs’的时候,代码将会触发‘c’,然后‘ch’,然后‘che’,然后‘chem’等等直到完成的单词输入完成。他不止是频繁向发送无用的查询,带来的是结果列表频繁刷新导致极差的用户体验。通过添加弹性时间,代码只会在完整的字符串输入完成之后运行一次(假设用户字符之间输入间隔用不了1.5秒)。
最后,我们添加了最终的操作符:

  1. this.subredditControl.valueChanges.debounceTime(1500).distinctUntilChanged().subscribe

这样只有值和上次的值不一样的时候才会运行代码。所以当用户输入‘gifs’,点击回退键将改成‘gif’,然后又假设‘s’将他变成‘gifs’,什么事情都不会发生。我们不需要重新为‘gifs’加载数据,因为他已经是了。
最终结果是当值发生改变的时候代码才会触发,用户现在还没有输入,输入值和之前的一样。触发的代码只是简单的检查是否提供了一个空值,然后通过更改Redditsubreddit成员变量,调用他的changeSubreddit()函数来改变活跃的subreddit。我们也调用了Keyboaid插件的close()方法;由于我们做的事情违反了输入域的规则,键盘不知道何时关闭,所以我们手动来关闭。
这可能是应用最迷惑的部分,特别是当你刚知道Observable的情况下。你也知道他允许我们很轻松的创建一些很有用的功能,但是我们也可以很简单的使用普通 ngModel 方式和使用一个按钮来触发搜索行为来替换他。

总结

本课中我们完成了一个非常好的开头,我们在制作一些比较酷的这条路上也渐渐上到。我们也有了一个很好的基础架构这样允许我们在下一课中在它只是轻松的制作功能。如果你现在通过以下命令运行应用的时候:

  1. ionic serve

应该可以看到这样的效果图:
效果图

好丑哇。甚至你还可以在控制台中看到一些错误。相信我,最后一切都会解决的!下一棵将会从reddit拉取一些真实数据。