6.1 简介

Angular有自己的HTTP库,我们可以用它来调用外部的API。

当应用对外部服务器发出请求时,我们希望用户能继续与页面进行交互。也就是说,我们不希望页面在HTTP请求从外部服务器返回前一直失去响应。因此,我们的HTTP请求是异步的。

一直以来,处理异步代码比处理同步代码更加棘手。在JavaScript中,通常有3种处理异步代码的方式:

(1)回调(callback)

(2)承诺(promise)

(3)可观察对象(observable)

在Angular中,处理异步代码的最佳方式就是使用可观察对象,所以我们会在本章中介绍这种方式。

 关于RxJS和可观察对象:本章会涉及可观察对象的使用,但不会对其进行过多的解释。第10章会通过深入解析RxJS来讲解可观察对象。

在本章中,我们将:

(1)展示一个Http的基本例子;

(2)创建一个随敲随搜(search-as-you-type)组件用于搜索YouTube;

(3)讨论Http库的API细节。

示例代码本章所用示例的完整代码可以在示例代码下的http文件夹中找到。文件夹中包含一个README.md文件,其中介绍了如何构建及运行项目。

在阅读本章时,最好尝试运行一下相关代码。请随意尝试,以深入了解这些代码的工作原理。

6.2 使用 @angular/http

HTTP在Angular中被拆分为一个单独的模块。这意味着你需要从@angular/http中导入一些常量。比如,我们通常会像下面这样导入@angular/http中的常量:

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

从@angular/http中导入

在app.ts代码中,我们要导入HttpModule,这是一个便于使用的模块集合。

code/http/app/ts/app.ts

/*

* Angular

*/

import {

 Component

} from '@angular/core';

import { NgModule } from '@angular/core';

import { BrowserModule } from '@angular/platform-browser';

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { HttpModule } from '@angular/http';

我们把HttpModule作为依赖项,加入NgModule的imports列表之中。这样就可以把Http(和另外一些模块)导入组件之中。

code/http/app/ts/app.ts

@NgModule({

 declarations:[

  HttpApp,

  SimpleHTTPComponent,

  MoreHTTPRequests,

  YouTubeSearchComponent,

  SearchBox,

  SearchResultComponent

 ],

 imports:[

  BrowserModule,

  HttpModule // <—— right here

 ],

 bootstrap:[ HttpApp ],

 providers:[

  youTubeServiceInjectables

 ]

})

class HttpAppModule {}

现在就可以把Http服务注入到组件中了。(实际上也可以用在任何使用依赖注入的地方。)

class MyFooComponent {

 constructor(public http:Http){

 }

 makeRequest():void {

  // do something with this.http ……

 }

}

6.3 基本请求

首先做的就是向jsonplaceholder API发起一个简单的GET请求。

我们要做的是:

(1)有一个调用makeRequest的button;

(2)makeRequest会调用http库向API发起一个GET请求;

(3)当请求返回时,使用返回结果中的数据更新this.data。

该示例的截图如图6-1所示。

图6-1 基本请求

6.3.1 构建SimpleHTTPComponent的@Component

首先要导入一些模块,然后指定@Component的selector。

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

/*

* Angular

*/

import {Component} from '@angular/core';

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

@Component({

 selector:'simple-http',

6.3.2 构建SimpleHTTPComponent的template

然后构建视图。

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

 template:`

 <h2>Basic Request</h2>

 <button type="button"(click)="makeRequest()">Make Request</button>

 <div *ngIf="loading">loading……</div>

 <pre>{{data | json}}</pre>

`

要注意这里使用了ngIf指令。

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

(1)button

(2)载入指示器

(3)data

我们将控制器中的makeRequest函数绑定到button的(click)上,稍后会对这个函数进行定义。

我们要向用户说明请求正在处理中,所以需要在变量loading为true的时候,使用ngIf来显示loading……。

data是一个Object。这里使用了json管道,这是一种非常棒的输出调试方式。把这段代码放进pre标签内就可以获得漂亮、易读的格式。

6.3.3 构建SimpleHTTPComponent控制器

我们先为SimpleHTTPComponent定义一个新的class。

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

export class SimpleHTTPComponent {

 data:Object;

 loading:boolean;

现在,我们已经有了data和loading这两个实例变量。它们将分别用来存储API返回的数据值与表示加载状态。

然后定义constructor。

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

 constructor(private http:Http){

 }

constructor的方法体是空的,我们要注入一个关键模块Http。

需要记住,当我们在public http:Http中使用public关键字的时候,TypeScript会将http赋值给this.http。它是下面这种写法的简写:

    // other instance variables here

    http:Http;

    constructor(http:Http){

     this.http = http;

    }

现在,我们就通过实现makeRequest函数来发起第一个HTTP请求。

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;

   });

 }

