3.认识Angular2组件之1

简述:组件(component)是构成Angular应用的基础和核心.可以这样说,组件用来包装特定的功能,应用程序的有序运行依赖于组件之间的协同工作.

1. 组件化标准:W3C为了统一组件化的标准方式,提出了Web Component的标准.通过标准化的非侵入方式封装组件,每个组件包含自己的HTML,CSS,JavaScript代码,

并且不会对页面上其他组件产生影响.Web Component是由一些新技术构成的,还提供了浏览器原声的UI组件标准,所以不需要引入任何外部的依赖.要使用一个已有的

Web Component组件,仅需如下添加一行导入声明,如:

<link rel="import" href="xxxxx.html" />

Web Component标准包括如下四个重要的概念:

1.自定义元素:这个特性允许创建自定义的HTML标记和元素,每个元素都有属于自己的脚本和样式.

2.模板:模板允许使用<template>标签去预先定义一些内容,但并不随页面加载而渲染,而是可以在运行时使用JavaScript去初始化它.

3.Shadow DOM:通过Shadow DOM可以在文档流中创建一些完全独立于其他元素的DOM子树,这个特性可以让开发者开发一个独立的组件,并且不会干扰到其他DOM元素.

4.HTML导入:一种在HTML文档中引入其他HTML文档的方法,用于导入Web Component的机制.

注意:目前仅有Chrome浏览器对该标准支持最高,其他主流浏览器并未完全实现Web Component标准.

有关Web Component标准的更多信息可待以后研究,这里并不深究.

2. Angular组件:在Angular中引入了视图包装(ViewEncapsulation)的概念,允许通过设置ViewEncapsulation.Native选项来使用原生的Shadow DOM.Angular还支持模板,

自定义标签,异步加载组件等.Angular组件是自描述的--可以和宿主元素交互,知道如何以及合适渲染自己,可配置注入服务,有明确的Input和Output定义.所有Angular的组件都可以

独立存在,都可以作为根组件被引导,也可以被路由加载,或者在其他组件中使用.不过一个组件不能单独被启用,它必须被包装到模块(NgModule)中.

组件是Angular应用的最小的逻辑单元,模块则是在组件之上的一层抽象.组件以及其他部件,如指令,管道,服务,路由等都可以被包含到一个模块中.外部引用通过引用这个模块来使用

一系列封装好的功能.

3. 创建组件的步骤:

1.从@angular/core中引入Component装饰器.

2.建立一个普通的类,并用@Component修饰它.

3.在@Component中,设置selector自定义标签和template模板.

4. 组件装饰器:@Component是TypeScript的语法,它是一个装饰器,任何一个Angular组件类都会用这个装饰器修饰,组件类最终编译成的JavaScript代码如下:

var ContactItemComponent=(function(){

function ContactItemComponent(){};

ContactItemComponent=__decorate([core_1.Component({

selector:'contact-item',

template:`

<div>

<p>xzm</p>

<p>13648301556</p>

</div>

`

})__metadata(`design:paramtypes`,[])

],ContactItemComponent);

return ContactItemComponent;

}());

其中,Angular的@Component会被转换成一个__decorate()方法,元数据的定义通过core_1.Component传入,将ComtactItemComponent这个类装饰器来,使得

ContactItemComponent具有装饰器里定义的元数据属性.

5. 组件元数据:

5.1 selector:是用于定义组件在HTML代码中匹配的标签,它将称为组件的命名标记.通常情况下都需要设置selector,特俗情况可以忽略,不指定时设置默认为匹配div元素.

selector的命名方式建议使用"烤肉串式"命名,即采用小写字母并以-分隔.

5.2 template是为组件指定一个内联模板.内联模板建议使用ES6的多行字符串``(两个反引号),这样可创建多行.

5.3 templateUrl:是为组件指定一个外部模板的URL地址.

5.4 styles:是为组件制定内联样式,如:

@Component({

styles:[`

li:last-child{

border-bottom:none;

}

`]

})

5.5 styleUrls:是为组件指定一系列用于该组件的外联样式表文件,如:

@Component({

styleUrls:['app/list/item.component.css']

})

注意:styles和styleUrls允许同时指定.同时指定,styles中的样式会被先解析,也就是styles的样式会被styleUrls的覆盖.

6. 模板:每个组件都必须设置一个模板,angular才能将组件内容渲染到DOM上,这个DOM元素就是宿主元素.组件可以与宿主元素交互,交互的形式如下:

1.显示数据

2.双向数据绑定

3.监听宿主元素事件以及调用组件方法.

6.1 显示数据:可以使用插值语法{{}}来显示组件的数据.

