Angular 2(十): websocket

概览

  • 服务器端的socket和客户端的服务中的socket保持长连接,进行数据交换;
  • 客户端中的服务(socket)作为”被观察者”,客户端中的组件作为观察者;
  • 被观察者通过依赖注入,被注入到组件中;

组件订阅socket产生的流,当流发生变化时,组件就会产生相应的事务。

websocket.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export class WebSocketService{
ws: WebSocket;
createObservableSocket(url:string):Observable<any>{
this.ws = new WebSocket(url);
return new Observable(observer=>{
//指定什么时候派发数据:当后端返回数据时
this.ws.onmessage = (event)=>observer.next(event.data);
//指定错误发生时如何处理
this.ws.onerror = (evevt)=>observer.error(event);
//指定关闭时如何处理
this.ws.onclose = (event)=>observer.complete();
})
};
sendMessage(message:string){
this.ws.send(message); //向服务器发送数据
};
}

component.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
constructor(private wsServer: WebSocketService){}
ngOnInit(){
this.wsServer.createObservableSocket()
.subscript(
data=>console.log(data); //当流发送过来消息时
err=>console.log(err);
()=>console.log('流已经结束');
)
};
sendMessageToServer(){
this.wsServer.sendMessage(‘这是从组件向服务器发送的信息!’);
};

Angular 2(十): 指令

生成指令命令行:ng g directive directiveName

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Directive } from "@angular/core";
import { mobileValidator } from '../validators';
@Directive({
selector:'[ mobile ]',
providers:[
{
provide: NG_VALIDATORS,
useValue:mobileValidator,
//设置该字段,表示一个token下面可以挂载多个指令
multi:true
}
]
})
export class MobileValidatorDirective implements Init{
constructor(){}
}
1
<input type="text" #mobile="ngModel" ngModel name="mobile" (input)="onMobileInput(myForm)"/>
1
2
3
4
5
6
7
8
9
10
mobileValid:boolean = true;
mobileUntouched:boolean = true;
onMobileInput(formData:NgForm){
if(formData){
//用这两个值来控制错误信息的显示和隐藏
this.mobileValid = formData.form.get('mobile').valid;
this.mobileUntouched = formData.form.get('mobile').untouched;
}
}

Angular 2(八): 表单

标准HTML表单

  • 相关检测属性:required、partern、min、max
  • 一般写法
    1
    2
    3
    <form action="/register" method="post">
    <input type="text"/>
    </form>

在Angular中,有两种形式的表单:响应式表单、模板式表单

模板式表单:表单的数据模型是通过组件中的指令隐式创建的。使用这种方式定义表单的数据模型时,受限于HTML语法,模板驱动方式仅适用于一些简单的场景。

响应式表单:通过编写ts代码而不是HTML来创建底层数据模型。在模型创建之后,可以使用一些特定的指令将模板的HTML元素与底层数据模型连接在一起。

模板式表单

在Angular中,标准表单会被自动托管,常规的操作无效。如果希望使用原始表单,只需要在form标签内加上ngNoForm指令即可;

1
<form action="/register" method="post" ngNoForm></form>

知识梳理

  • #myForm=”ngForm”获取表单对象,myForm.value获取表单的值,取值形式:

  • (ngSubmit)=”onSubmit(myform.value)”代替了默认的submit

  • 只能在HTML中操作数据模型,不能在ts中操作

  • 在ngForm上使用双向绑定时,不需要像以往那样[(ngModel)]=”user.name”,改写为:直接在标签中加入ngModel指令,同时指定name属性:name=”username”

  • 如果要在ngmodel上使用组件模板变量,#username=”ngModel”,取值形式:

  • ngForm=>隐式创建FormGroup

  • ngModel=>隐式创建FormControl

  • ngModelGroup=>隐式创建FormGroup(嵌入结构,用于将一些字段更好地组织在一起,加入指定的对象中)

模板式表单的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm.value)">
<div>
姓名
<input type="text" #username="ngModel" ngModel name="username"/>
</div>
<div ngModelGroup="psw">
<div>
密码
<input type="text" #psw_1="ngModel" ngModel name="psw_1"/>
</div>
<div>
密码
<input type="text" #psw_2="ngModel" ngModel name="psw_2"/>
</div>
</div>
<button type="submit">提交</button>
</form>

1
2
3
onSubmit(val:any):void{
console.log(val); //查看表单的值
}

响应式表单

  • 响应式表单不可以使用模板变量,如:#myForm,只能在ts中操作数据模型

  • FormControl

    1
    2
    3
    4
    5
    6
    7
    8
    userName:FormControll=new FormControl();
    //可以指定初始值,userName:FormControll=new FormControl('Default');
    //常规使用
    <input [formControl]="username"/>
    //如果放在FormGroup中,需要改变写法
    <input formControlName="username"/>
  • FormGroup

    1
    2
    3
    4
    5
    6
    //构造函数需要一个对象
    //一般用于创建一组固定的子集,使用key检索
    formModel:FormGroup = new FormGroup({
    from: new FormControl(),
    to: new FormControl(),
    });
  • FormArray:创建一个可变长度子集

    1
    2
    3
    4
    5
    6
    //构造函数需要一个数组
    //使用索引值检索
    emails: FormArray = new FormArray([
    new FormControl(),
    new FormControl(),
    ])

