《Angular从零到一》一3.3 建立模拟Web服务和异步操作-阿里云开发者社区

开发者社区> 华章出版社> 正文

《Angular从零到一》一3.3 建立模拟Web服务和异步操作

简介:

 本节书摘来自华章出版社《Angular从零到一》一书中的第3章,第3节,作者王芃,更多章节内容可以访问云栖社区“华章计算机”公众号查看。


3.3 建立模拟Web服务和异步操作

实际开发中,我们的service是要和服务器API进行交互的,而不是现在这样简单地操作数组。但问题来了,现在没有Web服务,难道真要自己开发一个吗?答案是可以做个假的,假作真时真亦假。我们在开发过程中经常会遇到这类问题,等待后端开发的进度是很痛苦的。所以Angular内建提供了一个可以快速建立测试用Web服务的方法:内存 (in-memory)服务器。

3.3.1 构建数据模型

一般来说,你需要知道自己对服务器的期望是什么,期待它返回什么样的数据,有了这个数据,我们就可以自己快速地建立一个内存服务器。拿这个例子来看,我们可能需要一个这样的对象:

class Todo {
  id: string;
  desc: string;
  completed: boolean;
}

对应的JSON应该是这样的:


{
  "data": [
    {
      "id": "f823b191-7799-438d-8d78-fcb1e468fc78",
      "desc": "blablabla",
      "completed": false
    },
    {
      "id": "c316a3bf-b053-71f9-18a3-0073c7ee3b76",
      "desc": "tetssts",
      "completed": false
    },
    {
      "id": "dd65a7c0-e24f-6c66-862e-0999ea504ca0",
      "desc": "getting up",
      "completed": false
    }
  ]
}

首先我们需要安装angular-in-memory-web-api,输入npm install --save angular-in-memory-web-api,然后在Todo文件夹下创建一个文件


src\app\todo\todo-data.ts:
import { InMemoryDbService } from 'angular-in-memory-web-api';
import { Todo } from './todo.model';

export class InMemoryTodoDbService implements InMemoryDbService {
  createDb() {
    let todos: Todo[] = [
      {id: "f823b191-7799-438d-8d78-fcb1e468fc78", desc: 'Getting up', completed: true},
      {id: "c316a3bf-b053-71f9-18a3-0073c7ee3b76", desc: 'Go to school', completed: false}
    ];
    return {todos};
  }
}

3.3.2 实现内存Web服务

可以看到,我们创建了一个实现InMemoryDbService的内存数据库,这个数据库其实也就是把数组传入进去。接下来,我们要更改src\app\app.module.ts,加入类引用和对应的模块声明:

mport { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryTodoDbService } from './todo/todo-data';

然后在imports数组中紧挨着HttpModule加上:

InMemoryWebApiModule.forRoot(InMemoryTodoDbService),。

现在我们在service中试着调用我们的“假Web服务”

import { Injectable } from '@angular/core';
import { Http, Headers } from '@angular/http';
import { UUID } from 'angular2-uuid';

import 'rxjs/add/operator/toPromise';

import { Todo } from './todo.model';

@Injectable()
export class TodoService {

  //定义你的假Web API地址,这个定义成什么都无所谓
  //只要确保是无法访问的地址就好
  private api_url = 'api/todos';
  private headers = new Headers({'Content-Type': 'application/json'});

  constructor(private http: Http) { }

  // POST /todos
  addTodo(desc:string): Promise<Todo> {
    let todo = {
      id: UUID.UUID(),
      desc: desc,
      completed: false
    };
    return this.http
            .post(this.api_url, JSON.stringify(todo), {headers: this.headers})
            .toPromise()
            .then(res => res.json().data as Todo)
            .catch(this.handleError);
  }

  private handleError(error: any): Promise<any> {
    console.error('An error occurred', error); 
    return Promise.reject(error.message || error);
  }
}

上面的代码中定义了一个

api_url = 'api/todos'