6.2 双向数据绑定,使用[(ngModule)]='property'的语法.

6.3监听宿主元素事件及调用组件方法:()是Angular提供的事件绑定语法糖,通过(eventName)的方式可以轻易地响应UI事件.

7. 组件和模块:Angular提供了@NgModule装饰器来创建模块,一个应用可以有多个模块,但只有一个根模块(RootModule),其他模块叫作特性模块(FeatureModule).根模块是启动应用

的入口模块,根模块必须通过bootstrap元数据来指定应用的根组件,然后通过bootstrapModule()方法来启动应用.

7.1 NgModule主要的元素居如下:

1.declarations:用于指定属于这个模块的是视图类(View Class),即指定那些部件组成了这个模块.Angular又组件,指令和管道三种视图类,这些视图类只能属于一个模块,必须

注意不能再次声明属于其他模块的类.

2.exports:导出视图类.当该模块被引入到外部模块时,这个属性指定了外部模块可以使用该模块的那些视图类,所以它的值类型跟declarations一致.

3.imports:引入该模块依赖的其他模块或路由,引入后模块里的组件模板才能引用外部对应的组件,指令和管道.

4.providers:指定模块依赖的服务,引入后该模块中的所有组件都可以使用这些服务.

7.2 导出视图类以及导入依赖模块:有时候模块中的组件,指令或管道,可能也会在其他模块中使用,这时可以使用exports元数据对外暴露这些组件,指令或管道.而相对应的,如果在一个模块

中想要使用其他模块对外暴露的组件,服务等,除了需要在模块的文件头使用import from导入模块,同时还要在NgModule的元数据import中为该模块制定要导入的依赖模块,这其中的

两个导入(import),前一个是TypeScript的模块导入,后一个是Angular框架的模块导入,希望这里不要混淆了.

7.3 服务引入:

引入服务有两种方式:

1.通过@NgModule的providers引入,通过它引入的服务,在模块的所有组件都可以使用.

2.通过@Component的providers引入,通过它引入的服务,在组件及其子组件中都可以共用这些引入的服务.

8. 组件交互:组件交互就是组件通过一定的方式来访问其他组件的属性或方法,从而实现数据的双向流通.组件交互有很多种方式,非父子关系的组件可通过服务来实现数据交互通信.

8.1 组件的输入输出属性:Angular除了提供@Input和@Output装饰器语法来处理组件数据的流入流出外,还提共了在组件的元数据中使用inputs,outputs来设置输入输出属性,设置的值必须

为字符串数组,元素的名称需要和成员变量相对应.

如:

@Component({

inputs:['contact'], //'contact' 匹配成员变量contact

outputs['routerNavigate']

})

8.2 父组件向子组件传递数据:父组件的数据是通过子组件的输入属性流入子组件,在子组件完成接收或者拦截,从而实现了数据由上而下的传递.

Angular会从根组件开始启动,并解析整棵组件树,数据以由上而下的方式流向下一级子组件.不过需要注意的是,目标组件的属性必须通过输入属性(@Input)明确的标记才能接收到来自

父组件的数据.

8.3 拦截输入数据:子组件可以拦截输入属性的数据并进行相应的处理.

有如下两种方式:

1.setter拦截输入属性:getter和setter通常一起使用,用来对属性进行相关约束.它们提供了对属性读写的封装,使代码结构更清晰,更具可扩展性.setter可对属性进行再封装

处理,对复杂的内部逻辑通过访问权限控制来隔绝外部调用,以避免外部的错误调用影响到内部的状态.同时也要把内部复杂的逻辑结构封装成高度抽象可被简单调用的属性,再通过

getter返回要设置的属性值,方便调用者使用.

setter拦截器示例,如:

@Component({

selector:'list-item',

template:`

<div>

<label class='contact-name'>{{contactObj.name}}</label>

</div>

`

})

exports class ListItemComponent impl OnInit{

_contact:object={};

@Input

set contactObj(contact:object){

this.)contact.name=(contact.name && contact,name.trim()) || '名字为空!';

}

get contactObj(){return this._contact;}

}

这里通过settet的方式设置一个contactObj属性对象,其作用是通过对@Input修饰符获取的数据contact(父组件传入的)进行二次处理,再通过getter返回这个contactObj对象.

2.ngOnChanges监听数据变化:ngOnChanges用于及时响应Angular在属性绑定中发生的数据变化,该方法接收一个对象参数,包含当前值和变化前的值.在ngOnInit之前,或者当数据

绑定的输入值发生变化时会触发.

ngOnChange方法接收一个参数,该参数类型是SimpleChanges类.它是Angular的一个基础类,用于处理数据的前后变化,其中包含两个重要成员,分别是previousValue和

