项目:Giflist - 第四课: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设备上默认是嵌入页面播放,但是可以设置默认全屏播放。
知道了这些不同,我们就需要找出如果解决这些问题。我们基本上有两个可选项:
- 接受默认行为,不同平台使用相同的代码
- 检查运行平台,运行不同代码来达到需求
个人而言,我更希望嵌入网页播放视频。但是由于在iOS小型设备上会默认全屏,我觉得还是用默认的行为好些。这意味着在iOS和Android上表现会有所不同,但是我觉得两者都很完美都可以接受,同时可以保持我们的代码简单整洁。
好了,我们开始工作了。我们将实现home.ts里面的一些函数定义然后一个个的讲解。
从Reddit获取数据
我们从最复杂最有趣的函数开始,同时也是整个应用最重要的核心功能:fetchData()。我们先添加代码然后讲解。
> 修改 src/app/providers/reddit.ts 的 fetchData 函数为如下:
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(/&/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数组的长度,然后我们将新帖子加入其中。
循环处理新加载的帖子的时候,我们做了如下处理:
- 将.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 为如下:
<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,传入$event(稍后解释)以及点击到的post的引用。对于有一个单独的点击事件用于在InAppBrowser中启动这个主题。
我们也给列表添加了加载GIF动画。当用户点击视频的时候,需要一点时间来加载他,所以我们加上一个加载动画这样用户知道应用在处理一些事情。没有他的话,用户可能会觉得应用啥都没干。
我们现在来给loadSettings函数加点代码,这样运行应用的时候就会调用fetchData(因为loadSettings是在构造器中调用的)。这就是应用会发生改变的做法,我们稍后来改动他。
> 修改 src/pages/home/home.ts 的 loadSettings 函数为如下:
loadSettings(): void {
this.redditService.fetchData();
}
如果在浏览器中重新加载应用的话,应该可以看到这样的画面。
看起来还是蛮矬的,但是还是稍有改进,因为我们可以看到一些酷的GIF展示出来(现在还不能播放)。我们再改改。
播放GIF(视频)
现在列表里面GIF准备就绪,我们现在要让他们摇摆起来。我们所以现在来实现playVideo函数。
> 修改 src/pages/home/home.ts 的 playVideo 函数为如下:
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插件吧?我们要用上了。当用户点击视频的头的时候,我们会启动一个浏览器来展示原帖。
> 添加以下导入语句到 src/pages/home/home.ts 顶部:
import { InAppBrowser } from 'ionic-native';
> 修改 src/pages/home/home.ts 的 showComments 函数为如下:
showComments(post): void {
let browser = new InAppBrowser('http://reddit.com' + post.data.permalink,'_system');
}
跟其他函数对比,这个函数很简单了。我们简单的获取了帖子数据的连接燃油使用InAppBrowser来启动这个链接。
加载更多GIF
现在还有个重要的函数没有实现:loadMore,这个是用户点击‘LoadMore’按钮的时候调用的函数。这个函数要做的就是加载下一页的GIF。由于我们设置好了fetchData函数,所以这个功能就非常简单了。
> 添加以下函数到 src/providers/reddit.ts :
nextPage(){
this.page++;
this.fetchData();
}
> 修改 src/pages/home/home.ts 的 loadMore 函数为如下:
loadMore(): void {
this.redditService.nextPage();
}
我们所作的只是增加页数然后重新调用fetchData,简单!!!
更换Subreddit
最后需要完成的函数是changeSubreddit(后面还有一点点)函数。但是,首先,我们的给Reddit提供者添加一个新的函数来操作subreddit的变更。
> 添加以下函数到 src/providers/reddit.ts 顶部:
resetPosts(){
this.page = 1;
this.posts = [];
this.after = null;
this.fetchData();
}
> 修改 src/pages/home/home.ts 的 changeSubreddit 函数为如下:
changeSubreddit(): void {
this.redditService.resetPosts();
}
我们早先把最难的骨头啃下来,这也是另一个非常简单的函数。当subreddit改变的时候我们需要重置所有相关事物。如果我们已经来到了‘chemicalreactiongifs’第五页的时候,我们在切换到‘perfectloops’之后就不用继续去加载第五页了。我们这里所作的就是重置页面,清理帖子数据,清理after值,然后重新调用fetchData函数。我们已经在他处设置了subreddit,所以调用fetchData的时候他会直接使用当前的subreddit。
总结
本课所讲内容是整个应用的大部分了,还有些许待作工作,但是现在可以稍作歇息。此时你的应用已经可以很好的工作了 — 看起来还是很难看并且没有保存设置的能力,但是他能正常跑起来。下节课就来解决这两个问题。