响应式表单的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//注意(submit)和(ngSubmit)写法的区别
<form [formGroup]="formModel" (submit)="onSubmit()">
//通过formControlName将底层数据模型和HTML元素绑定在一起
//formControl在FormGroup中的写法
<input type="text" formControlName="username">
//使用formGroupName将特定的属性存储在指定(dateRange)的对象中
<div formGroupName="dateRange">
//FormGroup的实例对象通过key(from、to)检索元素
开始日期:<input type="date" formControlName="from"/>
结束日期:<input type="date" formControlName="to"/>
</div>
<div>
//绑定到FormArray的实例对象
<ul formArrayName="emails">
//由于FormArray是可变长度,一般与ngFor搭配使用
//this.formModel.get('emails')获取的是一个new FormArray对象,通过.controls取到内部元素集合
<li *ngFor="let email of this.formModel.get('emails').controls;let i = index;">
//由于FormArray使用的是索引值检索,故使用索引值作为模板的名字
//formControlName在此处为输入属性
邮箱:<input type="email" [formControlName]="i"/>
</li>
</ul>
<div (click)="addEmail()">增加一个</div>
</div>
<button type="submit">保存</button>
</form>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//在ts中建立底层数据模型
formModel:FormGroup = new FormGroup({
//formControl在FormGroup中的写法
username:new FormControl(),
dateRange:new FormGroup({
from: new FormControl(),
to: new FormControl(),
}),
emails:new FormArray([
new FormControl(),
])
});
addEmail():void{
//通过this.formModel.get('emails')获取的是特定的FormArray的实例对象(类型为对象),需要转换成FormArray类型
var emails = this.formModel.get('emails') as FormArray;
emails.push(new FormControl());
}
onSubmit():void{
//获取表单的值
console.log(this.formModel.value);
}

特别介绍

数据类型:FormGroup、FormControl、FormArray

指令:formGroup、formControl

指令:formGroupName、formControlName、formArrayName

  • formGroup和formGroupName用来绑定FormGroup
  • formControl和formControlName用来绑定FormControl
  • formArrayName用来绑定FormArray

Angular表单进阶

借助Angular内部工具FormBuilder改建表单

1
2
3
4
5
6
7
8
9
10
11
formModel: FormGroup;
constructor(public fb:FormBuilder){
this.formModel = fb.group({
username:[''],
mobile:[''],
passwords:fb.group({
psw_1:[''],
psw_2:[''],
})
})
}

表单校验

  • 标准格式
    1
    2
    3
    nameValidator(control:AbstractControl):{[key:string]:any}{
    return null;
    }

从标准格式可以看出,校验之后返回的对象为null或者一个key为字符串的对象

  • Angular提供的校验器:Validators.required、Validators.minLength(len)等等
    1
    2
    3
    4
    //使用自带校验器对username字段进行校验
    //在数据模型中(formModel)
    //校验器以数组的形式出现
    username: ['', [ Validator.required,Validator.minLength(6)] ]
1
2
3
4
onSubmit():viod{
let isValid:boolean = this.formModel.get('username').valid;
let error:any = this.formModel.get('username').error;
}
  • 自定义校验器

对于单独的FormControl检验

1
2
3
4
5
6
7
mobileValidator(control:FormControl):any{
let reg = /^[0-9]*$/;
let valid = reg.test(control.value);
//当校验通过时,返回null,否则返回一个空对象
return valid ? null : { mobile:true }
}

  • 注意校验器放置的位置
    1
    mobile:['', this.mobileValidator]

对于放在FormGroup内部的FormControl的校验

1
2
3
4
5
6
7
8
equalPswValidator(group:FormGroup):any{
let psw_1:FormControl = group.get('psw_1') as FormControl;
let psw_2:FormControl = group.get('psw_2') as FormControl;
let valid = ( psw_1.value === psw_2.value );
return valid ? null : { equalPsw:{msg:'我是错误提示信息!'} }
}

  • 注意校验器放置的位置和写法:{validator:this.equalPswValidator}
    1
    2
    3
    4
    passwords:fb.group({
    psw_1:[''],
    psw_2:[''],
    },{validator:this.equalPswValidator})

异步校验器:

通过代码模拟http请求处理

1
2
//只需要修改return语句即可,返回的是一个可观察对象
return Observable.of(valid? null:{mobile:{msg:'错误信息!'}}}).delay(3000);

1
mobile:['',mobileValidator,asyncMobileValidator]

通过校验器可以做到的额外的事

  • 通过校验器方法来控制组件中的提示语句隐藏或者显示:hasError、getError
  • 获取独立的FormControl的校验结果:formModel.hasError(‘mobile’,’mobile’);
  • 获取存在FormGroup内部的FormControl的校验结果:formModel.hasError(‘minLength’,[‘passwords’,’psw_1’])
  • 获取独立的FormControl校验反馈的信息:formModel.getError(‘mobile’,’mobile’);
  • ④获取存在FormGroup内部的FormControl校验反馈的信息:formModel.getError(‘equalPsw’,’passwords’) ? msg:null;

注意方法的第一个参数不是校验器的名称,而是校验器返回来的对象的key,即:

1
2
return valid ? null : { equalPsw:true } 中的 equalPsw
return valid ? null : { mobile:true } 中的mobile

在④中,如果发生错误,那么将会获取到返回的“错误提示信息”。