currentValue,previousValue是获取变化前的数据,而currentValue是获取变化后的数据.

如:

//父组件代码 detail.component.ts

import { Component } from '@angular/core';

@Component({

selector:'detail',

template:`

<a class='edit' (clikc)='editContact()'>编辑</a>

<change-log [contact]='detail'></changelog>

`

})

export class DetailComponent implements OnInit{

detail:any={};

//...此处省略给 detail获取数据的代码

//完成联系人编辑修改

editContact(){

//...

this.detail=data;

}

}

//子组件代码 changelog.component.ts

import { Component,Input,Onchanges,SimpleChanges } from '@angular/core';

@Component({

selector:'change-log',

template:`

<h4>change log</h4>

<ul>

<li *ngFor="let change of changes">{{}change}</li>

</ul>

`

})

export clas ChangeLogComponent implements OnChanges{

@Input() contact:any={};

changes:string[]=[];

ngOnChange(changes:{[propKey:string]:SimpleChanges}){ // 有关{ [propKey:string]:SimpleChanges }代码的解释请看下面说明

let log:string[]=[];

for(let propName in changes){

let changeProp=changes[propName],

from =JSON.stringify(changeProp.previousValue),

to =JSON.stringify(changeProp.currentValue);

log.push(`${propName} changed from ${from} to ${to}`);

}

this.changes.push(log.join(','));

}

}

注:上面代码中又一行代码{[propKey:string]:SimpleChanges},这是作为一个TypeScript类来定义ngOnChanges方法的参数类型,那么该代码代表的是什么类呢?

直接说,其实就是一个双列集合,或者叫字典,如同Jave里面的Map集合,C#里面的Directory.而在TypeScript这里,字典类的定义比较麻烦,属于可索引类型接口,索引为

字符串类型,也就是[propKey:stirng],其后":SimpleChanges"定义了该字典类型中的元素类型必须为SimpleChanges类型,由于TypeScript的字典是通过接口的语

法方式实现的,所以这里最后又用了一对花括号把它包裹起来,这表明它是一个匿名的接口类型,也就是匿名的可索引接口类型.

8.4 子组件向父组件传递数据:使用事件传递是子组件向父组件传递数据最常用的方式.子组件需要实例化一个用来订阅和触发自定义事件的EventEmitter类,这个实例对象是一个由

装饰器@Output修饰的输出属性,当有用户操作行为发生时该事件会被触发,父组件则通过事件绑定的方式来订阅来自子组件触发的事件,即子组件触发具体的事件(自定义)会被其父

组件订阅到.

示例:

//父组件,收藏联系功能

import { Component } from '@angular/core';

@Component({

selector:'collection',

template:`

<contact-collect [contact]="detail" (onCollect)="collectTheContact($event)"></contact-collect>

`

})

export class CollectionComponent implements OnInit{

detail:any={};

collectTheContact(){

this.detail.collection== 0 ? this.detail.collection= 1 : this.detail.collection=0;

}

}

父组件CollectionComponent通过绑定自定义事件onCollect订阅来自子组件触发的事件.当有来自子组件对应的事件被触发,在父组件中能够监听到该事件.

注意:这里的具体业务逻辑功能是在父组件的collectTheCOntact()中实现的.

示例:

//子组件

import { Component ,EventEmitter,Input,Output } from '@angular/core';

@Component({

selector:'contact-collect',

template:`<i [ngClass]='{ collected:contact.collecttion }' (click)='collectTheContact()'>收藏</i>`

})

export class ContactCollectComponent{

@Input() contact:any={};

@Output() onCollect=new EventEmitter<boolean>();

collectTheContact(){

this.onCollect.emit();

}

}

通过输出属性@Output将数据流向父组件,在父组件完成事件的监听,以此来实现从子组件到父组件的数据交互.这个过程父组件数据流入子组件不太一样,其实一个父组件主动流入方式,

子组件的数据是在一些特定的时候或者条件下,由子组件主动发起,父组件被动触发(订阅到)事件来的得到子组件传递的数据.

8.5 通过局部变量实现数据交互:通过创建模板局部变量的方式,来实现父组件与子组件数据交互,即在父组件的模板中为子组件创建一个局部变量,那么这个父组件可以通过这个局部来获取

子组件公共成员变量和函数的权限.模板局部变量的作用域范围仅存在于该模板局部变量的子组件.

示例代码:

import { } from '@angular/core';

@Component({

selector:'collection',

template:`

<contact-collect (click)='collect.collectTheContact()' #collect></contact-collect>

`

})

export class CollectionComponent{}

