您的位置:首页 > 理论基础 > 计算机网络

第八章 HTTP

2016-06-02 00:21 429 查看

注:学习使用,禁止转载

介绍

angular有它自己的Http库,我们可以使用他们去调用外部的数据和API。

当我们去请求外部数据的时候,我们希望页面能够与用户继续保持交互。也就是说,我们不希望我们的页面一直冻结到外部数据返回,为了完成这个,我们的HTTP请求时异步的。

历史证明,处理异步代码比处理同步代码复杂得多。

在javascript中,有三种方式去处理异步代码:

Callbacks

Promises

Observables

在angular2中,首选的方式就是使用Observables,因此,这一章我们会讲解这部分的内容。

在这章中,我们学习:

展示一个HTTP的基本例子

创建一个YouTuBe的search-as-you-type组件

讨论Http库里面的API细节



:fa-info-circle:这一张的完整代码在http文件夹下,这里面包含一个readme.md文件,它讲解了怎么去运行这个例子的方法。

使用@angular/http

在angular2中,http单独放在了一个模块里面。这也就意味着,为了使用它,你必须import @angular/http。比如,我们可能会像下面这样:

import { Http, Response, RequestOptions, Headers } from '@angular/http';


import from @angular/http

在我们的例子中,我们使用import HTTP_PROVIDERS,它是一个快捷的方式去引入http里面的指令。

code/http/app/ts/app.ts

import {
Component
} from '@angular/core';
import { bootstrap } from '@angular/platform-browser-dynamic';
import { HTTP_PROVIDERS } from '@angular/http';


当我们运行app时,我们会添加HTTP_PROVIDERS作为一个依赖。结果就是我们可以在我们的app里面注入http模块里面的内容。

bootstrap(HttpApp, [ HTTP_PROVIDERS ]);


现在,我们可以注入http服务进我们的组件里面了。

class MyFooComponent {
constructor(public http: Http) {
}

makeRequest(): void {
// do something with this.http ...
}
}


一个基本的请求

我们将要做的第一件事情就是做一个简单的GET请求。

我们要做的是:

1. 有一个button去调用马克Request

2. makeRequest去调用http库完成一个GET请求

3. 当请求返回的时候,使用返回的数据去更新this.data。并在视图上面渲染出来。

下面是我们的截图:



构建一个简单的SimpleHTTPComponent组件

我们要做的第一件事情就是去import一些http模块,并且表明组件的selector。

code/http/app/ts/components/SimpleHTTPComponent.ts

import {Component} from '@angular/core';
import {Http, Response} from '@angular/http';


构建SimpleHTTPComponent模板

接下来构建视图

code/http/app/ts/components/SimpleHTTPComponent.ts

@Component({
selector: 'simple-http',
template: `
<h2>Basic Request</h2>
<button type="button" (click)="makeRequest()">Make Request</button>
<div *ngIf="loading">loading...</div>
<pre>{{data | json}}</pre>
`
})


这个模板有三个有趣的部分:

1. button

2. loading indicator

3. data,数据

button上面,(click)绑定了makeRequest

为了提醒用户,我们正在加载数据,使用ngIf去查看loading是不是true,如果是显示loading….

data是一个对象,为了调试,使用json管道,它会返回一个易读好看的格式

构建SimpleHTTPComponent控制器

我们为了我们的组件定义一个新类:SimpleHTTPComponent:

code/http/app/ts/components/SimpleHTTPComponent.ts