当我们调用makeRequest时,首先要设置this.loading = true。这会在页面上显示载入指示器。

发起HTTP请求的方式非常简明:调用this.http.request并传入URL作为GET请求的参数。

http.request会返回一个Observable对象。我们可以使用subscribe订阅变化(类似于在一个promise上使用then)。

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

  this.http.request('http://jsonplaceholder.typicode.com/posts/1')

   .subscribe((res:Response)=> {

当http.request(从服务器)返回一个流时,它就会发出一个Response对象。我们用json方法提取出响应体并解析成一个Object,然后将这个Object赋值给this.data。

只要我们得到了响应,就不会再加载任何东西了,所以这里需要设置 this.loading = false。

 .subscribe同样可以处理失败和流完结的情况,只要分别在第二和第三个参数中传入一个函数就可以了。对于一个产品级应用来说,处理这两种情况是个好主意。当请求失败(即流中发生错误)的时候,this.loading也应当被设置为false。

6.3.4 完整的SimpleHTTPComponent

下面就是完整的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(private 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;

   });

 }

}

6.4 编写YouTubeSearchComponent

上一个例子是从代码中获取API服务器上数据的最简方式。现在我们要尝试构建一个更复杂的例子。

在这一节里,我们会打造一个随着输入搜索YouTube的组件。当搜索结果返回时,通过一个列表来展示每一个视频的缩略图、描述和链接。

搜索cats playing ipads时的屏幕截图如图6-2所示。

图6-2 能让我的猫咪写Angular吗

在这个例子中,我们要实现下列功能:

(1)一个SearchResult对象,用于存放每条搜索结果;

(2)一个YouTubeService服务,用于管理向YouTube的API发出的请求并将结果转成一个SearchResult[]流;

(3)一个SearchBox组件,用于根据用户输入内容调用YouTube服务;

(4)一个SearchResultComponent组件,用于渲染具体的SearchResult结果;

(5)一个YouTubeSearchComponent组件,封装整个YouTube搜索功能并渲染结果列表。

下面逐一处理每个部分。

 Patrick Stapleton维护着一个非常棒的代码仓库angular2-webpack-starter。里面有使用RxJS实现搜索GitHub仓库时自动补全的示例。本节中的一些想法就是受这个示例的启发。它是个包含各种示例的酷炫项目,也许你该看一下。

