# -- 第四课：Reddit API和HTML5 Video

Giflist最有趣的地方是他里面没有一个GIF文件。GIF文件尺寸很大，加载很慢，虽然用户更在意他们的数据，但是这个也是个很大的问题。\
所以我们要做的是拉取GIF提供&#x7684;**.webm**或&#x8005;**.gifv**格式。这意味着我们不会展示GIF，我们将展示的是**video视频**。\
我们将使用HTML5的*video*标签来展示这些视频。始终记住，用HTML5来制作移动应用就可以使用HTML5的所有功能。非常典型的一个例子是**Geolocation** -- 我们可以使用本机【native】API来访问设备的GPS，但是在网页上我们也可以使用HTML5自带的Geolocation API。基本上，任何网页上可以做到的事情，移动应用上也可以做到（很明显，我们可以做到更多，因为我们可以访问本机功能）。\
在实现此功能之前，我们先来熟悉一下HTML5 Video。

## HTML5 Video在iOS和Android上的行为

使用类似Ionic这样的框架意味着他们帮我们处理了平台之间的差异性，但是当制作跨平台应用的时候，还是会遇到一些平台差异性相关的问题。\
首先，对于*video*元素有一些需要知道的事情：

* 他可以全屏展示也可以嵌入使用
* 他有一个**poster**属性用于在视频加载之前作为封面展示
* 可以控制的视频的：控件是否展示，视频是否自动播放，是否嵌入页面播放

重点记住，*video*元素根据运行平台的不同，他的行为会有所不同。

* iOS上视频默认是全屏播放，但是也可以通过**webkit-playinline**属性来强制默认嵌入页面播放。同时，这也不是所有iOS设备通用的。在小的设备上，即使你指定了**webkit-playinline**属性，他还是会默认全屏播放（基本上，这个是没法解决的）。
* Android设备上默认是嵌入页面播放，但是可以设置默认全屏播放。

知道了这些不同，我们就需要找出如果解决这些问题。我们基本上有两个可选项： 1. 接受默认行为，不同平台使用相同的代码 2. 检查运行平台，运行不同代码来达到需求

个人而言，我更希望嵌入网页播放视频。但是由于在iOS小型设备上会默认全屏，我觉得还是用默认的行为好些。这意味着在iOS和Android上表现会有所不同，但是我觉得两者都很完美都可以接受，同时可以保持我们的代码简单整洁。\
好了，我们开始工作了。我们将实现**home.ts**里面的一些函数定义然后一个个的讲解。

## 从Reddit获取数据

我们从最复杂最有趣的函数开始，同时也是整个应用最重要的核心功能：*fetchData()*。我们先添加代码然后讲解。\
\&#xNAN;**> 修改 src/app/providers/reddit.ts 的 fetchData 函数为如下：**

```typescript
fetchData(): void {
    //基于用户当前偏好组装URL来访问API
    let url = 'https://www.reddit.com/r/' + this.subreddit + '/' + this.sort + '/.json?limit='+ this.perPage;
    //如果我们不是在第一页的话，我们需要加上after参数才能得到新的结果
    //这个参数基本上就是讲"把 AFTER 这个帖子的帖子给我"
    if(this.after){
        url += '&after=' + this.after;
    }
    //我们现在拉取数据，所有要将loading变量设为 true
    this.loading = true;
    //向指定的URL发起请求然后订阅他的 response
    this.http.get(url).map(res => res.json()).subscribe(data => {
        let stopIndex = this.posts.length;
        this.posts = this.posts.concat(data.data.children);
        //循环所有的 NEW 帖子。
        //我们倒序循环的原因是因为需要移除一些项。
        for(let i = this.posts.length - 1; i >= stopIndex; i--){
            let post = this.posts[i];
            //添加一个新属性用于切换单个帖子的加载动画
            post.showLoader = false;
            post.alreadyLoaded = false;
            //给 NSFW 帖子添加 NSFW 印记
            if(post.data.thumbnail == 'nsfw'){
                this.posts[i].data.thumbnail = 'images/nsfw.png';
            }
            /*
            * 移除所有非 .gifv 或者 .webm 格式的帖子，然后将保留下来的帖子转换成.mp4文件
            * 
            */
            if(post.data.url.indexOf('.gifv') > -1 ||  post.data.url.indexOf('.webm') > -1){
                this.posts[i].data.url = post.data.url.replace('.gifv', '.mp4');
                this.posts[i].data.url = post.data.url.replace('.webm', '.mp4');
                //如果有缩略图的话，将他指定到 post 的 'snapshot'
                if(typeof(post.data.preview) != "undefined"){
                    this.posts[i].data.snapshot =  post.data.preview.images[0].source.url.replace(/&amp;/g, '&');
                    //如果 snapshot 未定义的话, 将他指定为空这样就不会显示一个破裂图
                    if(this.posts[i].data.snapshot == "undefined"){
                        this.posts[i].data.snapshot = "";
                    }
                }
                else {
                    this.posts[i].data.snapshot = "";
                }
            }
            else {
                this.posts.splice(i, 1);
            }
        }
        //如果没有得到够一页的数据那么继续获取GIF
        //但是，如果连续20次都没获取足够的数据的话就放弃
        if(data.data.children.length === 0 || this.moreCount > 20){
            this.moreCount = 0;
            this.loading = false;
        } else {
            this.after = data.data.children[data.data.children.length - 1].data.name;
            if(this.posts.length < this.perPage * this.page){
                this.fetchData();
                this.moreCount++;
            }
            else {
                this.loading = false;
                this.moreCount = 0;
            }
        }
    }, (err) => {
        //静默失败，此时加载旋转动画会持续显示
        console.log("subreddit doesn't exist!");
    });
}
```