可能会问这个是怎么来的?分两部分看,api/todos中前面的api定义成什么都可以,但后面这个todos是有讲究的,我们回去看一下src\app\todo\todo-data.ts返回的return {todos},这个其实是return {todos: todos}的省略表示形式,如果我们不想让这个后半部分是todos,我们可以写成{nahnahnah: todos}。这样,我们改写成api_url = 'blablabla/nahnahnah'也无所谓,因为这个内存Web服务的机理是拦截Web访问,也就是说,随便什么地址都可以,内存Web服务会拦截这个地址并解析你的请求是否满足RESTful API的要求。

3.3.3 内存服务器提供的Restful API

简单来说,RESTful API中以“名词”来标识资源,比如todos;以“动词”标识操

作,比如:GET请求用于查询,PUT用于更新,DELETE用于删除,POST用于添加。比如,如果url是api/todos,那么:

查询所有待办事项:以GET方法访问api/todos。

查询单个待办事项:以GET方法访问api/todos/id,比如id是1,那么访问api/todos/1。

更新某个待办事项:以PUT方法访问api/todos/id。

删除某个待办事项:以DELETE方法访问api/todos/id。

增加一个待办事项:以POST方法访问api/todos。

在service的构造函数中我们注入了HTTP,而Angular的HTTP封装了大部分我们需要的方法,比如例子中增加一个Todo,我们就调用this.http.post(url, body, options),上面代码中.post(this.api_url, JSON.stringify(todo), {headers: this.headers})的含义是:构造一个POST类型的HTTP请求,其访问的url是this.api_url,request的body是一个JSON(把Todo对象转换成JSON),在参数配置中我们配置了request的header。

这个请求发出后返回的是一个Observable(可观察对象),我们把它转换成Promise,然后处理res(Http Response)。Promise提供异步的处理,注意到then中的写法,这个和传统编程写法不大一样,它叫做lamda表达式,相当于一个匿名函数,(input parameters) => expression,=>前面的是函数的参数,后面的是函数体。

还要一点需要强调的是:在用内存Web服务时,一定要注意res.json().data中的data属性必须要有,因为内存Web服务在返回的json中加了data对象,你真正要得到的json在这个data里面。

下一步我们来更改Todo组件的addTodo方法以便可以使用我们新的异步http方法。

addTodo(){
  this.service
    .addTodo(this.desc)
    .then(todo => {
      this.todos = [...this.todos, todo];
      this.desc = '';
    });
}

这里,前半部分应该还是好理解的:this.service.addTodo(this.desc)调用service的对应方法而已,但后半部分用来做什么?...这个貌似省略号的东西是ES7中计划提供的Object Spread操作符,它的功能是将对象或数组“打散,拍平”。这么说可能还是不清晰,下面举个例子:


let arr = [1,2,3];
let arr2 = [...arr]; 
arr2.push(4); 

// arr2 变成了 [1,2,3,4]
// arr 保存原来的样子

let arr3 = [0, 1, 2];
let arr4 = [3, 4, 5];
arr3.push(...arr4);
// arr3变成了[0, 1, 2, 3, 4, 5]

let arr5 = [0, 1, 2];
let arr6 = [-1, ...arr5, 3];
// arr6 变成了[-1, 0, 1, 2, 3]

所以,上面的this.todos = [...this.todos, todo];相当于为todos增加一个新元素,这和push很像,那为什么不用push呢?因为这样构造出来的对象是全新的,而不是引用的,在现代编程中一个明显的趋势是不要在过程中改变输入的参数。还有就是这样做会带给我们极大的便利性和编程的一致性。下面通过给该例子添加几个功能,我们来一起体会一下。

3.3.4 Angular 2内建的HTTP方法

首先更改


src\app\todo\todo.service.ts:
//src\app\todo\todo.service.ts
import { Injectable } from '@angular/core';
import { Http, Headers } from '@angular/http';
import { UUID } from 'angular2-uuid';

import 'rxjs/add/operator/toPromise';

import { Todo } from './todo.model';

