HTTP
Posted wpengch1
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HTTP相关的知识,希望对你有一定的参考价值。
注:学习使用,禁止转载
介绍
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代表是否在加载。
接下来定义构造器:
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测试,它对单元测试来说是一个非常好的帮助。关于测试的详细内容见测试一节。
以上是关于HTTP的主要内容,如果未能解决你的问题,请参考以下文章