代码已提交至Github
通过router已经把主页和详情页分开了,但是分的还不够彻底。
gundam-host.component.ts和gundam-detail.component.ts还是太臃肿了。
理论上,处理数据相关的业务逻辑应该再进一步从controller里分离,而Angular提供的分离方式就是service。
在gundam-host.component.ts文件里,获得gundam列表的方式是导出数组,但如果其他地方也用到该数据,或者数据需要进一步加工、改变、添加——每次改变都要在所有用到地方修改,总得来说是不科学的。
在src/service目录下新建gundam.service.ts文件。
import { Injectable } from '@angular/core';
@Injectable()
export class GundamService{
}
ps:有关@Injectable和@Component,都是angular中的关键字或者关键注解。通过注解来表明js文件的类型,以方便angular框架进行调用。
@Component表示该js文件所导出的类是组件。
@Injectable表示该js文件所导出的文件是服务,而服务是可以通过注入来创建的。
服务的注入,是angular中用来剥离controller和业务逻辑的方式。
我们准备用这个处理gundam相关业务的service,要取代2个组件中的具体的逻辑。
目前gundam-host.component.ts文件长这样:
import {
Component
} from '@angular/core';
import { Gundam } from '../../model/gundam';
import { GUNDAMS } from './../../service/data';
@Component({
template: `
"let gundam of gundams" routerLink="/detail/{{parseGundamToString(gundam)}}">
{{gundam.name}}
`
})
export class GundamHostComponent {
gundam: Gundam = {
name: '海牛',
type: 'NewType'
};
gundams = GUNDAMS;
selectedGundam: Gundam; // 定义一个selectedGudam作为展示详情的变量
onSelected (gundam: Gundam): void {
this.selectedGundam = gundam; // 通过参数赋值
}
parseGundamToString(gundam: Gundam): string {
return gundam.name + '&' + gundam.type;
}
}
selectedGundam接受参数的类已经不需要了,删掉。
定义的gundam类已经不需要了,删掉。
gundams = GUNDAMS的数据初始化也不会用到了,删掉。
归纳起来,host文件只有2个功能:
1. 获得gundam列表。
1. 将gundam转为str。
先在service的class里增加上相关方法:
// 获得全部数据
getGundams(): Gundam[]{
return null;
}
// 将gundam转换成字符串
parseGundamToString(gundam: Gundam): string {
return gundam.name + '&' + gundam.type;
}
目前service其实还是从data.ts里获取数据,所以先把数组的import转移过来。而且希望gundam-host.component.ts一开始就获得gundam列表,所以用生命周期OnInit来初始化数据。
修改gundam-host.component.ts文件和gundam.service.ts文件。
gundam-host.component.ts:
import {
Component,
OnInit
} from '@angular/core';
import { Gundam } from '../../model/gundam';
@Component({
template: `
<div *ngFor="let gundam of gundams" routerLink="/detail/{{parseGundamToString(gundam)}}">
{{gundam.name}}
div>
`
})
export class GundamHostComponent implements OnInit {
gundams: Gundam[];
ngOnInit(): void {
this.gundams = null;
}
parseGundamToString(gundam: Gundam): string {
return gundam.name;
}
}
gundam.service.ts
import { Injectable } from '@angular/core';
import { Gundam } from '../model/gundam';
import { GUNDAMS } from './data';
@Injectable()
export class GundamService{
// 获得全部数据
getGundams(): Gundam[]{
return GUNDAMS;
}
// 将gundam转换成字符串
parseGundamToString(gundam: Gundam): string {
return gundam.name + '&' + gundam.type;
}
}
现在的问题是,如何把service引入到组件里?
理论上service是可以直接被new出来,但是Google官方并不推荐这么做。
官方的说法是:
The component has to know how to create a HeroService. If you change the HeroService constructor, you must find and update every place you created the service. Patching code in multiple places is error prone and adds to the test burden.
You create a service each time you use new. What if the service caches heroes and shares that cache with others? You couldn’t do that.
With the AppComponent locked into a specific implementation of the HeroService, switching implementations for different scenarios, such as operating offline or using different mocked versions for testing, would be difficult.简单的说就是如果用new的方法创建service,一点service本身的构造方法改编,那么就要在所有用到service的地方进行修改。同时,如果service中有缓存,如果影响其他service的话怎么办?
说了怎么多,用一句话总结:用new的方式创建对象,麻烦一大堆。
所以谷歌已经准备好了简单方便的办法,比如注入。
在gundam-host.component.ts注入gundamService的步骤如下:
1 导入gundamService
import { GundamService } from '../../service/gundam.service';
2 在gundam-host.component.ts里的@Component中追加一个属性provider,属性值类型为数组,把gundamService传进去。
providers: [GundamService]
3 在class中使用构造器constructor,把gundamService注入。
constructor(private gundamService: GundamService){}
经过以上3步,便可以在class里以直接调用gundamService实例 了。怎么创建实例由angular处理,不必再去关心了。
用gundamService代替具体的业务逻辑。
import {
Component,
OnInit
} from '@angular/core';
import { GundamService } from '../../service/gundam.service';
import { Gundam } from '../../model/gundam';
@Component({
template: `
<div *ngFor="let gundam of gundams" routerLink="/detail/{{parseGundamToString(gundam)}}">
{{gundam.name}}
div>
`,
providers: [GundamService]
})
export class GundamHostComponent implements OnInit {
gundams: Gundam[];
constructor(private gundamService: GundamService) {}
ngOnInit(): void {
this.gundams = this.gundamService.getGundams();
}
parseGundamToString(gundam: Gundam): string {
return this.gundamService.parseGundamToString(gundam);
}
}
刷新页面:
同理,改造gundam-detail.component.ts中的parseStringToGundam方法,也把他放到GundamService里并用service代替具体的业务逻辑。
import {
Component,
OnInit
} from '@angular/core';
import {
Gundam
} from '../../model/gundam';
import {
ActivatedRoute,
Params
} from '@angular/router';
import { GundamService } from '../../service/gundam.service';
import 'rxjs/add/operator/switchMap';
@Component({
template: `
<div *ngIf="selectedGundam">
{{selectedGundam.name}}
{{selectedGundam.type}}
div>
`,
providers: [GundamService]
})
export class GundamDetailComponent implements OnInit {
selectedGundam: Gundam;
gundamStr: string;
constructor(
private route: ActivatedRoute,
private gundamService: GundamService,
) {}
ngOnInit(): void {
this.route.params.switchMap((params: Params) => this.gundamStr = params['gundam'])
.subscribe(() => this.selectedGundam = this.gundamService.parseStringToGundam(this.gundamStr));
}
}
刷新界面,此时页面应该无异常。
在实际的项目中,数据保存在服务端。值传递是不会采用现在这种方式的,或者说全值传递是错误的。
举个简单的例子说明:在主页看到一件衣服的价格是80,点击进去入详情页准备购买时,主页传递所有数据包括价格80给详情页后,服务端更新了衣服的价格,比如说变成了800。购买的时候如果不做处理(一般服务端和前端都会做校验),就可以用80元购买800元的衣服,逻辑上是有漏洞的。
web页面之间的值传递,如果数据保存在服务器惯例是传递一个关键的key,在下个页面继续调用key再查一遍数据。
回到本项目,目前的用作Model的数据 gundam类里只有2个字段,type会有重复(巴巴托斯和红异端都是近战型机体),名字虽然不重复但是查询起来会很麻烦(影响效率),而且还涉及到转码与反转码,所以需要再增加一个字段:id。
修改gundam.ts,增加id字段,类型是number(数值)。
修改data.ts,增加id字段。
有了id,就不必在view之间传递字符串了。只需要在主页中把gundam的id传到详情页,在详情页拿到以后再去查询数据就可以了。
修改gundam-host.component.ts文件中的temple
<div *ngFor="let gundam of gundams" [routerLink]="['/detail', gundam.id]">
<span>
{{gundam.name}}
span>
div>
这里我换了一种写法,用 [routerLink]=”[‘/detail’, gundam.id] 代替了 routerLink=”/detail/{{gundam.id}}”。
这是官方给出的规范写法,不过之前那样写也能运行。
修改app-routing.module.ts文件,将path中的detail/: gundam 改成detail/:id
对于gundam-detail.component.ts文件中的ngOnInit方法,需要做如下修改:
1 拿到id以后不再需要把字符串转换成gundam,而是在service里查询对应的gundam。
在gundam.service.ts增加一个getGundamById的方法
// 根据Id查询高达
getGundamById(id: number): Gundam {
return this.getGundams().find( gundam => gundam.id === id );
}
箭头函数出现了!
this.getGundams()会返回一个gundam数组,gundam数组中有一个方法find,find接受一个函数 用gundam作为单个的item遍历gundam数组,返回gundam.id === id的gundam。
2 从路径中拿到的参数不再是gundam,是id。
完全修改gundam-detail.component.ts中的方法:
this.route.params.switchMap((params: Params) => this.gundamId = params['id'])
.subscribe(() => this.selectedGundam = this.gundamService.getGundamById(+this.gundamId));
注:千万注意getGundamById中的那个 + ,绝对是神坑。无论任何类型从params里拿出来的都是字符串类型,虽然定义了this.id是number也是然!并!卵!,不用 +转类型那么id依然是作为字符串类型传入的(JavaScript弱数据类型语言 没有类型校验),因为service中的比较是用gundam.id === id来做条件判断,而gundam.id的类型又是number。所以可能会出现
3 === ‘3’ //(会被判断为false)
从而不返回数据的尴尬。
所以要么定义成number然后用+转类型,要么干脆就定义成string类型然后用parseInt强转(parseInt只接受str类型做为参数)。
总的来说是一个相当蛋疼又相当隐蔽的大坑,别问我怎么知道的,反正我爬出来了.
gundam-host.component.ts和 gundam.service.ts中的字符串转换已经可以删除了。
刷新,可以正常显示。
到目前为止,所有的方法都是同步的,但是实际上数据是放在服务器上的。所以service请求数据的动作,都是异步请求。
所以还需要继续进一步修改gundam.service.ts,JavaScript对异步的解决方式就是Promise。
先改造获取所有列表中的gundam:
// 获得全部数据
getGundams(): Promise {
return Promise.resolve(GUNDAMS);
}
再改造获取单个gundam的方法
// 根据Id查询高达
getGundamById(id: number): Promise {
return this.getGundams().then( gundams => gundams.find(gundam => gundam.id === id ));
}
同时修改gundam-host.component.ts文件和gundam-detail.component.ts文件
gundam-host.component.ts:
ngOnInit(): void {
this.gundamService.getGundams().then(gundams => this.gundams = gundams);
}
gundam-detail.component.ts:
ngOnInit(): void {
this.route.params.switchMap((params: Params) => this.gundamService.getGundamById(+params['id']))
.subscribe( gundam => this.selectedGundam = gundam );
}
总的来说,Promise是比较眼花缭乱以及脑仁大的东西。有关promise和箭头函数,其实我也不太懂,但是我可以解释。
Promise对象接收2个函数:
1. resolve
1. reject
前者是异步正常的处理函数,后者是异步异常的处理函数。
我在getGundams(): 里直接用
Promise.resolve(GUNDAMS)
表示异步正常,返回一个包含着gundam数组的Promise对象。
在gundam-host.component.ts中的
this.gundamService.getGundams()
可以获得该Promise对象。
Promise自带then方法,而then方接收的就是**resolve和reject**2个函数。因为异步正常,所以接受异步正常的Promise对象中包裹的元素作为参数,也就是 gundams => this.gundams = gundams 中的gundams 就是 Promise.resolve(GUNDAMS) 中的GUNDAMS。
resolve函数固定参数不固定方法体,从而可以在方法体内对返回值进行进一步操作。
then方法中的第二个函数可写可不写,但是如果写了的话也是固定参数error不固定方法体,参数error表示在异步的时候出现的错误。
在
getGundamById(id: number)
中同理,我查询了全部的数组,在then方法里返回一个数组属性id和传入的id相等的元素。
有点绕,我也是写了好久才明白的。
ES6里的箭头函数,我个人是又爱又恨。爱它写法是如此的优雅简洁,恨它简洁起来完全求尼玛老公抱的看不懂。
经过改造以后,业务逻辑已经被单独抽离成service。只要在组件中注入service,即便业务逻辑有变更,只需要在service中修改就可以达到改一处动全身,controller的耦合度进一步降低了。
现在的项目形态已经非常接近完全体了,当然可能scss样式不怎么好看(偷懒没有写),但是骨架上已经只差一点点了。
这差的一点点,就是数据源。