@Injectable()
export class TodoService {

  private api_url = 'api/todos';
  private headers = new Headers({'Content-Type': 'application/json'});

  constructor(private http: Http) { }

  // POST /todos
  addTodo(desc:string): Promise<Todo> {
    let todo = {
      id: UUID.UUID(),
      desc: desc,
      completed: false
    };
    return this.http
            .post(this.api_url, JSON.stringify(todo), {headers: this.headers})
            .toPromise()
            .then(res => res.json().data as Todo)
            .catch(this.handleError);
  }

  // PUT /todos/:id
  toggleTodo(todo: Todo): Promise<Todo> {
    const url = '${this.api_url}/${todo.id}';
    console.log(url);
    let updatedTodo = Object.assign({}, todo, {completed: !todo.completed});
    return this.http
            .put(url, JSON.stringify(updatedTodo), {headers: this.headers})
            .toPromise()
            .then(() => updatedTodo)
            .catch(this.handleError);
  }

  // DELETE /todos/:id
  deleteTodoById(id: string): Promise<void> {
    const url = '${this.api_url}/${id}';
    return this.http
            .delete(url, {headers: this.headers})
            .toPromise()
            .then(() => null)
            .catch(this.handleError);
  }

  // GET /todos
  getTodos(): Promise<Todo[]>{
    return this.http.get(this.api_url)
              .toPromise()
              .then(res => res.json().data as Todo[])
              .catch(this.handleError);
  }

  private handleError(error: any): Promise<any> {
    console.error('An error occurred', error); 
    return Promise.reject(error.message || error);
  }
}

上面的代码中可以看到对应Restful API的各个“动词”,Angular 2.x 提供了一系列对应名称的方法,非常简单易用。比如说在deleteTodoById方法中,我们要访问的API是/todos/:id,使用的HTTP方法是DELETE,那么我们就使用this.http.delete(url, {headers: this.headers})。

3.3.5 JSONP和CORS

除了提供HTTP方法,在同一个Module中(HttpModule)我们还能看到有JSONP。那么JSONP是什么呢?简单来说,由于浏览器的限制,JavaScript进行跨域的HTTP资源请求是不允许的。比如你自己的服务器域名是foo.bar,那么在你域名下host的JavaScript如果要请求boo.bar域名下的某个API,其本应返回json,但你的JavaScript可能出现无法访问的情况。这是因为浏览器出于安全的考虑,限制了不同源的资源请求。

但很快有人发现Web页面上调用js文件时则不受是否跨域的影响,而且发现凡是拥有src这个属性的标签都拥有跨域的能力,比如<script>、<img>、<iframe>。跨域访问数据就存在一种可能,那就是在远程服务器上设法把数据装进js格式的文件里,供客户端调用和进一步处理。

JSON在JavaScript中有良好的支持,而且JSON可以简洁地描述复杂数据结构,所以在客户端几乎可以随心所欲地处理这种格式的数据。Web客户端通过与调用脚本一模一样的方式,来调用跨域服务器上动态生成的js格式文件(一般以JSON为后缀)。显而易见,服务器之所以要动态生成JSON文件,目的就在于把客户端需要的数据装入进去。

这样逐渐形成了一种非正式传输协议,这就是JSONP了,该协议的一个要点就是允许用户传递一个callback参数给服务器端,然后服务器端返回数据时会将这个callback参数作为函数名来包裹住JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。

Angular提供了JSONP对象,同时提供很多和HTTP类似的方法便于大家使用JSONP解决跨域问题。

当然,这个问题目前其实有比JSONP更好的解决方案,那就是CORS(跨来源资源共享),这是个正式浏览器技术的标准。提供了Web服务从不同网域传来沙盒脚本的方法,以避开浏览器的同源策略,是JSONP模式的现代版。与JSONP不同,CORS 除了GET要求方法以外也支援其他的HTTP要求。大部分现代浏览器都已经支持CORS。当然,JSONP可以在不支援CORS的老旧浏览器上运作。

