组件基础
组件用来包装特定的功能,应用程序的有序运行依赖于组件之间的协同工作。
组件是angular应用的最小逻辑单元,模块则是在组件之上的一层抽象。所有angular组件都可以独立存在,也意味着任何组件都可以作为根组件被引导,也可以被路由加载,或在其他组件中使用。不过一个组件不能单独被启动,它必须被包装到模块里,然后通过Bootstrap
模块接口引导入口模块来启动angular应用。
创建组件的步骤
创建angular组件三步骤:
- 从
@angular/core
中引入Component
装饰器 - 建立一个普通的类,并用
@Component
修饰它 - 在
@Component
中,设置selector
自定义标签和template
模板
//contactItem.component.ts
import { Component } from '@angular/core';
@Component({
selector:'contact-item',
template:`
<div>
<p>张三</p>
<p>13800000</p>
</div>
`
})
export class ContactItemComponent {}
以上代码创建了一个最简单的组件。使用这个组件需要在HTML中添加<contact-item>
自定义标签,然后angular便会在此标签中插入ContactItemComponent
组件中指定的模板。
<div>
<contact-item></contact-item>
</div>
最终会被渲染成:
<div>
<contact-item>
<div>
<p>张三</p>
<p>13800000</p>
</div>
</contact-item>
</div>
组件基础构成
组件装饰器
@Component
是TypeScript的语法,它是一个装饰器,任何一个angular的组件类都会用这个装饰器修饰,如果移除了这个装饰器,它将不再是angular的组件。
组件元数据
在ContactItemComponent
这个组件中的@Component
装饰器部分,使用到了大部分组件需要的元数据:用于定义组件标签名的selector
;用于定义组件宿主元素模板的template
。
1.selector
selector
是用于定义组件在HTML代码中匹配的标签,它将成为组件的命名标记。通常情况下都需要设置selector
,特殊情况下也可以忽略,不指定时默认为匹配div
元素,但不建议这样做。selector
的命名方式建议用“烤肉串式”命名,即采用小写字母并以“-”分隔。
2.template
template
是为组件指定一个内联模板。使用ES6的多行字符串``语法能够创建多行模板。
3.templateUrl
templateUrl
是为组件指定一个外部模板的URL地址。
@Component({
templateUrl:'app/component/contact-item.html'
})
每个组件只能指定一个模板,可以使用template
或templateUrl
的引入方式。
4.styles
styles
是为组件指定内联样式。
@Component({
styles:[
`
li:last-child{
border-bottom:none;
}
`
]
})
5.styleUrls
styleUrls
是为组件指定一系列用于该组件的外联样式表文件。
@Component({
styleUrls:['app/list/item.component.css']
})
styles
和styleUrls
允许同时指定。如果同时指定,styles
中的样式会先被解析,然后才会解析styleUrls
中的样式,换句话说,styles
的样式会被styleUrls
的样式覆盖。另外,也可以在模板的DOM节点上直接写样式,它的优先级最高。
模板
每个组件都必须设置一个模板,angular才能将组件内容渲染到DOM上,这个DOM元素叫做宿主元素。组件可以与宿主元素交互,交互的形式包括:
- 显示数据:在模板中可以使用
{{}}
来显示组件的数据。
//contactItem.component.ts
import { Component } from '@angular/core';
@Component({
selector:'contact-item',
template:`
<div>
<p>{{name}}</p>
<p>{{phone}}</p>
</div>
`
})
export class ContactItemComponent{
name:string='张三';
phone:string='13800000';
}
- 双向数据绑定:
[(ngModel)]="property"
- 监听宿主元素事件以及调用组件方法:
(click)="addContact()"
组件与模块
组件通过与其他组件协作,完成一个完整的功能特性。这样的功能特性通常会封装到一个模块里。
模块是在组建之上的一层抽象,组件以及指令、管道、服务、路由等都能通过模块去组织。
模块的构成
angular提供了@NgModule
装饰器来创建模块,一个应用可以有多个模块,但有且只有一个根模块,其他模块叫做特性模块。根模块是启动应用的入口模块,根模块必须通过bootstrap
元数据来指定应用的根组件,然后通过bootstrapModule()
方法来启动应用。
//app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-brower';
import { ContactItemComponent } from './contactItem.component';
@NgModule({
imports:[BrowserModule],
declarations:[ContactItemComponent],
bootstrap:[ContactItemComponent]
})
export class AppModule {}
//app.ts
import { platformBrowseerDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
bootstrap
这个元数据用于指定应用的根组件,NgModule
主要的元数据如下。
- declarations:用于指定属于这个模块的视图类,即指定哪些部件组成了这个模块。angular有组件、指令和管道三种视图类,这些视图类只能属于一个模块,不能再次声明属于其他模块的类。
- exports:导出视图类。当该模块被引入到外部模块时,这个属性指定了外部模块可以使用该模块的哪些视图类,所以它的值类型跟
declarations
一致(组件、指令和管道)。 - imports:引入该模块依赖的其他模块或路由,引入后模块里的组件模板才能引用外部对应的组件、指令和管道。
- providers:指定模块依赖的服务,引入后该模块中的所有组件都可以使用这些服务。
视图类引入
NgModule
提供了declarations
这个元数据来将指令、组件或管道等视图类引入到当前模块。
在上面的AppModule
中,通过declarations
将ContactItemComponent
组件引入到了AppModule
模块中,使得所有属于AppModule
模块的其它组件的模板都可以使用<contact-item>
。
//app.module.ts
@NgModule({
declarations:[
AppComponent,
ListComponent,
ListItemComponent,
DetailComponent,
CollectionComponent,
EditComponent,
HeaderComponent,
FooterComponent,
PhonePipe,
BtnClickDirective
]
//...
})
export class AppModule {}
如果在ListComponent
组件的模板代码list.component.html
中,使用到了HeaderComponent
、FooterComponent
和ListItemComponent
这三个组件,这个时候必须在ListComponent
所属的模块(即AppModule
)中,通过declarations
引入这三个组件和ListItemComponent
组件后才能在模板中使用他们。
<!--list.component.html-->
<!--组件中制定了HeaderComponent,才能使用my-header标签-->
<my-header title="所有联系" [isShowCreateButton]="true"></my-header>
<ul class="list">
<li *ngFor="let contact of contacts">
<list-item [contact]="contact" (routerNavigate)="routerNavigate($event)"></list-item>
</li>
</ul>
<my-footer></my-footer>
ngFor
是angular的内置指令,在引入BrowserModule
的时候就已经引入了常用的内置指令。
导出视图类以及导入依赖模块
有时候模块中的组件、指令或管道,可能也会在其他模块中使用。可以使用export
元数据对外暴露这些组件、指令或管道。
//contact.module.ts
import { NgModule } from '@angular/core';
import { ContactItemComponent } from './contactItem.component';
@NgModule({
declarations:[ContactItemComponent],
exports:[ContactItemComponent] //导出组件
})
export class ContactModule {}
//message.module.ts
import { NgModule } from '@angular/core';
import { ContactModule } from './contact.module';
import { SomeOtherComponent } from './someother.component';
@NgModule({
//在SomeOtherComponent组件的模板中可以使用到<contact-item>组件了
declarations:[SomeOtherComponent],
imports:[ContactModule] //导入模块
})
export class MessageModule {}
服务引入
服务通常用于处理业务逻辑及相关的数据。引入服务有两种方式:一种是通过@NgModule
的providers
,另一种是通过@Component
的providers
。
//app.module.ts
import { ContactService } from './shared';
//...
@NgModule({
//...
providers:[ContactService],
bootstrap:[AppComponent]
})
export class AppModule {}
所有被包含在AppModule
中的组件,都可以使用到这些服务。同样,在组件中也可以用providers
来引入服务,该组件及其子组件都可以公用这些引入的服务。
组件交互
组件交互就是组件通过一定的方式来访问其他组件的属性或方法,从而实现数据双向流动。组件交互包括父子组件交互和一些非父子关系组件的交互。组件交互有很多方式,非父子关系的组件可以通过服务来实现数据交互通信。
组件的输入输出属性
angular提供了输入(@Input
)和输出(@Output
)语法来处理组件数据的流入流出。
//item.component.ts
export class ListItemComponent implements OnInit {
@Input() contact:any={};
@Output() routerNavigate=new EventEmitter<number>();
}
<!--list.component.html-->
<li *ngFor="let contact of contacts">
<list-item [contact]="contact" (routerNavigate)="routerNavigate($event)">
</list-item>
</li>
ListItemComponent
组件的作用是显示单个联系人的信息。由于联系人列表数据是在ListCommponent
组建中维护的,在显示单个联系人时,需要给ListItemComponent
传入单个联系人数据。另外在单击单个联系人时,需要跳转到此联系人的明细信息,需要子组件通知父组件进行跳转,[contact]
和(routerNavigate)
的输入输出变量,用于满足上述功能。
这里的输入输出是以当前组件角度去说的,contact
和routerNavigate
分别是ListItemComponent
组件的输入和输出属性。输出属性一般是以事件的形式,将数据通过EventEmitter
抛出去。
除了使用@Input
和@Output
修饰外,还可以在组件的元数据中使用inputs
、output
来设置输入输出属性,设置的值必须为字符串数组,元素的名称需要和成员变量相对应。
@Component({
inputs:['contact'],
outputs:['routerNavigate']
})
export class ListItemComponent implements OnInit {
contact:any={};
routerNavigate=new EventEmitter<number>();
}
父组件向子组件传递数据
父组件的数据通过子组件的输入属性流入子组件,在子组件完成接收或拦截,以此实现了数据由上而下的传递。
父到子组件间的数据传递
父组件LIstComponent
将获取到联系人的数据,通过属性绑定的方式流向子组件ListItemComponent
。
//list.component.ts
import { Component,OnInit } from '@angular/core';
@Component({
selector:'list',
template:`
<ul class="list">
<li *ngFor="let contact of contacts">
<list-item [contact]="contact"></list-item>
</li>
</ul>
`
})
export class ListComponent implements OnInit {
this.contacts=data;//data为获取到的联系人数据
}
//item.component.ts
import { Component,OnInit,Input } from '@angular/core';
@Component({
selector:'list-item',
template:`
<div class="contact-info">
<label class="contact-name">{{contact.name}}</label>
<span class="contact-tel">{{contact.telNum}}</span>
</div>
`
})
export class ListItemComponent implements OnInit {
@Input() contact:any={};
}
在之前的例子中,app.module.ts
中已经通过@NgModule
的元数据declarations
将子组件ListItemComponent
的实例引入到AppModule
中,使得所有属于AppModule
中的其他组件都可以使用ListItemComponent
组件,因此在父组件ListComponent
中可直接引用该子组件。将每个联系人对象通过属性绑定的方式绑定到属性contact
中来供子组件来引用,数据由上而下流入子组件,在子组件中通过@Input
装饰器进行数据的接收。
拦截输入属性
子组件可以拦截输入属性的数据并进行相应的处理。有两种拦截处理方式。
1.setter拦截输入属性
getter
和setter
通常配套使用,用来对属性进行相关约束。他们提供了一些属性读写的封装。setter可以对属性进行再封装处理,对复杂的内部逻辑通过访问权限控制来隔绝外部调用,以避免外部的错误调用影响到内部的状态。同时也把内部复杂的逻辑结构封装成高度抽象可被简单调用的属性,再通过getter
返回要设置的属性值。
通过输入输出属性可以实现父子组件的数据交互,还可以通过setter
来拦截来自父组件的数据源,并进行处理,使数据的输出更合理。
//ListComponent.ts使用前面的例子
//ListItem.component.ts
@Component({
selector:'list-item',
template:`
<div>
<label class="contact-name">{{contactObj.name}}</label>
<span class="contact-tel">{{contactObj.telNum}}</span>
</div>
`
})
export class ListItemComponent implements OnInit{
_contact:object={};
@Input()
set contactObj(contact:object){
this._contact.name=(contact.name&&contact.name.trim())||'no name seet';
this._contact.telNum=contact.telNum||'000-000';
}
get contactObj(){ return this._contact; }
}
这里通过setter
的方式设置一个contactObj
属性对象,其作用是对通过@Input
修饰符获取的数据contact
进行二次处理,再通过getter
返回这个contactObj
对象。这样处理的作用是使得联系人不会出现null
或undefined
的情况。getter
和setter
其实是在组件类的原型对象上设置了一个contactObj
属性。
Object.defineProperty(ListItemComponent.prototype,"contactObj",{
//...
})
2.ngOnChanges监听数据变化
ngOnChanges
用于及时响应angular在属性绑定中发生的数据变化,该方法接收一个对象参数,包含当前值和变化前的值。在ngOnInit
之前,或者当数据绑定的输入属性的值发生变化时会触发。ngOnChanges
是组件的生命周期钩子之一。
//父组件detail.component.ts
import { Component } from '@angular/core';
@Component({
selector:'detail',
template:`
<a class="edit" (click)="editContact()">编辑</a>
<change-log [contact]="detail"></change-log>
`
})
export class DetailComponent implements OnInit{
detail:any={};
//完成联系人编辑修改
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 class ChangeLogComponent implements OnChanges{
@Input() contact:any={};
changes:string[]=[];
ngOnChanges(changes:{[propKey:string]:SimpleChanges}){
let log:string[]=[];
for(let propName in changes){
let changedProp=changes[propName],
from=JSON.stringify(changedProp.previousValue),
to=JSON.stringify(changedProp.currentValue);
log.push(`${propName} changed from ${from} to ${to}`);
}
this.changes.push(log.join(', '));
}
}
SimpleChanges
类是angular的一个基础类,用于处理数据的前后变化,其包含两个重要成员变量,分别是previousValue
和currentValue
,previousValue
是获取变化前的数据,currentValue
是获取变化后的数据。
ngOnChanges
当且仅当组件输入数据变化时被调用,“输入数据”指的是通过@Input
装饰器显式指定的那些数据。
子组件向父组件传递数据
使用事件传递是子组件向父组件传递数据的最常用方式。子组件需要实例化一个用来订阅和触发自定义事件的EventEmitter
类,这个实例对象是一个由装饰器@Output
修饰的输出属性,当有用户操作行为发生时该事件会被触发,父组件则通过事件绑定的方式来订阅来自子组件触发的事件。
//collection.component.ts 父组件
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
订阅来自子组件触发的事件。当有来自子组件对应的事件被触发,在父组件中能够监听到该事件。
//contactCollect.component.ts 子组件
import { Component,EventEmitter,Input,Output } from '@angular/core';
@Component({
selector:'contact-collect',
template:`
<i [ngClass]="{collected:contact.collection}" (click)="collectTheContact()"收藏</i>
`
})
export class ContactCollectComponent {
@Input() contact:any={};
@Output() onCollect=new EventEmitter<boolean>();
collectTheContact(){
this.onCollect.emit();
}
}
其他组件交互方式
父子组件间数据传递的实现还有两种方式:
- 父组件通过局部变量获取子组件引用
- 父组件使用
@ViewChild
获取子组件的引用
通过局部变量实现数据交互
模板局部变量可以获取子组件的实例引用。通过创建模板局部变量的方式,来实现父组件与子组件数据交互,即在父组件的模板中为子组件创建一个局部变量,这个父组件可以通过这个局部变量来获得读取子组件公共成员变量和函数的权限。模板局部变量的作用域范围仅存在于定义该模板局部变量的子组件。
//父组件collection.component.ts
import { Component } from '@angular/core';
@Component({
selector:'collection',
template:`
<contact-collect (click)="collectTheContact()" #collect></contact-collect>
`
})
export class CollectionComponent {}
//子组件contactcollect.component.ts
import { Component } from '@angular/core';
@Component({
selector:'contact-collect',
template:`
<i [ngClass]="{collect:detail.collection}">收藏</i>
`
})
export class ContactCollectComponent{
detail:any={};
collectTheContact() {
this.deetail.collection==0?this.detail.collection=1:this.detail.collection=0;
}
}
这里通过在标签元素<contact-collect>
中放置一个局部变量collect
,用来获取子组件的实例引用,来访问子组件中的成员变量和方法。
使用@ViewChild实现数据交互
当父组件需要获取到子组件中变量或方法的读写权限时,可以通过@ViewChild
注入的方式来实现。组件中元数据ViewChild
的作用是声明对子组件元素的实例引用,它提供了一个参数来选择将要引用的组件元素,这个参数可以是一个类的实例,也可以是一个字符串。
- 参数为类实例,表示父组件将绑定一个指令或子组件实例
- 参数为字符串类型,表示将起到选择器的作用,即相当于在父组件中绑定一个模板局部变量,获取到子组件的一份实例对象的引用
组件中元数据ViewChild
的参数为字符串的实现方式和绑定模板局部变量是一样的。
import { Component,AfterViewInit,ViewChild } from '@angular/core';
@Component({
selector:'collection',
template:`
<contact-collect (click)="collectTheContact()"></contact-collect>
`
})
export class CollectionComponent{
@ViewChild(ContactCollectComponent) contactCollect:CotactCollectComponent;
ngAfterViewInit(){
//...
}
collectTheContact(){
this.contactCollect.collectTheContact();
}
}
组件内容嵌入
内容嵌入通常用来创建可复用组件。
import { Component } from '@angular/core';
@Component({
selector:'example-content',
template:`
<div>
<h4>ng-content 示例</h4>
<div style="background-color:gray;padding:5px;margin:2px">
<ng-content select="header"></ng-content>
</div>
</div>
`
})
class NgContentExampleComponent {}
接着再定义一个根组件来使用它。
import { Component } from 'angular/core';
@Component({
selector:'app',
template:`
<example-content>
<header>Header content</header>
<!--将自定义的内容放到 example-content标签之间-->
</example-content>
`
})
export class NgContentAppComponent {}
最后组件的DOM树会被渲染成:
<app>
<example-content>
<div>
<h4>ng-content示例</h4>
<div style="background-color:gray;padding:5px;margin:2px">
<ng-content select="header"></ng-content>
</div>
</div>
<example-content>
</app>
<example-content>
标签之间的内容,也就是<header>Header content</header>
,被填充到ng-content
,而NgContentExampleComponent
组件模板中的其他元素没有受到影响。select="header"
表示匹配<example-content>
标签之间的第一个header
标签,并将其填充到相应的ng-content
中。
还可以同时使用多个嵌入内容。
import { Component } from '@angular/core';
@Component({
selector:'example-content',
template:`
<div>
<h4>component with ng-content</h4>
<div style="background-color:green;padding:5px;margin:2px">
<ng-content select="header"></ng-content>
</div>
<div style="background-color:gray;padding:5px;margin:2px">
<ng-content select=".class-select"></ng-content>
</div>
<div style="background-color:blue;padding:5px;margin:2px">
<ng-content select="[name=footer]"></ng-content>
</div>
</div>
`
})
class NgContentExampleComponent {}
//NgContemtAppComponent.ts
import { Component } from 'angular/core';
@Component({
selector:'app',
template:`
<example-content>
<header>Header content</header>
<div class="class-select">div with .class-content</div>
<div name="footer">Footer content</div>
</example-content>
`
})
export class NgContentAppComponent {}
组件生命周期
组件的生命周期由angular内部管理,从组件的创建、创建,到数据变动事件的触发,再到组件从DOM中移除,angular都提供了一系列钩子。这些钩子可以在这些事件触发时,执行相应的回调函数。
生命周期钩子
钩子的接口包含@angular/core
中。每个接口都对应一个名为ng+接口名
的方法。
class ExampleInitHook implements OnInit {
constructor() {}
ngOnInit() {
console.log('OnInit');
}
}
以下是组件常用的生命周期钩子方法,angular会按以下的顺序依次调用钩子方法:
- ngOnChanges
- ngOnInit
- ngDoCheck
- ngAfterContentInit
- ngAfterContentChecked
- ngAfterViewInit
- ngAfterViewChecked
- ngOnDestroy
除此之外,有的组件还提供了自己特有的生命周期钩子,例如路由有routerOnActivate
钩子。
ngOnChanges
ngOnChanges
钩子用来响应组件输入值发生变化时触发的事件。该方法接收一个SimpleChanges
对象,包含当前值和变化前的值。该方法在ngOnInit
之前,或者当数据绑定输入属性的值发生变化时触发。
只要在组件里定义了ngOnChanges
方法,在输入数据发生变化时该方法就会被自动调用。这里的“输入数据”指的是通过@Input
装饰器显示指定的变量。
ngOnInit
ngOnInit
钩子用于数据绑定输入属性之后初始化组件。该钩子方法会在第一次ngOnChanges
之后被调用。
使用ngOnInit
有两个重要原因:
- 组件构造后不久就要进行复杂的初始化
- 需要在输入属性设置完成之后才构建组件
在组件中,经常会使用ngOnInit
获取数据。
ngDoCheck
用于变化监测,该钩子方法会在每次变化监测发生时被调用。
每一个变化监测周期内,不管数据值是否发生了变化,ngDoCheck
都会被调用。但要慎用,例如鼠标移动时会触发mousemove
事件,此时变化监测会被频繁触发,随之ngDoCheck
也会被频繁调用。因此,ngDoCheck
方法中不能写复杂的代码,否则性能会受影响。
绝大多数情况下,ngDoCheck
和ngOnChanges
不应该一起使用。ngOnChanges
能做的事,ngDoCheck
也能做到,而ngDoCheck
监测的力度更小,可完成更灵活的变化监测逻辑。
ngAfterContentInit
在组件中使用<ng-content>
将外部内容嵌入到组件视图后就会调用ngAfterContentInit
,它在第一次ngDoCheck
执行后调用,且只执行一次。
ngAfterContentChecked
在组件使用了<ng-content>
自定义内容的情况下,angular在这些外部内容嵌入到组件视图后,或每次变化监测的时候都会调用ngAfterContentChecked
。
ngAfterViewInit
ngAfterViewInit
会在angular创建了组件的视图及其子视图之后被调用。
ngAfterViewChecked
ngAfterViewChecked
在angular创建了组件的视图及其子组件视图之后被调用一次,并且在每次子组件变化监测时也会被调用。
ngOnDestroy
ngOnDestroy
在销毁指令/组件之前触发。那些不会被垃圾回收器自动回收的资源(如已订阅的观察者事件、绑定过的DOM事件、通过setTimeout
或setInterval
设置过的计时器等)都应当在ngOnDestroy
中手动销毁掉,从而避免发生内存泄漏等问题。