6.4.1 编写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}`;

 }

}

这里使用obj?:any方式来模拟关键词参数。我们可以创建一个新的SearchResult并且只传入一个包含指定键的对象。

唯一要特别指出的是,我们在构造videoUrl时使用了硬编码的URL格式。你也可以将其重构为一个根据多个参数来生成路径的函数,或者直接在视图中使用视频的id来构造URL。

6.4.2 编写YouTubeService

API

在这个例子中,我们将使用YouTube第3版搜索 API

为了使用这个API,你需要一个API密钥。我们已经在示例代码中包含了一个可供大家使用的API密钥。尽管如此,当你读到这里的时候,可能发现这个密钥已经超过了使用频率限制。如果是这样的话,你就需要去生成一个自己的密钥了。

要生成自己的密钥,可以查看文档:https://developers.google.com/youtube/registering_an_application#Create_API_Keys。为了简单起见,我已经注册了一个服务器密钥;如果你要将你的JavaScript代码放到线上,那么还需要一个浏览器密钥。

我们将为YouTubeService设置两个用来表示API密钥和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)在测试期更容易替换要注入的值。

通过注入这些值,我们将获得更多的灵活性。

为了让这些值可被注入,我们使用{ provide:…… , useValue:…… }语法。

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

export var youTubeServiceInjectables:Array<any> = [

 {provide:YouTubeService, useClass:YouTubeService},

 {provide:YOUTUBE_API_KEY, useValue:YOUTUBE_API_KEY},

 {provide:YOUTUBE_API_URL, useValue:YOUTUBE_API_URL}

];

这里我们指定,要把YOUTUBE_API_KEY的值绑定到可被注入的YOUTUBE_API_KEY上。(YOUTUBE_API_URL也一样,稍后还们还将定义YouTubeService。)

也许你还记得,为了在本应用中进行依赖注入,我们需要将其放入NgModule的providers里。因为这里导出了youTubeServiceInjectables,所以就能在app.ts中使用它了。

// http/app.ts

import { HttpModule } from '@angular/http';

import { youTubeServiceInjectables } from "components/YouTubeSearchComponent";

// ……

// further down

// ……

@NgModule({

 declarations:[

  HttpApp,

  // others ……

 ],

 imports:[ BrowserModule, HttpModule ],

 bootstrap:[ HttpApp ],

 providers:[

  youTubeServiceInjectables // <—— right here

 ]

})

class HttpAppModule {}

现在我们使用注入(来自youTubeServiceInjectables的)YOUTUBE_API_KEY的方式来代替直接使用变量。

YouTubeService构造函数

我们通过编写一个class并使用@Injectable对其进行注解来创建YouTubeService。

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

/**

* YouTubeService connects to the YouTube API

* See:* https://developers.google.com/youtube/v3/docs/search/list

*/

@Injectable()

export class YouTubeService {

 constructor(private http:Http,

       @Inject(YOUTUBE_API_KEY)private apiKey:string,

       @Inject(YOUTUBE_API_URL)private apiUrl:string){

 }

我们在constructor中注入三样东西:

(1)Http

(2)YOUTUBE_API_KEY

(3)YOUTUBE_API_URL

这里要注意,我们使用这三个参数创建实例变量。这意味着可以分别通过this.http、this.apiKey和this.apiUrl来访问它们。

还要注意,我们使用@Inject(YOUTUBE_API_KEY)进行显式注入。

YouTubeService搜索

下一步,我们来实现search函数。search传入一个要查询的string并返回一个会发出SearchResult[]流的Observable。换句话说,它发出的每个条目都是一个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}`;

这里使用了手动的方式来构造queryUrl。我们简单地将查询参数放入params变量之中。(你可以查阅搜索API文档了解每个值的含义。)

然后将apiUrl与params拼接起来作为queryUrl。

现在就有了一个可以用来发起请求的queryUrl了。

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}`;

  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.get的返回值,并用map来从请求中获取Response。这里使用.json()从response中提取返回体并同时实例化成一个对象。然后遍历每一个项目并将其转换成一个SearchResult。

如果你想看看item的原始值,可以取消对console.log的注释,然后在浏览器的开发者控制台检查输出的值。

注意,这里调用了(<any>response.json()).items。这是在干什么?这是在告诉TypeScript,我们并不想在这里进行严格的类型检查。

当我们使用JSON API时,通常并没有API响应体的类型定义信息,所以TypeScript不知道返回的Object中会有一个items键。因此,编译器会在这里出问题。

我们也可以调用response.json()["items"]并将其转换成一个Array类型,但是这里(以及创建SearchResult时)将其作为any类型来使用会更加简洁,只是牺牲了一点类型检查的严格性。

YouTubeService的完整代码

这里是YouTubeService的完整代码。

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

/**

* YouTubeSearchComponent is a tiny app that will autocomplete search YouTube.

*/

import {

 Component,

 Injectable,

 OnInit,

 ElementRef,

 EventEmitter,

 Inject

} from '@angular/core';

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

import { Observable } from 'rxjs';

/*

This API key may or may not work for you.Your best bet is to issue your own

API key by following these instructions:

https://developers.google.com/youtube/registering_an_application#Create_API_Ke\

ys

Here I've used a **server key** and make sure you enable YouTube.

Note that if you do use this API key, it will only work if the URL in

your browser is "localhost"

*/

export var YOUTUBE_API_KEY:string = 'AIzaSyDOfT_BO81aEZScosfTYMruJobmpjqNeEk';

export var YOUTUBE_API_URL:string = 'https://www.googleapis.com/youtube/v3/sear\

ch';

let loadingGif:string =((<any>window).__karma__)? '':require('images/loadin\

g.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(private 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> = [

 {provide:YouTubeService, useClass:YouTubeService},

 {provide:YOUTUBE_API_KEY, useValue:YOUTUBE_API_KEY},

 {provide:YOUTUBE_API_URL, useValue:YOUTUBE_API_URL}

];

/**

* 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>

 `

})