完整的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<form [formGroup]="formModel" (submit)="onSubmit()">
<div>
用户名:<input type="text" formControlName="username"/>
<div [hidden]="!formModel.hasError('minlength','username')">
用户名输入不正确
</div>
</div>
<div>
手机号:<input type="text" formControlName="mobile"/>
<div [hidden]="!formModel.hasError('mobile','mobile')">
<div>{{ formModel.getError('mobile','mobile')?.msg }}</div>
</div>
</div>
<div formGroupName="passwords">
<div>
密码:<input type="text" formControlName="psw_1"/>
<div [hidden]="!formModel.hasError('minlength',['passwords','psw_1'])">
密码最小长度不符合要求
</div>
</div>
<div>
确认密码:<input type="text" formControlName="psw_2"/>
<div [hidden]="!formModel.hasError('equalPsw','passwords')">
{{ formModel.getError('equalPsw','passwords')?.msg }}
</div>
</div>
</div>
<button type="submit">提交</button>
</form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
formModel:FormGroup;
constructor(fb:FormBuilder){
this.formModel = fb.group({
username:['',[Validators.required,Validators.minLength(6)]],
mobile:['',this.mobileValidator], //发现一个现象,如果此处加上Validators.required,程序会报错
passwords:fb.group({
psw_1:['',Validators.minLength(8)],
psw_2:[''],
},{validator:this.equalPswValidator})
})
}
mobileValidator(control:FormControl):any{
let reg = /^[0-9]*$/;
let valid = reg.test(control.value);
return valid ? null : { mobile:{msg:'手机号码不正确!'} }
}
equalPswValidator(control:FormGroup):any{
let psw_1 = control.get('psw_1') as FormControl;
let psw_2 = control.get('psw_2') as FormControl;
let valid = (psw_1.value === psw_2.value);
return valid ? null : { equalPsw:{msg:'密码错误提示信息!'}}
}
onSubmit(){
let valid = this.formModel.valid;
console.log(valid);
};

状态字段

  • touched/untouched:字段是否获取过焦点,如果获取过则:true/false,否则相反;

    1
    2
    3
    <div [hidden]="!formModel.get('mobile').valid || formModel.get('mobile').untouched">
    当获得过焦点,且表单校验为false,那么显示这段错误提示信息!
    </div>
  • pristine/dirty:如果一个值未被修改过,则:true/false,否则相反;

    1
    2
    3
    <div [hidden]="formModel.get('mobile').valid || formModel.get('mobile').pristine">
    当值被修改过,且不合法的情况下显示该提示信息!
    </div>
  • pending:当一个字段正处于异步校验中时,该字段为true

Angular 2(九): 服务

Angular的http服务返回的是Observable(可观察数据流),通过订阅来获取可用数据。

订阅方式有:手动订阅(subscript)、异步管道订阅(async)

手工订阅

1
<div *ngFor="let prod of products">{{prod.name}}</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
products:Array<any> = [];
dataSource:Observable<any>;
constructor(private http:Http){
this.dataSource = this.http.get('/products')
//由于返回的数据格式是Response,过需要转成JSON
.map((res)=>res.json())
};
ngOnInit():void{
this.dataSource.subscript(
(data)=> this.products = data;
)
}
  • http请求的发送并不是由get方法触发的,而是subscribe;
  • get方法只是定义了一个http请求,请求的发送是通过订阅完成的;

异步管道订阅

1
<div *ngFor="let prod of products | async">{{prod.name}}</div>
1
2
3
4
5
6
products:Observable<any> = [];
constructor(private http:Http){
this.products = this.http.get('/paoducts')
.map((res)=>res.json());
}

添加Headers

在定义https请求时,需要添加headers的时候,如下:

1
2
3
4
5
6
7
constructor(private http:Http){
let myHeaders:Headers = new Headers();
myHeaders.append("Autor","tuluffy");
this.products = this.http.get('/products',{headers:myHeaders})
.map((res)=>res.json());
}

其他

有些时候Angular启动的端口地址和服务器端口地址不一致,导致所有的http请求失败。我们需要做端口转发。

  • 新建转发配置文件,proxy.conf.json:
    1
    2
    3
    4
    5
    {
    "/api":{
    "target":"http://localhost:8080"
    }
    }

即:将以”/api”开头的请求地址转发到”http://localhost:8080

  • 修改package.json的start项

    1
    start:"ng serve --proxy-config proxy.conf.json"
  • 修改请求的地址

    1
    2
    3
    this.http.get('/products').map((res)=>res.json())
    改为
    this.http.get('/api/products').map((res)=>res.json())

Angular 2(七): 生命周期钩子

钩子的调用顺序:

consttructor -> ngOnChanges -> ngOnInit -> ngDoCheck ->

ngAfterContentInit -> ngAfterContentChecked -> ngAfterViewInit ->

ngAfterViewChecked -> ngOnDestroy