3.3.6 页面展现更新src\app\todo\todo.component.ts,调用新的service中的方法。有趣的是,利用Object Spread操作符,我们看到代码风格更一致,逻辑也更容易理解了:


mport { Component, OnInit } from '@angular/core';
import { TodoService } from './todo.service';
import { Todo } from './todo.model';

@Component({
  selector: 'app-todo',
  templateUrl: './todo.component.html',
  styleUrls: ['./todo.component.css'],
  providers: [TodoService]
})
export class TodoComponent implements OnInit {
  todos : Todo[] = [];
  desc: string = '';

  constructor(private service: TodoService) {}
  ngOnInit() {
    this.getTodos();
  }

  addTodo(){
    this.service
      .addTodo(this.desc)
      .then(todo => {
        this.todos = [...this.todos, todo];
        this.desc = '';
      });
  }

  toggleTodo(todo: Todo) {
    const i = this.todos.indexOf(todo);
    this.service
      .toggleTodo(todo)
      .then(t => {
        this.todos = [
          ...this.todos.slice(0,i),
          t,
          ...this.todos.slice(i+1)
          ];
      });
  }

  removeTodo(todo: Todo) {
    const i = this.todos.indexOf(todo);
    this.service
      .deleteTodoById(todo.id)
      .then(()=> {
        this.todos = [
          ...this.todos.slice(0,i),
          ...this.todos.slice(i+1)
        ];
      });
  }

  getTodos(): void {
    this.service
      .getTodos()
      .then(todos => this.todos = [...todos]);
  }
}
模板文件src\app\todo\todo.component.html需要把对应的功能体现在页面上,于是我们增加了toggleTodo(切换完成状态)的checkbox和removeTodo(删除待办事项)的button:
<section class="todoapp">
  <header class="header">
    <h1>Todos</h1>
    <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="desc" (keyup.enter)="addTodo()">
  </header>
  <section class="main" *ngIf="todos?.length > 0">
    <input class="toggle-all" type="checkbox">
    <ul class="todo-list">
      <li *ngFor="let todo of todos" [class.completed]="todo.completed">
        <div class="view">
          <input class="toggle" type="checkbox" (click)="toggleTodo(todo)" [checked]=
"todo.completed">
          <label (click)="toggleTodo(todo)">{{todo.desc}}</label>
          <button class="destroy" (click)="removeTodo(todo); $event.stopPropagation()">
</button>
        </div>
      </li>
    </ul>
  </section>
  <footer class="footer" *ngIf="todos?.length > 0">
    <span class="todo-count">
      <strong>{{todos?.length}}</strong> {{todos?.length == 1 ? 'item' : 'items'}} 
left
    </span>
    <ul class="filters">
      <li><a href="">All</a></li>
      <li><a href="">Active</a></li>
      <li><a href="">Completed</a></li>
    </ul>
    <button class="clear-completed">Clear completed</button>
  </footer>
</section>

更新组件的css样式: src\app\todo\todo.component.css 和 src\styles.css,这两个文件比较大,可以到下面列出的本节代码中去查看。

其中src\app\todo\todo.component.css有一段代码稍微讲一下,这段代码把复选框原本的方块替换成SVG格式的图片,以便实现比较炫酷的效果:

...

.todo-list li .toggle:after {
    content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" 
width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill=
"none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todo-list li .toggle:checked:after {
    content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" 
width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill=
"none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l
-4 4 20 20 34-52z"/></svg>');
}
...

现在我们看看成果吧,现在好看多了,如图 3.5所示。

 

图3.5 带样式的待办事项

本章代码:https://github.com/wpcfan/awesome-tutorials/tree/chap03/angular2/ng2-tut

打开命令行工具使用 git clone https://github.com/wpcfan/awesome-tutorials 下载。然后键入git checkout chap03切换到本章代码。


附件下载:https://developer.aliyun.com/topic/download?id=386

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:

华章出版社

官方博客
官网链接