在父组件模板中的子组件标签上绑定一个局部变量,以#号标记,以此来获取子组件类的实例对象.如上代码#collect就是绑定子组件类的实例对象.

模板局部变量的方式是在子组件方法中实现具体的业务逻辑,和父组件订阅子组件自定义事件的方式实现业务逻辑地方正好相反.

8.6 @ViewChild实现数据交互:使用模板变量只能在模板中使用,不能直接在父组件类里使用,这又一定的局限性.当父组件需要获取子组件中的变量,方法的读写权限时,可以通过@ViewChild

注入的方式来实现.

组件中元数据ViewChild的作用是声明对子组件元素的实例引用,它提供了一个参数来选择将要引用的组件元素,这个参数可以是一个类的实例,也可以是一个字符串.

1.参数为类实例,表示父组件将绑定一个指令或者子组件实例.

2.参数为字符串类型,表示将起到选择器的作用,即相当于在父组件中绑定一个模板局部变量,获取到子组件的一份实例对象的引用.

示例如:

import { } from '@angular/core';

@Component({

selector:'collection',

template:`

<contact-collect (click)='collectTheContact()'></contact-collect>

`

})

export class CollectionComponent{

@ViewChild(ContactCollectComponent) contactCollect:ContactCollectComponent; //子组件类变量

collectTheContact(){

this.contactCollect.collectTheContact();

}

}

9. 组件内容嵌入:内容嵌入(ng-content)是组件的一个高级功能特性,使用组件的内容嵌入特性能很好的扩从组件的功能,方便代码的复用.内容嵌入通常用来创建可复用的组件,典型的例子

是模态对话框或导航栏.

示例如:

import { Component } from '@angular/core';

@Component({

selector:'example-content',

template:`

<div>

<h4>ng-content 示例</h4>

<div >

<ng-content selector="header"></ng-content>

</div>

</div>

`

})

export class NgContentExampleComponent{}

在上面代码中,使用了<ng-content>标签,这个标签使用来渲染组件嵌入内容的.在<ng-content>中有个selector='header'属性,用于匹配内容,并填充到ng-content中.

有了以上带有内容嵌入的组件,那么就可以在如下的跟组件中使用该组件,例如:

import { Component } from '@angular/core';

@Component({

selector:'app',

template:`

<example-content>

<header>组件动态内容嵌入部分,可以替换该组件中的ng-content标签中的内容</header>

</example-content>

`

})

export class NgContengAppComponent{}

上面用到的selector是一个选择器,与CSS选择器类似,selector='header',表示匹配组件模板调用中<header>标签,当然还有其他的匹配方式,如下:

1.selector='.class-select' :通过标签上的某个CSS类来匹配.

2.selector='[name=footer]' :通过标签上的某个属性值来匹配.

10. 组件的生命周期:组件的生命周期由Angular内部管理,从组件的创建,渲染,到数据变动事件的触发,再到组件从DOM中移除,Angular都提供了一系列的钩子.

10.1 生命周期的钩子:开发者可以实现一个或者多个生命周期钩子(接口),从而在生命周期的各阶段做出适当的处理.这些钩子包含在@Angular/core中,

以下是组件常用的生命周期钩子:

1.ngOnChanges:它是用来响应组件输入值(通过@Input装饰去显式指定的变量)发生变化时触发的事件,接收一个SimpleChanges对象,包含当前值和变化前值,

该方法在ngOnInit之前.

2.ngOnInit:用于数据绑定输入性之后初始化组件,该钩子方法会在第一次ngOnChanges之后被调用.使用ngOnInit有以下两个重要原因:

a.组件构造后不久需要进行复杂的初始化.

b.需要在输入属性设置完成之后才构建组件.

3.ngDoCheck:用于变化监测,该钩子方法会在每次变化监测发生时被调用.每一个变化监测周期内,不管数据值是否发生变化,ngDoCheck都会被调用,该方法需要慎用,

如鼠标移动触发mousemove事件

4.ngAfterContentInit:在组件使用<ng-content>将外部内容嵌入到组件视图后就会调用它,它在第一次ngDoCheck执行后调用,且只执行一次.

5.ngAfterContentChecked:在组件使用了<ng-content>自定义内容的情况下,Angular在这些外部内容嵌入到组件视图后,或者每次变化监测的时候都会调用

ngAfterContentChecked.

6.ngAfterViewInit:会在Angular创建了组件的视图及其子组件视图之后被调用.

7.ngAfterViewChecked:在Angular创建了组件的视图及其子组件视图之后被调用一次,并且在每次子组件变化监测时也会被调用.

8.ngOnDestroy:在销毁指令/组件之前触发.那些不会被垃圾回收器自动回收的资源都应当在ngOnDestory中手动销毁.