export class SearchBox implements OnInit {

 loading:EventEmitter<boolean> = new EventEmitter<boolean>();

 results:EventEmitter<SearchResult[]> = new EventEmitter<SearchResult[]>();

 constructor(private 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);

    }

   );

 }

}

@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;

}

@Component({

 selector:'youtube-search',

 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

 }

}

6.4.3 编写SearchBox

SearchBox组件在应用中扮演着关键的角色:它是UI与YouTubeService的中间连接层。

SearchBox将会:

(1)观察input的keyup事件,并向YouTubeService提交搜索;

(2)在正在加载(或者不再加载)时,触发一个loading事件;

(3)在获取到新的结果时,触发一个results事件。

定义SearchBox的@Component

我们来定义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',

我们之前已经见过很多次selector了:它允许我们创建<search-box>标签。

outputs指定了将从组件中触发的事件,也就是可以在视图中使用(output)="callback()"语法以侦听组件中的事件。例如,下面是我们将在视图中使用search-box标签的方式:

<search-box

(loading)="loading = $event"

(results)="updateResults($event)"

 ></search-box>

在这个例子中,当SearchBox组件触发一个loading事件时,我们要设置父上下文中的loading变量。同样,当SearchBox组件触发results事件时,我们将会调用父上下文中的updateResults()函数。

我们在@Component的配置当中简要地用"loading"和"results"字符串指定事件的名称。在这个例子中,每个事件都会有一个对应的EventEmitter作为控制器类的实例变量。稍后就会实现它们。

目前,要记住@Component就像是组件的公共API,所以这里只需要指定事件的名称,稍后再来看EventEmitter的具体实现。

定义SearchBox的template