这个函数还是蛮大的。我在里面加入了一些注释来帮助理解，但是我们还是来详细讲解一下每片代码。\
首先，我们创建了用来向Reddit API获取数据的URL。我们用到了用户当前设置的**subreddit**，**sort**和**perPage**。你可以任意修改这些值，他将会输出成对应的JSON。如果有提供**after**的话，那么他也会输出到JSON。这就是Reddit API的“分页”方式，如果用户点击“Load More”按钮三次的话，我们只会返回第三页的帖子，即，如果每页5个的话就是10-15.你可以给Reddit API提供一个帖子“name”，他只会返回这个帖子后面的帖子。\
然后我们用这个URL来发起Http请求，然后在将Reddit返回结果JSON字符串JSON话成一个对象之后，订阅他的Observable。\
之后，我们可以循环返回的数据对其施展魔法。我们不需要循环存储在*this.posts*变量中的每个帖，因为其中大部分都“处理过”，我们只要处理新加载的就可以了 -- 所以我们创建了一个“stopIndex”作为*this.posts*数组的长度，然后我们将新帖子加入其中。\
循环处理新加载的帖子的时候，我们做了如下处理：

* &#x5C06;*.gifv*&#x548C;*.webm*转换&#x6210;*.mp4*
* 给帖子新增一个‘showLoader’变量用来切换加载动画的显示
* 给NSFW帖子指定NSFW标志
* 如果帖子有预览图的话给他添加一个预览图属性

循环完之后，我们就可以得到一个格式合格的帖子数组来，可以用在列表显示中了。还有一个重要的待作步骤。如果我们每页从Reddit API加载10个帖子的话，但是其中只有3个帖子适应GIF，我们的页面尺寸将变成3。这对于用户来讲就不是很友好了。\
解决这个问题的方法是我们将在*fetchData()*&#x5185;递归多次调用*fetchData()*&#x51FD;数。这样我们的帖子数组里面将会有越来越多的帖子直到填满10个（或者当前页面尺寸）为止。同时我们也的设置一个限制以防无限调用这个函数，所以在调用了20次之后还没有填满的话我们会自动放弃。\
同时我们也给*http.get*添加了错误处理器。如果请求成功将会运行上面讨论的代码，如果失败的话（即用户想要访问的subreddit返回结果404），那么将会进入错误处理器而不是上面的代码。\
现在我们有了GIF加载到应用中，我们就可以将真实数据展示到列表中。但是，首先，我们的更新模板来用于展示真实数据。\
\&#xNAN;**> 修改 src/pages/home/home.html 为如下：**

```markup
<ion-header>
    <ion-navbar color="secondary">
    <ion-title>
    <ion-searchbar color="primary" placeholder="enter subreddit name..."  [(ngModel)]="subredditValue" [formControl]="subredditControl" value=""></ion-searchbar>
    </ion-title>
    <ion-buttons end>
    <button ion-button icon-only (click)="openSettings()"><ion-icon name="settings"></ion-icon></button>
    </ion-buttons>
    </ion-navbar>
</ion-header>
<ion-content>
    <ion-list>
        <div *ngFor="let post of redditService.posts">
            <ion-item (click)="playVideo($event, post)" no-lines style="background-color: #000;">
            <img src="assets/images/loader.gif" *ngIf="post.showLoader" />
            <video loop [src]="post.data.url" [poster]="post.data.snapshot">
            </video>
            </ion-item>
            <ion-list-header (click)="showComments(post)" style="text-align: left;">
            {{post.data.title}}
            </ion-list-header>
        </div>
        <ion-item *ngIf="redditService.loading" no-lines style="text-align:center;">
            <img src="assets/images/loader.gif" style="width: 50px" />
        </ion-item>
    </ion-list>
    <button ion-button color="light" full (click)="loadMore()">Load More...</button>
</ion-content>
```

