第四课:Reddit API和HTML5 Video

Giflist最有趣的地方是他里面没有一个GIF文件。GIF文件尺寸很大,加载很慢,虽然用户更在意他们的数据,但是这个也是个很大的问题。
所以我们要做的是拉取GIF提供的.webm或者.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()。我们先添加代码然后讲解。
> 修改 src/app/providers/reddit.ts 的 fetchData 函数为如下:

  1. fetchData(): void {
  2. //基于用户当前偏好组装URL来访问API
  3. let url = 'https://www.reddit.com/r/' + this.subreddit + '/' + this.sort + '/.json?limit='+ this.perPage;
  4. //如果我们不是在第一页的话,我们需要加上after参数才能得到新的结果
  5. //这个参数基本上就是讲"把 AFTER 这个帖子的帖子给我"
  6. if(this.after){
  7. url += '&after=' + this.after;
  8. }
  9. //我们现在拉取数据,所有要将loading变量设为 true
  10. this.loading = true;
  11. //向指定的URL发起请求然后订阅他的 response
  12. this.http.get(url).map(res => res.json()).subscribe(data => {
  13. let stopIndex = this.posts.length;
  14. this.posts = this.posts.concat(data.data.children);
  15. //循环所有的 NEW 帖子。
  16. //我们倒序循环的原因是因为需要移除一些项。
  17. for(let i = this.posts.length - 1; i >= stopIndex; i--){
  18. let post = this.posts[i];
  19. //添加一个新属性用于切换单个帖子的加载动画
  20. post.showLoader = false;
  21. post.alreadyLoaded = false;
  22. //给 NSFW 帖子添加 NSFW 印记
  23. if(post.data.thumbnail == 'nsfw'){
  24. this.posts[i].data.thumbnail = 'images/nsfw.png';
  25. }
  26. /*
  27. * 移除所有非 .gifv 或者 .webm 格式的帖子,然后将保留下来的帖子转换成.mp4文件
  28. *
  29. */
  30. if(post.data.url.indexOf('.gifv') > -1 || post.data.url.indexOf('.webm') > -1){
  31. this.posts[i].data.url = post.data.url.replace('.gifv', '.mp4');
  32. this.posts[i].data.url = post.data.url.replace('.webm', '.mp4');
  33. //如果有缩略图的话,将他指定到 post 的 'snapshot'
  34. if(typeof(post.data.preview) != "undefined"){
  35. this.posts[i].data.snapshot = post.data.preview.images[0].source.url.replace(/&/g, '&');
  36. //如果 snapshot 未定义的话, 将他指定为空这样就不会显示一个破裂图
  37. if(this.posts[i].data.snapshot == "undefined"){
  38. this.posts[i].data.snapshot = "";
  39. }
  40. }
  41. else {
  42. this.posts[i].data.snapshot = "";
  43. }
  44. }
  45. else {
  46. this.posts.splice(i, 1);
  47. }
  48. }
  49. //如果没有得到够一页的数据那么继续获取GIF
  50. //但是,如果连续20次都没获取足够的数据的话就放弃
  51. if(data.data.children.length === 0 || this.moreCount > 20){
  52. this.moreCount = 0;
  53. this.loading = false;
  54. } else {
  55. this.after = data.data.children[data.data.children.length - 1].data.name;
  56. if(this.posts.length < this.perPage * this.page){
  57. this.fetchData();
  58. this.moreCount++;
  59. }
  60. else {
  61. this.loading = false;
  62. this.moreCount = 0;
  63. }
  64. }
  65. }, (err) => {
  66. //静默失败,此时加载旋转动画会持续显示
  67. console.log("subreddit doesn't exist!");
  68. });
  69. }

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

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

循环完之后,我们就可以得到一个格式合格的帖子数组来,可以用在列表显示中了。还有一个重要的待作步骤。如果我们每页从Reddit API加载10个帖子的话,但是其中只有3个帖子适应GIF,我们的页面尺寸将变成3。这对于用户来讲就不是很友好了。
解决这个问题的方法是我们将在fetchData()内递归多次调用fetchData()函数。这样我们的帖子数组里面将会有越来越多的帖子直到填满10个(或者当前页面尺寸)为止。同时我们也的设置一个限制以防无限调用这个函数,所以在调用了20次之后还没有填满的话我们会自动放弃。
同时我们也给http.get添加了错误处理器。如果请求成功将会运行上面讨论的代码,如果失败的话(即用户想要访问的subreddit返回结果404),那么将会进入错误处理器而不是上面的代码。
现在我们有了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..." [(ngModel)]="subredditValue" [formControl]="subredditControl" value=""></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. <div *ngFor="let post of redditService.posts">
  14. <ion-item (click)="playVideo($event, post)" no-lines style="background-color: #000;">
  15. <img src="assets/images/loader.gif" *ngIf="post.showLoader" />
  16. <video loop [src]="post.data.url" [poster]="post.data.snapshot">
  17. </video>
  18. </ion-item>
  19. <ion-list-header (click)="showComments(post)" style="text-align: left;">
  20. {{post.data.title}}
  21. </ion-list-header>
  22. </div>
  23. <ion-item *ngIf="redditService.loading" no-lines style="text-align:center;">
  24. <img src="assets/images/loader.gif" style="width: 50px" />
  25. </ion-item>
  26. </ion-list>
  27. <button ion-button color="light" full (click)="loadMore()">Load More...</button>
  28. </ion-content>

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

  1. loadSettings(): void {
  2. this.redditService.fetchData();
  3. }

如果在浏览器中重新加载应用的话,应该可以看到这样的画面。
预览

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

播放GIF(视频)

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

  1. playVideo(e, post): void {
  2. //创建视频的引用
  3. let video = e.target.getElementsByTagName('video')[0];
  4. if(!post.alreadyLoaded){
  5. post.showLoader = true;
  6. }
  7. //切换视频播放
  8. if(video.paused){
  9. //展示加载gif
  10. video.play();
  11. //一旦开始播放视频,隐藏加载gif
  12. video.addEventListener("playing", function(e){
  13. post.showLoader = false;
  14. post.alreadyLoaded = true;
  15. });
  16. } else {
  17. video.pause();
  18. }
  19. }

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

在In App Browser里启动Comments

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

  1. import { InAppBrowser } from 'ionic-native';

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

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

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

加载更多GIF

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

  1. nextPage(){
  2. this.page++;
  3. this.fetchData();
  4. }

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

  1. loadMore(): void {
  2. this.redditService.nextPage();
  3. }

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

更换Subreddit

最后需要完成的函数是changeSubreddit(后面还有一点点)函数。但是,首先,我们的给Reddit提供者添加一个新的函数来操作subreddit的变更。
> 添加以下函数到 src/providers/reddit.ts 顶部:

  1. resetPosts(){
  2. this.page = 1;
  3. this.posts = [];
  4. this.after = null;
  5. this.fetchData();
  6. }

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

  1. changeSubreddit(): void {
  2. this.redditService.resetPosts();
  3. }

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

总结

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