第六章 真正的服务
点此处可以下载本章完成后的 示例代码 。运行方法为:
- 解压缩文件,进入
 heroes项目根目录。- 使用
 npm install补上依赖包。也可以把之前备份的node_modules目录复制一份在项目根目录下。- 使用
 json-server data.json启动后端服务。- 使用
 npm start运行项目,打开浏览器访问http://localhost:4200/进入项目。
到目前为止,hero的数据还都是来源于一个内存中的数组,这显然与实际情况相去甚远。现在,我们使用一个json-server来实现真正的服务。json-server将数据存储在一个JSON格式的文件data.json中,并通过http服务提供一个RESTful API的web服务。
关于 RESTful API 和 JSON 的知识,请参见 附录C RESTful API 。
1. 使用json-server以提供RESTful服务
安装 json-server
我们假设你已经成功安装了json-server。如果没有,请参看 附录A Angular 环境搭建 来安装json-server。
建立data.json文件
data.json是我们的数据文件,json-server读取该文件并提供一个RESTful服务。这个文件可以放在任何位置,但是请 不要 放在src目录及其子目录之下,因为这个文件记录的数据会随时被更改,而Angular项目在运行时发现源代码文件被修改就会自动重加载。我们把它放在项目根目录上,与src目录平行。
{
  "heroes": [
    {
      "id": 1,
      "name": "The Monkey King",
      "desc": "I can fly!"
    },
    {
      "id": 2,
      "name": "The Spider Man",
      "desc": "I can climb!"
    }
  ]
}
启动json-server
在data.json所在文件夹,命令行输入
json-server data.json
json-server默认运行在本地3000端口,可以用-p指定不同的端口。
如果 json-server 正常运行,会输出下面的信息。
 \{^_^}/ hi!
  Loading data.json
  Done
  Resources
  http://localhost:3000/heroes
  Home
  http://localhost:3000
2. 英雄数据接口
- GET /heroes:列出所有英雄
 - POST /heroes:新建一个英雄
 - GET /heroes/ID:获取某个指定英雄的信息
 - PUT /heroes/ID:更新某个指定英雄的信息(提供该英雄的全部信息)
 - PATCH /heroes/ID:更新某个指定英雄的信息(提供该英雄的部分信息)
 - DELETE /heroes/ID:删除某个英雄
 