如上所示，我们把帖子的URL作为video元素的源，同时也将预览snapshot作为海报，title作为页首。我们也添加了其他一些东西。\
我们添加了一个点击处理器当视频被点击的时候调用*playVideo*，传&#x5165;*$event*（稍后解释）以及点击到的post的引用。对于有一个单独的点击事件用于在InAppBrowser中启动这个主题。\
我们也给列表添加了加载GIF动画。当用户点击视频的时候，需要一点时间来加载他，所以我们加上一个加载动画这样用户知道应用在处理一些事情。没有他的话，用户可能会觉得应用啥都没干。\
我们现在来给*loadSettings*函数加点代码，这样运行应用的时候就会调用*fetchData*（因为*loadSettings*是在构造器中调用的）。这就是应用会发生改变的做法，我们稍后来改动他。 **> 修改 src/pages/home/home.ts 的 loadSettings 函数为如下：**

```typescript
loadSettings(): void {
    this.redditService.fetchData();
}
```

如果在浏览器中重新加载应用的话，应该可以看到这样的画面。\
![预览](https://877130497-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LBtuaet5GRr7L6p_j6_%2F-LBty0R2-90Yy_bs85eB%2F-LBty4RoJppydMXuclor%2F3.4.1.jpg?generation=1525685970467945\&alt=media)

看起来还是蛮矬的，但是还是稍有改进，因为我们可以看到一些酷的GIF展示出来（现在还不能播放）。我们再改改。

## 播放GIF（视频）

现在列表里面GIF准备就绪，我们现在要让他们摇摆起来。我们所以现在来实现*playVideo*函数。\
\&#xNAN;**> 修改 src/pages/home/home.ts 的 playVideo 函数为如下：**

```typescript
playVideo(e, post): void {
    //创建视频的引用
    let video = e.target.getElementsByTagName('video')[0];
    if(!post.alreadyLoaded){
        post.showLoader = true;
    }
    //切换视频播放
    if(video.paused){
        //展示加载gif
        video.play();
        //一旦开始播放视频，隐藏加载gif
        video.addEventListener("playing", function(e){
            post.showLoader = false;
            post.alreadyLoaded = true;
        });
    } else {
        video.pause();
    }
}
```

用户点击视频的时候，我们会将视频在播放和暂停之间切换。我们用模板传入的事件来获取视频本身，然后就可以对这个视频进行播放或者暂停。此处我们也切换了*showLoader*属性以决定加载图标显示与否。我们只需要用户在第一次点击视频的时候显示加载动画，其他时间只在用户暂停的时候显示，所以在切换他之前我们先检查一下*alreadyLoaded*标记。

## 在In App Browser里启动Comments

还记得我们设置应用的时候，有安装In App Browser插件吧？我们要用上了。当用户点击视频的头的时候，我们会启动一个浏览器来展示原帖。\
\&#xNAN;**> 添加以下导入语句到 src/pages/home/home.ts 顶部：**

```typescript
import { InAppBrowser } from 'ionic-native';
```

**> 修改 src/pages/home/home.ts 的 showComments 函数为如下：**

```typescript
showComments(post): void {
    let browser = new InAppBrowser('http://reddit.com' + post.data.permalink,'_system');
}
```

跟其他函数对比，这个函数很简单了。我们简单的获取了帖子数据的连接燃油使用InAppBrowser来启动这个链接。

## 加载更多GIF

现在还有个重要的函数没有实现：*loadMore*，这个是用户点击‘LoadMore’按钮的时候调用的函数。这个函数要做的就是加载下一页的GIF。由于我们设置好了*fetchData*函数，所以这个功能就非常简单了。\
\&#xNAN;**> 添加以下函数到 src/providers/reddit.ts ：**

```typescript
nextPage(){
    this.page++;
    this.fetchData();
}
```

**> 修改 src/pages/home/home.ts 的 loadMore 函数为如下：**

```typescript
loadMore(): void {
    this.redditService.nextPage();
}
```

我们所作的只是增加页数然后重新调用*fetchData*，简单！！！

## 更换Subreddit

最后需要完成的函数是*changeSubreddit*（后面还有一点点）函数。但是，首先，我们的给**Reddit**提供者添加一个新的函数来操作subreddit的变更。\
\&#xNAN;**> 添加以下函数到 src/providers/reddit.ts 顶部：**

```typescript
resetPosts(){
    this.page = 1;
    this.posts = [];
    this.after = null;
    this.fetchData();
}
```

**> 修改 src/pages/home/home.ts 的 changeSubreddit 函数为如下：**

```typescript
changeSubreddit(): void {
    this.redditService.resetPosts();
}
```

我们早先把最难的骨头啃下来，这也是另一个非常简单的函数。当subreddit改变的时候我们需要重置所有相关事物。如果我们已经来到了‘chemicalreactiongifs’第五页的时候，我们在切换到‘perfectloops’之后就不用继续去加载第五页了。我们这里所作的就是重置页面，清理帖子数据，清理after值，然后重新调用*fetchData*函数。我们已经在他处设置了subreddit，所以调用*fetchData*的时候他会直接使用当前的subreddit。

## 总结

本课所讲内容是整个应用的大部分了，还有些许待作工作，但是现在可以稍作歇息。此时你的应用已经可以很好的工作了 -- 看起来还是很难看并且没有保存设置的能力，但是他能正常跑起来。下节课就来解决这两个问题。