ngOnchanges

  • 当父组件初始化或者修改子组件的输入属性时调用;
  • 如果一个组件没有输入属性,那么永远不会调用该钩子;
  • 首次调用一定在ngOnInit之前;
  • 可以被多次调用;
    可变对象和不可变对象
    1
    2
    3
    let val = 'hello';
    val = 'hello world';
    `
  • ‘hello’在内存中被创建后赋值给val;
  • ‘hello world’在内存中被创建后赋值给val;

无论val如何变化,’hello’在内存中的地址始终保持不变。字符串是不可变对象。

1
2
let user = { name:'roy' };
user.name = 'joy';
  • 在内存中创建了user,name属性指向’roy’所在的地址;
  • 在内存中创建了’joy’,地址赋值给了user的属性name;
  • name发生了变化,但是user的地址始终不变;也就是说由于属性的改变,user(对象)是可变对象;

ngOnChanges触发机制

父组件
1
2
3
4
5
6
7
8
9
10
11
<div>我是父组件</div>
<div>
<span>问候语</span>
<input type="text" [(ngModel)]='hello'/>
</div>
<div>
<span>名字</span>
<input type="text" [(ngModel)]='user.name'>
</div>
<app-child [childHello]='hello' [childUser]='user'></app-child>
1
2
hello:string;
user:{name:string} = { name: '' };
子组件
1
2
3
<div>{{childHello}}</div>
<div>{{childUser.name}}</div>
<input type="text" [(ngModel)]="message"/>
1
2
3
4
5
6
7
8
@Input() childHello:string;
@input() childUser:{name:string};
message:string;
① ngOnChanges(changes:SimpleChanges):void{
console.log(changes);
}

以上代码结果表现为:

  • 当输入问候语hello时,①被触发。原因:由于字符串是不可变对象,然后输入后却发生了变化,故调用ngOnChanges;
  • 修改名字name时,①不触发。原因:由于修改的是可变对象(user的属性虽然变化了,但是user在内存中的地址依然没变),故不会触发ngOnChanges;
  • 当修改message时,①不处罚。原因:ngOnChanges钩子只针对输入属性,message为非输入属性;

虽然修改可变对象不会触发ngOnChanges钩子,但是子组件上的值已然发生了变化。这是由于Angular的变更检测机制仍然不活了组件中每个对象的属性的变化。


ngDoCheck

在这里,需要先重点说一下Angular的变更检测机制
  • 在Angular1.x中,任何原生事件都不会触发脏检查,必须使用NG事件才有效。如果使用了原生事件,你需要指定$apply()和$digest()来告诉Angular使用变更检测去处理它。
  • 在Angular2中,变更检测机制是由zone.js提供的,保证组件的属性的变化和页面的变化是同步的。

    • 浏览器中发生的任何事件都会触发变更检测,如:点击事件、输入数据;
    • 可以随意使用任何原生事件;

变更检测机制仅仅是将组件属性的改变反应到模块上,它并不会去改变组件属性的值。

变更检测
  • 每一个组件生成属于他们的变更检测器。当属性发生变化时,变更检测器指定的变更检测机制(default和Onush)就会响应,并判断是否需要更新模块;
  • 变更检测策略:
    • Default策略:采用Default策略的组件,无论组件树的哪个地方发生变化,都会被检测;检测顺序:根组件->子组件->孙组件;
    • Onpush策略:只有当组件的输入属性发生变化,变更检测机制才会检测该组件及其子组件;
对可变对象的检测:ngDoCheck
1
2
3
4
5
6
ngDoCheck():void{
if(this.user.name != this.oldname){
//do something
console.log(this.user.nane); //新值
}
}
  • 组件中只要有事件发生,ngDoCheck就会被调用①,比如:在两个文本框之间切换;
  • 由于①的原因,ngOnCheck非常容易被调用,使用时需要小心。对于ngOnCheck的实现要非常高效和轻量,否则容易因其性能问题;
  • 在Default策略下,每次变更检测,组件树中所有带check后缀的钩子都会被调用;

view钩子

  • 子组件

    1
    2
    3
    4
    5
    6
    getVal(val:string):void{
    console.log(val);
    }
    ①ngAfterViewInit():void{}
    ②ngAfterViewChecked():void{}
  • 父组件

    1
    2
    3
    4
    <div>我是父组件</div>
    <app-child #child1></app-child>
    <app-child #child2></app-child>
    <button (click)="child2.getVal('joy')"></button>
1
2
3
4
5
6
7
8
@ViewChild('child1') child1:ChildComponent;
ngOnInit():void{
this.child1.getVal('hahh');
}
③ngAfterViewInit():void{}
④ngAfterViewChecked():void{}

钩子调用顺序:①->②->③->④

  • 很明显,子组件必须在父组件之前组装完毕,init方法只会被调用一次;
  • 在变更检测周期中禁止修改属性,如果必须修改需要在子线程中操作;
    1
    2
    3
    4
    5
    6
    7
    8
    ngAfterViewInit():void{
    this.val = "hhahh"; //严重错误
    //解决方案
    setTimeout(()=>{
    this.val = "hahha";
    },0)
    }

ngContent指令

  • 一般写法

子组件

1
2
3
4
<div>
<div>我是子组件</div>
<ng-content></ng-content> //投影点
</div>

父组件

1
2
3
4
5
6
7
<div>
<div>我是父组件</div>
<app-child>
<div>这里面是投影内容</div>
<div>{{title}}</div> //虽然是投影在子组件中的内容,但是只能在父组件中操作
</app-child>
</div>

  • 多个投影的写法
    父组件
    1
    2
    3
    4
    5
    6
    7
    <div>
    <div>父组件</div>
    <app-child>
    <div class="firstPart">这是第一块内容</div>
    <div class="secondPart">这是第二块内容</div>
    </app-child>
    </div>

子组件

1
2
3
4
5
<div>
<div>子组件</div>
<ng-content select=".firstPart"></ng-content>
<ng-content select=".secondPart"></ng-content>
</div>


ngAfterContentInit、ngAfterContentChecked

  • 在投影内容组装完成之后调用、检测之后调用;

  • 执行顺序:ngAfterContentInit -> ngAfterContentChecked -> ngAfterContentInit -> ngAfterContentChecked -> ngAfterViewInit

  • 与ngAfterView的钩子不同,在ngAfterContent的钩子中可以改变属性的值


ngOnDestory

  • 当组件销毁的时候调用该钩子(配合路由使用);
  • 往往在该钩子中处理“反订阅一个流”、“清除定时器”等操作;

总结

  • 组件初始化

    ①constructor

    ②ngOnChanges

    ③ngOnInit

    ④ngDoCheck

    ⑤ngAfterContentInit

    ⑥ngAfterContentChecked

    ⑦ngAfterViewInit

    ⑧ngAfterViewChecked

    ⑨ngOnDestory

  • 组件销毁

    ⑨ngOnDestory

  • 变更检测

    ②ngOnChanges

    ④ngDoCheck

    ⑥ngAfterContentChecked

    ⑧ngAfterViewChecked

每一个组件都会经历三个阶段:初始化、变更检测、销毁

  • 当通过路由激活一个组件(按先后顺序):

    组件初始工作:

    • 启动constructor,提供所需实例化的对象

    • 如果有输入属性,启动ngOnChanges检测输入属性的变更

    • 启动ngOnInit,初始化一般数据

    • 启动ngDoCheck,进行一次变更检测

    开始组装组件:

    • 当组件中的投影内容被组装时,调用ngAfterContentInit

    • 启动ngAfterContentChecked,对投影内容检测

    • 当整个组件被组装时,调用ngAfterViewInit

    • 启动ngAfterViewChecked,对整个组件检测

      组件变更:

    • 当有事件发生(用户操作),启动ngDoCheck进行变更检测

    • 处理变更,如果需要更新组件,启动ngAfterContentChecked、ngAfterViewChecked;如果输入属性发生变化,启动ngOnChanges

    • 路由事件发生,启动ngOnDestory销毁组建,从头开始初始化一个组件;

Angular 2(六): 输入属性、输出属性

输入属性

父组件单向向子组件中传值

  • 父组件:

    1
    2
    <div>我是父组件</div>
    <app-child [myVal]="val"></app-child>
    1
    val: string = '这是在父组件的控制器中赋值!';
  • 子组件

    1
    <div>{{myVal}}</div>
    1
    2
    3
    //在子组件中将“myVal”声明为输入属性
    //至此,父组件中的值就能直接传递给"myVal"了
    @Input() myVal;

输出属性

数据从子组件中发射,在父组件中捕获

  • 子组件:对外发射数据

    1
    2
    3
    4
    5
    6
    7
    ① @Output()
    ② outObjName: EventEmitter<Product> = new EventEmitter();
    constructor(){
    ③ let prod: Product = new Product("joy",13);
    ④ this.outObjName.emit(prod);
    }
    • ①②表示声明一个输出属性(也可以说成输出对象),该属性具有发射数据的能力;
    • ②中,需要为声明的属性指明一个泛型,EventEmitter<泛型>。该泛型即是:对外发射的数据的类型;
    • ③生成一个符合规则的数据;
    • ④使用输出属性将数据发射出去(发射到父组件中);
  • 父组件:接收来自于子组件的数据
    1
    2
    <div>我是父组件</div>
    ④ <app-child (outObjName)="getDataFromChild($event)"></app-child>
1
2
3
4
5
① prodInParent : Product = new Product();
② getDataFromChild(ev:Product){
③ this.prodInParent = ev;
}

在Angular中,捕获子组件中发射的事件,和捕获原生的DOM事件是一样的操作。

  • ①中新建了了一个Product的实例,为接收数据做准备;
  • ②自定义函数,捕获从子组件中发射出来的指定类型的数据;
  • ③使用捕获到的数据
  • ④对子组件中发射事件进行监听,一旦捕获到,立即响应指定的操作(getDataFromChild)

补充说明:

  • 一般情况下,父组件中“捕获的事件名称”和子组件中“输出属性的名称”保持一致;
  • 如果需要给输出属性指定别名,可以:@Output(‘otherName’)。这时,在父组件中④的位置outObjName需要与otherName相同;

中间人模式:

  • 简单来说,使用一个公共的父组件来连通两个不相关的组件,使之间的数据和事件响应在一定程度上得到关联;
  • 使用中间人模式既实现既定功能,也实现了组件之间的解耦;

Angular 2(五): 注入器、数据绑定、管道

注入器

1.注入器及其层级关系

  • 第一步:Angular在应用启动时,会生成一个应用级的注入器,将主模块和引用模块中的提供器(provide)全部注入到其中;

    • NgModule中的providers内声明的提供器
    • imports中的模块(BrowserModule、HttpModule)依赖的提供器
  • 第二步:Angular创建启动模块指定的主组件(AppComponent),同时应用级的注入器会为主组件创建一个组件级的注入器。

    并将主组件中声明的提供器注入到组件级的注入其中;

  • 第三步:在主组件中的子组件被创建时,主组件的注入器会为被激活的子组件创建一个组件级的注入器;

    并将子组件中的提供器注入其中;

    以此类推,最终形成一套注入器层级关系。

    2.注入器的使用

  • Angular只提供在构造函数中注入的方式来创建服务;

    1
    2
    3
    constructor(private service:Service){
    //this.service.methodName();
    }
  • 除了常用的在构造函数的参数直接声明服务以外,还可以手动调用注入器获取需要的服务

    1
    2
    3
    4
    constructor(private injector:Injector){
    this.service = injector.get("Service");
    this.service.methodName();
    }

数据绑定

Angular才用的是DOM属性绑定的模式来完成数据绑定。

1.DOM属性绑定流程

1
<input type="text" value='1' (input)="console($event)"/>
1
2
3
4
console(ev):any{
① console.log(ev.target.value);
② console.log(ev.target.getAttribute('value'));
}

在input事件发生的时候,在控制台中可以看到①打印的值与文本框保持一致,而②始终是默认的value值;

这是因为HTML属性指向的是初始值,而DOM属性指向的是当前值,且HTML属性不可变;

  • DOM属性发生变化时的流程
    1. (组件)input中的值发生变化
    2. DOM属性发生变化,HTML属性不变
    3. 浏览器保持DOM属性和UI同步
    4. value渲染到页面

2.HTML属性绑定流程

  • HTML属性发生变化时的流程
    1
    2
    ① <div [colspan]="{{1+1}}"></div>
    ② <div [attr.colspan]="val"></div>

由于Angular使用的是DOM属性绑定,而DOM属性中没有colspan,故①中的写法会报错;

  • DOM属性发生变化时的流程

    1. (控制器)数据发生变化
    2. 更新HTML元素,不更新DOM元素
    3. (浏览器同步HTML元素和DOM对象)DOM节点发生变化,造成DOM对象的属性发生变化
    4. 浏览器同步DOM属性和UI
    5. 渲染页面

管道

cli命令创建管道:ng g pipe pipeName

1.管道的标准形式

1
2
3
4
5
6
7
8
9
import { Pipe,PipeTransform } from '@angular/core';
@Pipe({ name: "timeFilter" })
export class TimeFilter implements PipeTransform{
transform(value:any,argu:any):any{
return null;
}
}

2.管道:是数据扩大指定的倍数

  • extraMultiple.ts

    1
    2
    3
    4
    transform(value:number,multiple?:number):number{
    if(!multiple){ multiple = 1; }
    return value * multiple;
    }
  • app.component.html

    1
    <p>{{ val | extraMultiple: '3' }}

如果有多个过滤参数:

1
<p>{{ val | pipeName: 'argu1' : 'agru2' }}</p>


其他:

  • 少量HTML属性和DOM属性有一一对应的关系,如“id”;
  • 有些HTML属性没有对应的DOM属性,如“colspan”;
  • 有些DOM属性没有对应的HTML属性,如“textContent”;
  • 就算属性名字相同,DOM属性和HTML属性不是一样的东西;
  • HTML属性指定初始值(不可变),DOM属性指向当前值(可变);

Angular 2(四): 依赖注入和控制反转

基础概念

依赖注入和控制反转

  • A依赖于B,A不需要在代码内部直接创建B的实例对象,而是由外部提供(B的实例对象);
  • 将依赖的控制权从代码的内部转移到代码的外部;

依赖注入和控制反转是一体两面,依赖注入是目的,控制反转是手段,终究实现代码的松耦合。

token

  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    @NgModule({
    ①providers: [ ProdService ]
    ②providers: [{
    provide: ProdService,
    useClass: ProdService,
    }]
    })
  • token在Angular中代表一个被注入的对象的类型,由provide决定;
  • 如:示例代码中,注册的Token的类型为ProdService;
  • 当有组件或者指令声明(在其构造函数中)需要一个类型为ProdService的token时,注入器就会实例化一个ProdService对象(由useClass决定实例化哪个),并将其注入到组件中;
  • 在组件中,通过构造函数获得该对象
    1
    2
    3
    constructor(private prodService:ProdService){
    //prodService
    }

服务的三种使用形式:useClass、useFactory、useValue

@Injectable(),表示可以在该类中注入其他服务,建议所有的服务都加上该声明;

useClass

  • 服务建议放在模块(即:app.module.ts的providers中)中,使之成为全局共享;
  • 如果将服务放在组件中,那么它只能在局部使用(组件及其子组件)。对于同名的token请求,组件中的服务类将覆盖模块中的服务类;

1.productService.ts

1
2
3
4
5
6
7
@Injectable()
export class ProductService{
//do something
getProduct():Product{
return new Product(...)
}
}

2.anthorProductService.ts

1
2
3
4
5
6
7
@Injectable()
export class AnthorProductService implements ProductService{
//do something
getProduct():Product{
return new Product(...);
}
}

3.app.module.ts

1
2
3
@NgModule({
providers:[ ProductService ]
})

4.product.component.html

使用在模块中声明的服务

1
2
3
4
5
6
@Component({
...,
})
export class ProductComponent implements OnInit{
constructor(private ps:ProductService){}
}

使用在组件中的服务

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component({
...,
providers:[
{
provide:ProductService,
useClass:AnthorProductService
}
]
})
export class ProductComponent implements OnInit{
//声明使用ProductService类型的token,会使用AnthorProductService类创建实例
constructor(private ps:ProductService){}
}


提供器之 useFactory

在提供服务时,可能要根据不同的状态去初始化不同的服务实例,这时需要使用useFactory提供器来完成。

通过随机数模拟开发/生产状态来实例化不同的服务类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
providers:[
{
provide: ProductService,
useFactory:()=>{
① let log = new LogService();
let dev = Math.random() > 0.5;
if(dev){
return new ProductService(log);
}
else {
return new AnthorProductService(log);
}
}
},
LogService
],

  • 以上,我们就完成了模拟开发/生产环境来提供不同的服务实例,而在组件内部使用的时候根本不知道我们提供的是什么。这也就实现了服务和组件的初步解耦。
  • 同时,如果你细心一点,在使用的过程中会发现整个应用内部ProductService类型的token对应的服务实例是同一个。由此可以说:工厂方法创建的对象是单例对象,在该对象被初次创建之后,整个应用内部都是使用同一个。
  • 但是,轻而易举地发现在useFactory的方法内部出现了①这样的写法,LogService和ProductService强耦合。我们需要解决这个问题,使用提供器的第三个参数配置deps(数组):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
provide: ProductService,
useFactory:(log:LogService)=>{
let dev = Math.random() > 0.5;
if(dev){
return new ProductService(log);
}
else {
return new AnthorProductService(log);
}
},
deps:[ LogService ]
},
LogService
  • 上面代码实现了服务(ProductService)和服务(LogService)之间的解耦。ProductService将使用外部的LogService来注入工厂方法内部,从而创建实例。

提供器之 useValue

该提供器方法常常结合useFactory使用

1
2
3
4
5
6
7
8
9
10
11
12
13
providers:[
{
provide...,
useFactory:(log:LogService,isDev)=>{
//do something
},
deps:[ LogService,"IS_DEV" ]
},
{
provide:"IS_DEV",
useValue:false
}
]

  • useValue的值也可以使用对象:
    1
    2
    3
    4
    {
    provide:"APP_CONFIG",
    useValue:{ isDev:false }
    }

Angular在启动的时候,在创建服务的过程中会对每一个服务的依赖进行实例化并注入到当前服务中,如此一层一层递进直至结束。于是

  • 每一个服务都是独立的,服务与服务之间不存在耦合。
  • 每一个组件在申请使用服务的时候,并不知道服务是如何构建的,组件与服务之间不存在耦合。

以上两点就是Angular实现“依赖注入”和“控制反转”的核心。

Angular 2(三): 子路由和守卫

子路由

子路由相对于根路由的概念而来,示例:children:数组,配置路由相关信息

路由配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
path:'product',
component:ProductComponent,
children:[ //关键字
{
path:'',
component:ProductDescComponent //商品简介组件
},
{
path:'seller/:id',
component:SellerDetailComponent //具体商品信息组件
}
]
}

父组件:

1
2
3
4
注意[routerLink] = "['./']",路径中加"."指明是匹配子路由
<a [routerLink] = "['./']">商品简介</a>
<a [routerLink] = "['./seller/3']">商品信息</a>
<router-outlet></router-outlet> //供子组件使用的路由插座


辅助路由

概览:辅助路由可以有多个,根据“name”字段来匹配

  • 路由配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    path:'xxx',
    component:XComponent,
    outlet:"aux", //匹配路由名字,确定放在哪个辅助路由上
    },
    {
    path:'yyy',
    component:YComponent,
    outlet:"aux",
    }
  • 父组件写法,假设是在app.component.html中
    1
    2
    3
    4
    5
    6
    <a [routerLink]="['/home',{ outlets:{ aux:"xxx" } }]">GOTO XXX</a>
    <a [routerLink]="['/home',{ outlets:{ aux:"yyy" } }]">GOTO YYY</a>
    <router-outlet></router-outlet> //主路由
    <router-outlet name="aux"></router-outlet> //辅助路由
    <router-outlet name="other"></router-outlet> //可以有多个辅助路由

路由守卫

路由守卫的意义在于,路由事件发生后,是否允许默认操作(比如,跳转到指定的页面)

如何使用?示例:

  • 路由文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import LoginGuard from "...";
    import TokenGuard from "...";
    const routes : Routes = [
    {
    path:'product',
    component:ProductComponent,
    //路由钩子对象(对应loginGuard.ts中的钩子接口)
    canActivate:[ //是否可以进入该组件
    LoginGuard //控制函数(路由钩子函数),只有当所有的函数都返回true时,才可以进入组件
    TokenGuard
    ]
    }
    ];
    @NgModule({
    import:[RouterModule.forRoot(routes)],
    exports:[RouterModule],
    providers:[ LoginGuard,TokenGuard ]
    })
  • 守卫文件:loginGuard.ts

    1
    2
    3
    4
    5
    6
    7
    export class LoginGuard implements CanActivate{
    CanActivate():boolean{ //接收一个boolean类型的返回值
    //处理逻辑,比如判断token是否有效
    //return true;
    return false;
    }
    }

增加一个CanDeactivate守卫,表示是否可以离开该组件(常用来检查用户填写的信息是否已经保存):

实现守卫(实现CanDeactivate守卫接口)接口,unSaveGuard.ts文件中
注意事项:

  • 该守卫需要接受一个组件作为参数(即保护的组件);
  • 在守卫函数中,需要创建组件的对象(comp),通过该对象获得组件内部的属性判断是否已经保存
1
2
3
4
5
6
7
8
9
10
11
12
import { CanDeactivate } from '...';
export class UnSaveGuard implements CanDeactivate<ProductComponent>{
canDeactivate(comp:ProductComponent){
//逻辑操作,对组件内部的变量进行验证是否已经保存
comp.varibleName;
//return true; //是否允许离开
return false;
}
}
  • 将守卫加入到路由配置中

    1
    2
    3
    4
    5
    6
    {
    path: 'product',
    component: ProductComponent,
    canActivate:[ LoginGuard,TokenGuard ],
    canDeactivate:[ UnSaveGuard ] //只有一系列函数都return true才可以顺利执行路由操作
    }
  • 在NgModule中配置

    1
    providers: [ loginGuard,TokenGuard,UnSaveGuard ]

resolve守卫

该守卫常用在进入下一个组件之前,执行服务获取下一个组件需要用到的数据,并带入组件中。
所以该组件需要可以被注入,在写时需要加入@Injectable()标识

  • 路由配置项改写

    1
    2
    3
    4
    5
    6
    7
    {
    path:'product',
    component:ProductComponent,
    resolve:{
    prod: ProductResolve //prod是传入组件中的参数集合的对象
    }
    }
  • 写productResolve.ts文件,提供ProductResolve函数

  • 该class需要指定返回的参数的格式,即:

    1
    2
    3
    4
    5
    6
    7
    export class ProductResolve implements Resolve<Product>{
    resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Product>|Promise<Product>|Product {
    //发起请求,获取数据
    return data; //将解析好的数据传递到组件内部,之后订阅该字段获取数据
    }
    }
  • 在组建中取值(采用订阅的方式)

    1
    2
    3
    4
    5
    6
    7
    constructor(private routeInfo:ActivatedRoute){}
    ngOnInit(){
    this.routeInfo.data.subscribe((data:{prod:Product}) => {
    this.id = data.prod.id;
    })
    }

本章内容主要通过代码展示,比较粗略。如果发现文档中的错漏,或者涉及到具体问题,可以联系QQ:3265564490,备注:Angular 笔记

Angular 2(二): 路由

前言

router是Angular 内置模块,可以使用cli 提供的指令来创建包含route 组件的项目

1
$ ng new router --routing

此时,在app.module.ts 文件中可以看到已经被自动引入到 imports组中。

1
2
3
4
imports: [
BrowserModule,
AppRoutingModule
],


路由组件相关字段含义

1. Routes,路由的配置项

  • 保存路由信息,即:url和component的对应关系
  • 指定组件展示在哪个路有插座上(<RouterOutLet></RouterOutLet>)

2. RouterOutLet,路由插座,即:激活的组件显示在Html 代码片段中的位置

3. Router,路由对象,提供在控制器中使用的一系列方法

  • navigate()
  • navigateByUrl()

4. RouterLink,在Html 中使用的导航指令

5. ActivatedRoute,当前激活的组件的路由对象

  • 路由地址
  • 路由参数

注意事项

1. 配置项中,path 对象的值不能使用”/“ 开头,这样可以使在项目中自由使用绝对路径和相对路径

1
2
3
4
5
6
const router:Router = [
{
path:"product", //path:"/product",错误写法
component:ProductComponet
}
];

应用

1
<a [RouterLink]="['/product']">GOTO</a>
  • 路由地址以’/‘ 开头,表示查找的是根目录组件
  • 值的形式是数组,由路由地址和参数组成:[‘url’,参数]

该写法等价于:

1
<a (click)="GOTO()">GOTO</a>

1
2
3
4
5
6
Controller:
constructor(private router:Router){}
GOTO():void{
this.router.navigate(['/product'])
}

2. 基础写法

  • 路由匹配是采取优先原则,通配符配置放在最后面,当路径错误是展现指定的页面
    1
    2
    3
    4
    5
    const router : Router =[
    { path:"",component:HomeComponent }
    { path:"product",component:Product }
    { path:"**",component:404Component } //通配符配置
    ]

3. 参数的传递和接收

路由参数传递的方式有三种,分别是:

① 在查询参数中传递
② 在路径中传递
③ 在配置项中使用特定对象data传递

  • 在查询参数中传递
    传递方式:

    1
    <a [routerLink]="['/product']" [queryParams]="{id:1}">GOTO</a>

    接收方式:

    1
    2
    3
    4
    5
    6
    7
    constructor(private routeInfo : ActivatedRoute){}
    private id : number;
    ngOnInit(){
    this.id = this.routeInfo.snapshot.queryParams["id"];
    }
  • 在路径中设置参数
    修改router中的配置:

    1
    { path:"product/:id", component:ProductComponent }

    修改Html中的RouterLink的写法:

    1
    <a [routerLink] = "['/product', 1]">GOTO</a>

    Controller:

    1
    this.id = this.routeInfo.snapshot.params["id"];
  • 在路由的配置项中使用data对象
    修改路由配置项:
    1
    2
    3
    4
    5
    6
    7
    8
    {
    path:"product",
    component:ProductComponent,
    data:[
    {id:1},
    {name:2},
    ]
    }

获取传进来的参数:

1
2
3
4
5
constructor(private routeInfo: ActivatedRoute ){}
ngOnInit(){
this.id = this.routeInfo.data[0]["id"];
}


参数快照和参数订阅

通过路由激活某一个组件之后,获取参数的方式有两种

  • 参数快照,关键字:snapshot

    1
    2
    3
    ngOnInit(){
    this.id = this.routeInfo.snapshot.params["id"];
    };
  • 参数订阅,关键字:subscribe

    1
    2
    3
    4
    5
    ngOnInit(){
    this.routeInfo.params.subscribe((params : Params) => {
    this.id = params["id"];
    })
    };

    参数订阅与参数快照的使用场景:
    当由HomeComponent进入到ProductComponent时,ProductComponent被激活,ngOnInit和constructor调用一次。
    例如:Product组件内部产品间的切换,由于id是通过参数在ngOnInit中获取的:
    ①如果使用snapshot的方式,组件未被销毁,noOnInit不再执行,那么显示在外部的id值将不再更新,出现数据错误;
    ②处理方式,采用subscribe的方式,当参数发生变化,即可更新信息;