3. 提供并注册HTTP服务
HttpModule并不是 Angular 的核心模块。 它是 Angular 用来进行 Web 访问的一种可选方式,并位于一个名叫 @angular/http 的独立附属模块中,并作为 Angular 的 npm 包之一而发布出来。
我们的应用将会依赖于 Angular 的http服务,它本身又依赖于其它支持类服务。 来自@angular/http库中的HttpModule保存着这些 HTTP 相关服务提供商的全集。
我们要能从本应用的任何地方访问这些服务,就要把HttpModule添加到AppModule的imports列表中。 这里同时也是我们引导应用及其根组件AppComponent的地方。
在app.module.ts注册服务
import { HttpModule } from '@angular/http';
@NgModule({
  imports: [
    HttpModule,
  ],
4. 设置公用的API参数
我们可以把API的参数写到每一个service文件中,但是如果参数要修改的话(比如修改API服务器的地址和端口),就要一个文件一个文件修改,这样很麻烦,所以我们把参数写入了一个公用的文件:
src/app/service/api.service.ts
import { Headers } from '@angular/http';
export class ApiService {
    getUrl(): string {
        return 'http://localhost:3000';
    }
    getHeaders(): Headers {
        return new Headers({ 'Content-Type': 'application/json'});
    }
}
因为我们下面要使用 json-server 提供的服务,它默认的端口就是3000,而所有的数据交换格式将采用 JSON 格式。
同时,与HeroService一样,需要将ApiService服务添加到模块文件中。
app.module.ts
import { ApiService } from './service/api.service';
providers: [
  ApiService,
  ]
5. 修改 HeroService 中获得英雄信息的方法
HeroService可以从任何地方获取Hero数据 —— Web服务、本地存储或模拟数据源。 从组件中移除数据访问逻辑意味着你可以随时更改这些实现方式,而不影响需要这些英雄数据的组件。
修改getHeroes()方法
src/app/service/hero.service.ts
import { Injectable } from '@angular/core';
import { Http, Headers } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { Hero } from '../domain/hero';
import { ApiService } from './api.service';
@Injectable()
export class HeroService {
    private api_url ;
    private headers ;
    constructor(private http: Http, private apiService: ApiService) {
        this.api_url = apiService.getUrl() + '/heroes';
        this.headers = apiService.getHeaders();
    }
    getHeroes(): Promise<Hero[]> {
        const url = `${this.api_url}`;
        return this.http.get(url, {headers: this.headers})
            .toPromise()
            .then(res => res.json() as Hero[])
            .catch(this.handleError);
    }
    private handleError(error: any): Promise<any> {
        console.error('An error occurred', error); 
        return Promise.reject(error.message || error);
    }
}
异步服务与承诺
return this.http.get(url, {headers: this.headers})
    .toPromise()
    .then(res => res.json() as Hero[])
这个请求发出后返回的是一个Observable(可观察对象),我们把它转换成Promise然后处理res(Http Response)。Promise提供异步的处理,注意到then中的写法,这个和我们传统编程写法不大一样,叫做lambda表达式,相当于是一个匿名函数,(input parameters) => expression,=>前面的是函数的参数,后面的是函数体。
在这里我们利用了toPromise操作符把Observable直接转换成Promise对象。
然而要想使用toPromise操作符,我们也必须先import
import 'rxjs/add/operator/toPromise';
在then回调中提取数据
.then(res => res.json() as Hero[])
错误处理
在getHeroes()的最后,我们catch了服务器的失败信息,并把它们传给了错误处理器:
.catch(this.handleError);
这是一个关键的步骤! 我们必须预料到 HTTP 请求会失败,因为有太多我们无法控制的原因可能导致它们频繁出现各种错误。
 private handleError(error: any): Promise<any> {
    console.error('An error occurred', error);
    return Promise.reject(error.message || error);
  }
6. 增加按照ID获得一个英雄的信息的方法
    getHeroById(id: number): Promise<Hero> {
        const url = `${this.api_url}/${id}`;
        return this.http.get(url, {headers: this.headers})
            .toPromise()
            .then(res => res.json() as Hero)
            .catch(this.handleError);
    }
7. 增加,删除,更新英雄操作
对照上文的查询操作,我们来完成另外这三个操作
  //新建Hero
    createHero(hero: Hero): Promise<Hero> {
      const url = `${this.api_url}`;
      return this.http
        .post(url, JSON.stringify(hero), {headers: this.headers})
        .toPromise()
        .then(res => res.json() as Hero)
        .catch(this.handleError);
    }
    //按name新建Hero
    createHeroByName(name: string): Promise<Hero> {
      let hero = {
        name: name
      }
      const url = `${this.api_url}`;
      return this.http
        .post(url, JSON.stringify(hero), {headers: this.headers})
        .toPromise()
        .then(res => res.json() as Hero)
        .catch(this.handleError);
    }
    //修改Hero
    updateHero(hero: Hero): Promise<Hero> {
      const url = `${this.api_url}/${hero.id}`;
      return this.http
        .put(url, JSON.stringify(hero), {headers: this.headers})
        .toPromise()
        .then(res => res.json() as Hero)
        .catch(this.handleError);
    }
    //删除某个Hero
    deleteHero(hero: Hero): Promise<void> {
      const url = `${this.api_url}/${hero.id}`;
      return this.http.delete(url, {headers: this.headers})
        .toPromise()
        .then(() => null)
        .catch(this.handleError);
    }
    //按id删除某个Hero
    deleteHeroById(id: number): Promise<void> {
      const url = `${this.api_url}/${id}`;
      return this.http.delete(url, {headers: this.headers})
        .toPromise()
        .then(() => null)
        .catch(this.handleError);
    }
至此,HeroService改造完毕。