我们的template很简明。这里只有一个 input 标签。

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>

 `

})

定义SearchBox控制器

SearchBox控制器是一个新类。

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

export class SearchBox implements OnInit {

 loading:EventEmitter<boolean> = new EventEmitter<boolean>();

 results:EventEmitter<SearchResult[]> = new EventEmitter<SearchResult[]>();

我们通过implements OnInit让这个类实现对应的接口,这么做是因为需要使用生命周期中ngOnInit的回调。如果一个类声明implements OnInit,那么ngOnInit函数会在首次变化检查后调用。

ngOnInit是进行初始化工作的理想地方(相对于constructor),因为组件的各个输入参数在constructor中仍然是不可用的。

定义SearchBox控制器的constructor

我们来看一下SearchBox的constructor。

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

 constructor(private youtube:YouTubeService,

       private el:ElementRef){

 }

我们在constructor中注入:

(1)YouTubeService

(2)此组件所附着的元素el

其中的el是一个ElementRef类型的对象,此类型是Angular对原生元素的一个包装。

我们将注入的两个值作为实例变量。

定义SearchBox控制器ngOnInit

在输入框中,我们想要监视keyup事件。问题是,如果在每一次keyup后都直接进行搜索,可能效果并不好。我们可以用三种方式来提升用户体验:

(1)过滤掉空白与过短的查询;

(2)消除输入的“抖动”,也就是我们不希望每一个字符发生改变时都进行搜索,而是在用户完成输入并暂停一小段时间后再进行搜索;

(3)当用户进行新的搜索时,抛弃旧的搜索内容。

我们可以手动绑定keyup,并在每次keyup事件触发时调用一个函数,然后在其中实现字符过滤与抖动消除。不过我们有一种更好的方式:让keyup事件成为一个可观察流。

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')

要注意在fromEvent里面:

●第一个参数是this.el.nativeElement(组件附着的原生DOM元素);

●第二个参数是字符串'keyup',代表的是将要被转换成流的事件名称。

借助流的魔力,我们可以把它转换成SearchResult。下面来分步看看。

有了keyup事件的流,就能把多个方法串联起来。接下来我们会在流上串联一些转换流的函数,并在最后展示整个示例。

首先,我们要从input标签中提取输入值:

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

上面的代码表示,映射每一个keyup事件,然后找到它的目标(e.target,也就是input元素)并取出value。这意味着这个流现在变成了一个字符串流。

下一步:

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

filter表示该流在长度小于1的时候不会发送任何搜索字符串。如果你还希望忽略较短的搜索字符串,可以把这个值改大一点。

  .debounceTime(250)

debounceTime表示我们会忽略触发间隔小于250 ms的请求。也就是说,我们不会去搜索每一次键入的内容。只有在用户暂停输入一小段时间后才会触发搜索。

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

在流上使用do方法可以在流中对每个事件执行函数,但是这种方式不会改变流中的任何数据。这是因为已经获取到了具有足够长度并消除了输入抖动的搜索字符串,所以要在页面上显示loading。

this.loading是一个EventEmitter。我们通过发射true作为下一个事件来“开启”loading。我们通过调用next来在EventEmitter上发射数据。编写的this.loading.next(true)代表在loading这个EventEmitter上发射一个true事件。当监听此组件上的loading事件时,$event的值现在会被设置为true(稍后会深入探讨使用$event)。

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

  .switch()

在每一个触发的查询上使用.map以执行搜索。使用switch表示“除了最近的一次,忽略所有搜索事件”。这就是说,如果有一个新的搜索进来,我们就使用这个最新的并丢弃掉其他搜索。

 熟悉Reactive的专家对此一定不会陌生。你也可以在RxJS的文档中找到关于switch方法的更加具体的定义。

每当进入query时,都将对YouTubeService进行一次搜索(search)。

把这些串联在一起,结果如下所示。

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

   .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(

因为RxJS的API数量众多,所以看起来会有些吓人。尽管如此,我们使用简单的几行代码就实现了一个极为复杂的事件处理流!

因为是在调用YouTubeService,所以我们的流现在是一个SearchResult[]流了。这时可以订阅(subscribe)这个流,并执行相应的操作。

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);

    }

   );

 }

第一个参数指定了当流触发一个正常事件时将会执行的操作。这里我们会在这两个EventEmitter上触发一个事件:

(1)调用this.loading.next(false),表示停止加载;

(2)调用this.results.next(results),会触发一个包含结果列表数据的事件。

第二个参数指定了当流出现错误时将会执行的操作。这里我们只设置 this.loading.next(false)并记录下错误。

第三个参数指定了当流结束时将会执行的操作。这里依然会触发结束加载的事件。

SearchBox组件的完整代码

以下是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>

 `

})

export class SearchBox implements OnInit {

 loading:EventEmitter<boolean> = new EventEmitter<boolean>();

 results:EventEmitter<SearchResult[]> = new EventEmitter<SearchResult[]>();

 constructor(private 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);

    }

   );

 }

}

6.4.4 编写SearchResultComponent

之前的SearchBox相当复杂。现在来处理一个简单得多的组件:SearchResultComponent(如图6-3所示)。SearchResultComponent的作用就是渲染一个SearchResult。

图6-3 单一搜索结果组件

这里没有什么新东西,所以直接完整地列出来。

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;

}

有以下几点需要关注:

●@Component只有一个result输入参数,可以通过它把SearchResult赋值给组件;

●template里有标题、描述以及视频的缩略图,并通过一个按钮链接到视频上;

●SearchResultComponent在其实例中使用result变量存储SearchResult。