export class SimpleHTTPComponent {
data: Object;
loading: boolean;


我们有两个变量,data接收数据,loading代表是否在加载。

接下来定义构造器:


4000
code/http/app/ts/components/SimpleHTTPComponent.ts


constructor(public http: Http) {
}


这里注入了一个关键的模块:http

下面是makeRequest函数的实现:

code/http/app/ts/components/SimpleHTTPComponent.ts

makeRequest(): void {
this.loading = true;
this.http.request('http://jsonplaceholder.typicode.com/posts/1')
.subscribe((res: Response) => {
this.data = res.json();
this.loading = false;
});
}


当我们调用马克Request 的时候的第一件事情就是设置loading为true, 让loading指示器显示出来。HTTPreqeust的请求时很直接的。调用http.reqeust,然后传递想要请求的URL就可以了。

http.request返回一个Observable,我们可以subscribe去订阅它的改变(类似于then与Promise的关系)。

code/http/app/ts/components/SimpleHTTPComponent.ts

.subscribe((res: Response) => {
this.data = res.json();
this.loading = false;
});


当http.request返回的时候会发射一个Response对象,我们分解对象的body作为json对象,然后赋值给daa。同时,请求完成后,设置loading为false,表示我们加载完成。

SimpleHTTPComponent全部代码

code/http/app/ts/components/SimpleHTTPComponent.ts

/*
* Angular
*/
import {Component} from '@angular/core'; import {Http, Response} from '@angular/http';

@Component({ selector: 'simple-http', template: ` <h2>Basic Request</h2> <button type="button" (click)="makeRequest()">Make Request</button> <div *ngIf="loading">loading...</div> <pre>{{data | json}}</pre> ` })
export class SimpleHTTPComponent { data: Object; loading: boolean;

constructor(public http: Http) {
}

makeRequest(): void { this.loading = true; this.http.request('http://jsonplaceholder.typicode.com/posts/1') .subscribe((res: Response) => { this.data = res.json(); this.loading = false; }); }
}



编写YouTubeSearchComponent

上面的例子比较简单,现在我们建立一个更加复杂的例子。

在这里,我们希望构建一个按你自己的type去youtube搜索的方式。当搜索返回的时候,我们会显示每一个视频的缩略图及描述和视频的地址。

下面是当我们搜索“cats playing ipads”的画面:



这个例子中,我们将会编写几样东西:

1. 存储数据的SearchResult对象

2. 一个YouTubeService,它管理请求的API以及将请求的结果转换为一个SearchResult[]的流

3. 一个SearchBox组件

4. 一个SearchResultComponent组件,显示一个SearchResult

5. 一个YouTubeSearchComponent会封装我们所有的代码并渲染SearchResult列表。

编写SearchResult

让我们开始编写基础的SearchResult对象,它只是一个存储有兴趣的字段的类

code/http/app/ts/components/YouTubeSearchComponent.ts

class SearchResult {
id: string;
title: string;
description: string;
thumbnailUrl: string;
videoUrl: string;

constructor(obj?: any) {
this.id              = obj && obj.id             || null;
this.title           = obj && obj.title          || null;
this.description     = obj && obj.description    || null;
this.thumbnailUrl    = obj && obj.thumbnailUrl   || null;
this.videoUrl        = obj && obj.videoUrl       ||
`https://www.youtube.com/watch?v=${this.id}`;
}
}


这里仅仅需要指出的时候,当videoUrl为空时,我们硬编码了一个url:
https://www.youtube.com/watch?v=${this.id}


编写YouTubeService

API

这个例子中,我们使用the YouTube v3 search API。

我们会指定两个常量,去映射我们的YouTubeService到我们的key和api url:

let YOUTUBE_API_KEY: string = "XXX_YOUR_KEY_HERE_XXX";
let YOUTUBE_API_URL: string = "https://www.googleapis.com/youtube/v3/search";


我们发现,我们不总是想要去测试完整的产品–通常我们希望去测试产品的部分或者开发的api。

为了有利于这个事情,通常我们会使用注入参数做这个事情。

为什么要使用注入而不是通常的方式呢?因为如果我们使用注入的方式,我们可以:

1. 在部署的时候,根据不同的环境设置不同的值

2. 测试的时候,很容易更换注入的值

通过注入这些值,使得我们的app更加灵活。

为了注入这些值,我们使用bind(…).toValue(…)的语法,像下面这样:

code/http/app/ts/components/YouTubeSearchComponent.ts

export var youTubeServiceInjectables: Array<any> = [
bind(YouTubeService).toClass(YouTubeService),
bind(YOUTUBE_API_KEY).toValue(YOUTUBE_API_KEY),
bind(YOUTUBE_API_URL).toValue(YOUTUBE_API_URL)
];


在这里,我们指定要绑定YOUTUBE_API_KEY注入到YOUTUBE_API_KEY的值。

如果你还记得,为了让注入在我们的app中可用,需要在依赖启动的时候设置。因为我们在这里导出了youTubeServiceInjectables,所以,我们可以在我们的app中导入它。

// http/app.ts
import { youTubeServiceInjectables } from "components/YouTubeSearchComponent";

// ....
// further down
bootstrap(HttpApp, [ HTTP_PROVIDERS, youTubeServiceInjectables ]);


现在我们可以注入YOUTUBE_API_KEY,而不直接使用变量。

YouTubeService构造器

code/http/app/ts/components/YouTubeSearchComponent.ts

@Injectable()
export class YouTubeService {
constructor(public http: Http,
@Inject(YOUTUBE_API_KEY) private apiKey: string,
@Inject(YOUTUBE_API_URL) private apiUrl: string) {
}


在构造器中,我们注入了三样东西:

1. Http

2. YOUTUBE_API_KEY

3. YOUTUBE_API_URL

请注意,我们使用三个实例变量来接收注入,所以我们在其他地方可以使用http,apiKey,apiUrl来访问这些注入的内容。

注意,我们使用@Inject显式注入。

YouTubeService的搜索

现在,让我们实现search函数,search函数带一个查询字符串的参数并返回一个可以发射一个SearchResult[]对象的流。意思就是说,每重点内容一次发射一个SearchResult的数组:

code/http/app/ts/components/YouTubeSearchComponent.ts

search(query:string):Observable<SearchResult[]> {
let params:string = [
`q=${query}`,
`key=${this.apiKey}`,
`part=snippet`,
`type=video`,
`maxResults=10`
].join('&');
let queryUrl:string = `${this.apiUrl}?${params}`;


这里,我们手动建立请求的url,然后我们连接apiUrl和params组成queryUrl。

现在,我们有一个queryUrl,可以使用它请求数据了。

code/http/app/ts/components/YouTubeSearchComponent.ts

return this.http.get(queryUrl)
.map((response:Response) => {
return (<any>response.json()).items.map(item => {
// console.log("raw item", item); // uncomment if you want to debug
return new SearchResult({
id: item.id.videoId,
title: item.snippet.title,
description: item.snippet.description,
thumbnailUrl: item.snippet.thumbnails.high.url
});
});
});


这里,我们把http的返回值使用map映射得到相应的对象,然后从这个对象中解构出body部分,使用。json转换成对象,然后迭代转换成SearchResult。

YouTubeService完整代码

code/http/app/ts/components/YouTubeSearchComponent.ts

export var YOUTUBE_API_KEY:string = 'AIzaSyDOfT_BO81aEZScosfTYMruJobmpjqNeEk';
export var YOUTUBE_API_URL:string = 'https://www.googleapis.com/youtube/v3/search';
let loadingGif:string = ((<any>window).__karma__) ? '' : require('images/loading.gif');

class SearchResult {
id:string;
title:string;
description:string;
thumbnailUrl:string;
videoUrl:string;

constructor(obj?:any) {
this.id = obj && obj.id || null;
this.title = obj && obj.title || null;
this.description = obj && obj.description || null;
this.thumbnailUrl = obj && obj.thumbnailUrl || null;
this.videoUrl = obj && obj.videoUrl ||
`https://www.youtube.com/watch?v=${this.id}`;
}
}

/**
* YouTubeService connects to the YouTube API
* See: * https://developers.google.com/youtube/v3/docs/search/list */
@Injectable()
export class YouTubeService {
constructor(public http:Http,
@Inject(YOUTUBE_API_KEY) private apiKey:string,
@Inject(YOUTUBE_API_URL) private apiUrl:string) {
}

search(query:string):Observable<SearchResult[]> { let params:string = [ `q=${query}`, `key=${this.apiKey}`, `part=snippet`, `type=video`, `maxResults=10` ].join('&'); let queryUrl:string = `${this.apiUrl}?${params}`;
return this.http.get(queryUrl) .map((response:Response) => { return (<any>response.json()).items.map(item => { // console.log("raw item", item); // uncomment if you want to debug return new SearchResult({ id: item.id.videoId, title: item.snippet.title, description: item.snippet.description, thumbnailUrl: item.snippet.thumbnails.high.url }); }); });
}
}

export var youTubeServiceInjectables:Array<any> = [
bind(YouTubeService).toClass(YouTubeService),
bind(YOUTUBE_API_KEY).toValue(YOUTUBE_API_KEY),
bind(YOUTUBE_API_URL).toValue(YOUTUBE_API_URL)
];


编写SearchBox

SearchBox组件在我们的app中扮演一个关键的角色:它在我们的UI与YouTubeService中扮演一个中介的角色。

SearchBox会:

1. 关注input的输入,并提交一个搜索给YouTubeService。

2. 当我们loading的时候发送一个loading事件

3. 当我们有新的结果的时候发送一个results事件

SearchBox @Component定义

*code/http/app/ts/components/YouTubeSearchComponent.ts

/**
* SearchBox displays the search box and emits events based on the results
*/

@Component({
outputs: ['loading', 'results'],
selector: 'search-box',


outputs指明了这个组件会发送的事件。在外面我们可以监听。

<search-box
(loading)="loading = $event"
(results)="updateResults($event)"
></search-box>


在这个例子中,当发送一个loading事件时,外面进行判断处理,当发送一个results事件时,父组件调用它的updateResults函数。

对应的,每一个output对应了一个EventEmitter对象。

SearchBox 模板

code/http/app/ts/components/YouTubeSearchComponent.ts

template: `
<input type="text" class="form-control" placeholder="Search" autofocus>
`


SearchBox的控制器定义

code/http/app/ts/components/YouTubeSearchComponent.ts

class SearchBox implements OnInit {
loading:EventEmitter<boolean> = new EventEmitter<boolean>();
results:EventEmitter<SearchResult[]> = new EventEmitter<SearchResult[]>();


这两个EventEmitter对象对应上面的两个输出。

并且实现了OnInit接口,也就是说,会回调ngOnInit函数,ngOnInit函数中处理初始化时相当好的方式。

构造器:

code/http/app/ts/components/YouTubeSearchComponent.ts

constructor(public youtube:YouTubeService,
private el:ElementRef) {
}


构造器注入了一个YouTubeService,一个el,它代表本身这个组件的DOM元素。

ngOnInit:

在这个例子中,我们希望去监听按键释放事件,意思是说,如果我们简单的让每个按键释放的时候都去搜索不会太好,这里我们可以做三件事情去改善用户体验:

1. 过滤空的和简短的搜索

2. 对input防抖动,意思就是说,不要输入每一个字符都去搜索一边,而是当用户输入完成一段时间之后再去搜索,当用户连续输入的时候不去搜索。

3. 当用户进行一个新的搜索后,丢弃旧的搜索

我们可以手动绑定一个函数给keyup,然后实现过滤和防抖动。然而这里有一个更好的方式:将keyup事件转换成一个observable流。

RxJS提供了一种监听每个元素的事件:Rx.Observable.fromEvent。我们可以像这样使用它:

code/http/app/ts/components/YouTubeSearchComponent.ts

ngOnInit():void {
// convert the `keyup` event into an observable stream
Observable.fromEvent(this.el.nativeElement, 'keyup')
.map((e:any) => e.target.value) // extract the value of the input


注意fromEvent的两个参数:

1. this.el.nativeElement:代表想要关注的DOM元素

2. keyup:要转换成stream的事件名称

现在,我们可以执行一些RxJS的魔法,将这个流转换进SearchResult。让我们一步一步讲解:

获取keyup事件流让我们可以连接更多的方法,在下面,我们会连接几个函数进我们的流,它们将会转换流,最后,我们会展示整个代码。

首先,我们解析input的值:

.map((e: any) => e.target.value) // extract the value of the input


上面的意思就是,映射我们的keyup事件,找到他的target(在这个例子中是input),然后获取它的值,这就意味着我们的流现在变成了一个strings的流。

接下来:

.filter((text: string) => text.length > 1)


这个意思就是说,当字符数少于1的时候,不发送任何事件。

接下来:

.debounceTime(250)


意味着,我们会至少等待250毫秒后才请求,也就不会导致一直请求。

.do(() => this.loading.next(true)) // enable loading


使用do,就是在流的中间执行一个函数,但是它不改变流的任何东西。意思就是说,我们的字符数够了,时间也够了我们将要执行一次搜索,在搜索之前,我们将loading设置为true。

.map((query: string) => this.youtube.search(query))
.switch()


我们使用map去实行一次搜索,针对每个发送的事件,通过使用swith,意味着我们会忽略所有的搜索事件,只保留最新的一个。

全部组合起来就是:

code/http/app/ts/components/YouTubeSearchComponent.ts

Observable.fromEvent(this.el.nativeElement, 'keyup')
.map((e:any) => e.target.value) // extract the value of the input
.filter((text:string) => text.length > 1) // filter out if empty
.debounceTime(250)                         // only once every 250ms
.do(() => this.loading.next(true))         // enable loading
// search, discarding old events if new input comes in
.map((query:string) => this.youtube.search(query))
.switch()


RxJS的API可能是有点令人生畏的。这个就是意味着,我们可以使用很少的代码完成很多的事情。

因为我们调用我们的YouTubeService是一个SearchResult[]的流,我们可以订阅这个流去执行一些完成操作。

subscribe需要三个参数,onSuccess、onError、onCompletion。

code/http/app/ts/components/YouTubeSearchComponent.ts

.subscribe(
(results:SearchResult[]) => { // on sucesss
this.loading.next(false);
this.results.next(results);
},
(err:any) => { // on error
console.log(err);
this.loading.next(false);
},
() => { // on completion
this.loading.next(false);
}
);


onSuccess:是当流执行成功时调用

onError:当流发生错误的时候调用

onCompletion:当流完成的时候调用。

SearchBox完整代码

code/http/app/ts/components/YouTubeSearchComponent.ts

/**
* SearchBox displays the search box and emits events based on the results
*/

@Component({
outputs: ['loading', 'results'],
selector: 'search-box',
template: ` <input type="text" class="form-control" placeholder="Search" autofocus> `
})
class SearchBox implements OnInit { loading:EventEmitter<boolean> = new EventEmitter<boolean>(); results:EventEmitter<SearchResult[]> = new EventEmitter<SearchResult[]>();

constructor(public youtube:YouTubeService, private el:ElementRef) { }

ngOnInit():void { // convert the `keyup` event into an observable stream Observable.fromEvent(this.el.nativeElement, 'keyup') .map((e:any) => e.target.value) // extract the value of the input
.filter((text:string) => text.length > 1) // filter out if empty
.debounceTime(250) // only once every 250ms
.do(() => this.loading.next(true)) // enable loading
// search, discarding old events if new input comes in
.map((query:string) => this.youtube.search(query))
.switch()
// act on the return of the search
.subscribe( (results:SearchResult[]) => { // on sucesss this.loading.next(false); this.results.next(results); }, (err:any) => { // on error console.log(err); this.loading.next(false); }, () => { // on completion this.loading.next(false); } );

}
}


编写SearchResultComponent

SearchBox相当复杂,让我来编写一个简单的,SearchResultComponent,它仅仅渲染一个SearchResult。

这里没有新的知识,代码如下:

code/http/app/ts/components/YouTubeSearchComponent.ts

@Component({
inputs: ['result'],
selector: 'search-result',
template: `
<div class="col-sm-6 col-md-3">
<div class="thumbnail">
<img src="{{result.thumbnailUrl}}">
<div class="caption">
<h3>{{result.title}}</h3>
<p>{{result.description}}</p>
<p><a href="{{result.videoUrl}}"
class="btn btn-default" role="button">Watch</a></p>
</div>
</div>
</div>
`
})
export class SearchResultComponent {
result:SearchResult;
}


编写YouTubeSearchComponent

我们需要实现的最后一个组件是YouTubeSearchComponent。它将所有的东西组装起来。

YouTubeSearchComponent @Component

code/http/app/ts/components/YouTubeSearchComponent.ts

@Component({
selector: 'youtube-search',
directives: [SearchBox, SearchResultComponent],


YouTubeSearchComponent控制器

在构建模板之前,让我们看看控制器。

code/http/app/ts/components/YouTubeSearchComponent.ts

export class YouTubeSearchComponent {
results:SearchResult[];

updateResults(results:SearchResult[]):void {
this.results = results;
// console.log("results:", this.results); // uncomment to take a look
}
}


太简单,不解释。

YouTubeSearchComponent 模板

我们的视图需要做三件事情:

1. 显示加载指示器

2. 监听search-box的事件

3. 显示搜索结果

code/http/app/ts/components/YouTubeSearchComponent.ts

<div class='container'>
<div class="page-header">
<h1>YouTube Search
<img
style="float: right;"
*ngIf="loading"
src='${loadingGif}' />
</h1>
</div>


使用一个图片来显示正在加载。

code/http/app/ts/components/YouTubeSearchComponent.ts

<div class="row">
<div class="input-group input-group-lg col-md-12">
<search-box (loading)="loading = $event" (results)="updateResults($event)" ></search-box>
</div>
</div>


包装search-box,监听loading事件和results事件。

最后,显示结果:

code/http/app/ts/components/YouTubeSearchComponent.ts

<div class="row">
<search-result
*ngFor="let result of results"
[result]="result">
</search-result>
</div>
</div>


使用ngFor显示列表。

YouTubeSearchComponent完整代码

code/http/app/ts/components/YouTubeSearchComponent.ts

@Component({
selector: 'youtube-search',
directives: [SearchBox, SearchResultComponent],
template: `
<div class='container'> <div class="page-header"> <h1>YouTube Search <img style="float: right;" *ngIf="loading" src='${loadingGif}' /> </h1> </div>

<div class="row">
<div class="input-group input-group-lg col-md-12">
<search-box (loading)="loading = $event" (results)="updateResults($event)" ></search-box>
</div>
</div>

<div class="row"> <search-result *ngFor="let result of results" [result]="result"> </search-result> </div> </div>
`
})
export class YouTubeSearchComponent { results:SearchResult[]; updateResults(results:SearchResult[]):void { this.results = results; // console.log("results:", this.results); // uncomment to take a look } }


@angular/http API

当然,到目前为止,我们都是使用的简单的HTTP GET请求。我们需要知道怎么去做其他的请求也是很重要的。

POST请求

@angular/http的POST请求与GET请求很像,我们只需要添加一个参数:body。

如下:

code/http/app/ts/components/MoreHTTPRequests.ts

makePost(): void {
this.loading = true;
this.http.post(
'http://jsonplaceholder.typicode.com/posts',
JSON.stringify({
body: 'bar',
title: 'foo',
userId: 1
}))
.subscribe((res: Response) => { this.data = res.json(); this.loading = false; });
}


注意,第二个参数,我们传递了一个对象,并使用JSON.stringify转换成了json字符串

PUT / PATCH / DELETE / HEAD请求

这里有一些其他通用的HTTP请求,使用几乎相同的方式调用它们:

http.put 和 http.patch分别对应PUT和PATCH请求,它需要一个URL和一个body。

http.delete和http.head对应DELETE和HEAD请求,只需要一个URL,不需要body。

下面是表示怎么进行一个DELETE请求:

code/http/app/ts/components/MoreHTTPRequests.ts

makeDelete(): void {
this.loading = true;
this.http.delete('http://jsonplaceholder.typicode.com/posts/1')
.subscribe((res: Response) => { this.data = res.json(); this.loading = false; });
}


请求选项

到目前为止,我们讲的所有http函数都有一个可选的参数:RequestOptions.这个RequestOptions.封装了:

method

headers

body

mode

credentials

cache

url

search

让我们看看怎么样去创建一个特定X-API-TOKEN头的GET请求,我们可以创建一个像下面这样的请求:

code/http/app/ts/components/MoreHTTPRequests.ts

makeHeaders(): void {
let headers: Headers = new Headers();
headers.append('X-API-TOKEN', 'ng-book');

let opts: RequestOptions = new RequestOptions();
opts.headers = headers;

this.http.get('http://jsonplaceholder.typicode.com/posts/1', opts)
.subscribe((res: Response) => {
this.data = res.json();
});
}


总结

@angular/http还比较早,但是它已经全特性支持了。

关于@angular/http一个重要的事情是,它支持mock测试,它对单元测试来说是一个非常好的帮助。关于测试的详细内容见测试一节。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  angular