本节书摘来自华章出版社《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切换到本章代码。