6.4.5 编写YouTubeSearchComponent

我们要实现的最后一个组件就是YouTubeSearchComponent。这个组件最终会将所有东西组织在一起。

YouTubeSearchComponent的@Component

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

@Component({

 selector:'youtube-search',

@Component注解很容易理解:使用名为youtube-search的selector。

YouTubeSearchComponent控制器

在讨论template之前,需要先看一下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

 }

}

这个组件拥有一个实例变量:SearchResult数组类型的results。

我们还定义了一个函数:updateResults。updateResults直接把SearchResult[]的新值赋给this.results。

results和updateResults都会在template中用到。

YouTubeSearchComponent的template

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

(1)在加载时,显示加载指示器;

(2)监听search-box上的事件;

(3)显示搜索结果。

之后来看一下template。构建基本结构并在头部的旁边显示表示“正在加载”的gif动画。

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

 template:`

 <div class='container'>

   <div class="page-header">

    <h1>YouTube Search

     <img

      style="float:right;"

      *ngIf="loading"

      src='${loadingGif}' />

    </h1>

   </div>

 注意,img的src属性为${loadingGif},loadingGif变量来自于程序前面的require语句。这里使用了webpack的图像文件加载功能。如果你想探究其工作原理,可以看一下本章示例代码中的webpack配置,或者下载image-webpack-loader项目

因为只有当loading为真时,才需要显示加载图像,所以要用ngIf来实现这个功能。

接下来,看看使用search-box的地方。

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>

值得关注的是将results输出结果绑定到loading的方式。注意我们在这里使用了(output)="action()"语法。

对于loading输出,运行loading = $event表达式。$event会被EventEmitter发出的事件值替换掉。也就是说,当我们调用SearchBox组件中的this.loading.next(true)时,$event的值将会是true。

同样,对于results输出,每当一组新的结果发出时,都会调用updateResults()函数。这样就能实现更新组件中results实例变量值的效果。

最后,我们要在组件中获取results列表,并为每个组件渲染一个search-result。

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

  <div class="row">

   <search-result

    *ngFor="let result of results"

    [result]="result">

   </search-result>

  </div>

 </div>

YouTubeSearchComponent的完整代码

这里是YouTubeSearchComponent的完整代码。

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

@Component({

 selector:'youtube-search',

 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

 }

}

好了!这样我们就实现了一个针对YouTube视频的随敲随搜功能!如果你还不太明白,可以尝试执行示例代码。

6.5 @angular/http API

当然,到目前为止发起的所有HTTP请求都是简单的GET请求。知晓如何发起其他类型的请求也很重要。

6.5.1 发起一个POST请求

使用@angular/http发起POST请求与发起GET请求非常类似,仅仅多了一个额外的参数:请求体。

jsonplaceholder API同样提供了一个URL,可供测试POST请求。现在就来试一下。

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将Object转换为一个JSON字符串。

6.5.2 PUT/PATCH/DELETE/HEAD

还有其他一些常见的HTTP请求,也是用类似的方式进行调用。

●http.put和http.patch分别用于PUT和PATCH请求,并且它们都带有一个URL和一个请求体。

●http.delete和http.head分别用于DELETE和HEAD请求,并且都带有一个URL(没有请求体)。

下面展示了如何发起一个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;

   });

 }

6.5.3 RequestOptions

目前我们覆盖到的所有http方法还带有一个可选的末位参数:RequestOptions。Request Options 对象封装了:

●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();

   });

 }

6.6 总结

@angular/http非常灵活并且广泛适用于各种API。

@angular/http的一个强大特性就是支持模拟后台。这一点在测试中非常有用。想了解更多关于测试HTTP的内容,请参见第15章。

  1. http://jsonplaceholder.typicode.com
  2. https://github.com/angular-class/angular2-webpack-starter
  3. https://developers.google.com/youtube/v3/docs/search/list
  4. https://developers.google.com/youtube/v3/docs/search/list
  5. https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/switch.md
  6. https://github.com/tcoopman/image-webpack-loader
  7. http://jsonplaceholder.typicode.com