@adonia
2016-11-12T12:44:46.000000Z
字数 13982
阅读 533
angular2
在上一章节的示例应用中,体现了Angular2的一大特性---angular2的应用(Application
)是由一个或者多个组件(Component
)组成的,组件之间以树状结构呈现。而应用本身,就是组件树中的最顶端节点。在浏览器中加载应用的过程,其实就是引导(bootstrap
)组件的过程。
Angular2中的组件是可以组装的,也就是说,任何一个大的应用,都可以通过一个个小的组件拼装而成,每个组件各司其职。所有的组件是通过父子树状结构组装的,在加载应用时,会引导顶级的组件,然后逐层遍历加载所有的组件。
下面,通过一个具体的示例来说明。
大家都有过在淘宝上买东西的经历,例如下面的一个电商商品浏览页面:
我们可以根据上图呈现的内容,将其拆分成三大组件:
再将商品列表细分,可以看成是一系列的商品组件(Product Rows Component)组装而成的,如下:
如果,再将商品细分,又可以分成商品图片(Product Image Conponent),类目信息(Product Department component),以及商品价格(Price Display component)。
最终,整个商品应用的结构如下:
最顶层是应用本身---Inventory Management App,在应用的子节点中,包括了导航条,商品列表,以及面包屑。商品列表又是由一个个的商品组成的。而商品自身,包含了商品图片,物理类目信息,以及价格信息。
整个应用的蓝图如下:
下面就来一步步地介绍如何构建这个应用。
inventory_app
。将之前示例程序中的package.json
,tsconfig.json
拷贝至inventory_app
下,修改package.json
中的name
属性为inventory-app
。inventory_app
目录下,执行cnpm install
。Product
模型Angular2并没有限制特定的数据模型集合,而是可以灵活支持各种不同的模型和数据结构;而且,这意味着,如何去模拟实现一件事物,完全是在用户的掌控之中。
在inventory_app
目录下,创建app.ts
,在其中定义Product
类,代码如下:
class Product {
constructor(
public sku: string,
public name: string,
public imageuri: string,
public sortName: string[],
public price: number
){
};
}
这里呢,创建了Product
类,定义了其构造方法,即类中的constructor
方法。构造方法中的参数---public sku: string
,传递着两个含义:
Product
类示例,都有一个公共属性sku
,在类中的其他方法中,可以通过this.sku
访问该属性。sku
变量的类型是string
。Product
是用来将页面上的商品信息抽象化的,像这种,就对应着MVC
框架中的Model
。
组件由三部分组成:
前面提到过,Application
是由一个或多个Component
组成的,Application
自身就是顶级的组件。
Inventory App
的大致结构如下:
@Component({
selector: 'inventory-app',
template: `
<div>
(Products will go here soon~)
</div>
`
})
class InventoryApp {
}
bootstrap(InventoryApp);
Note:
@Component
就是装饰器(Decorator),装饰器会向其修饰的类中增加原数据(metadata)。@Component
修饰的类,即为跟其紧挨着,使用了当前注解的类,例如此处的InventoryApp
。此处,
@Component
装饰器中包含了两个属性,其中selector
定义了与之关联的DOM标签(即,在HTML中可以通过<inventory-app></inventory-app>
来引用此组件);template
则定义了视图(View
)。
Controller
则是由组件类定义的,例如此处的InventoryApp
,其中可以包括当前组件处理事件的一些方法,例如click
事件等。
至于bootstrap
方法,则是起到引导整个Application
的作用,所以,此处bootstrap
的入参是顶级的Component
。
下面详细介绍下组件中,常用的一些属性。
selector
的作用是,在加载HTML时,指明自定义的组件如何被识别,作用原理跟CSS
和JQuery
中的selector
类似。selector
指明HTML中的哪个标签,会与当前的组件相匹配。
当我们说,组件选择器inventory-app
时,也就意味着,在HMTL中,与之匹配的是inventory-app
标签。而且,无论在何处引用这个标签时,都会具备与之对应的组件的功能。例如,在HTML中定义如下标签:
<inventory-app></inventory-app>
在HTML加载时,就会初始化InventoryApp
组件来实现对应的功能。
同样的,也可以通过在常规的div
标签中,将组件当作一个属性的形式来使用,如下:
<div inventory-app></div>
@Component
的template
定义了组件拥有的HTML模板,例如:
@Component({
selector: 'inventory-app',
template: `
<div>
(Products will go here soon~)
</div>
`
})
此处,使用了TypeScript
的中模板语法---多行字符串语法。这里只是个示例,定义一个包含了简单文本的div
标签。
这样,在HTML中加载inventory-app
标签时,该标签的视图就会被template
中定义的内容所代替。
Product
inventory app
是用来展示商品信息的,下面让我们在创建个商品,代码如下:
let product = new Product('NiceHat', '一款很不错的黑色帽子', '/resources/images/black-hat.jpg', ['男士', '装饰品', '帽子'], 29.99);
此处,使用new
关键字来创建类实例,调用Product
类的构造方法(constructor
),传入对应的5个参数。
为了能够展示商品信息,我们需要把创建的Product实例维护起来,作为InventoryApp
组件的一个变量。如下:
class InventoryApp {
product: Product;
constructor() {
this.product = new Product('NiceHat', '一款很不错的黑色帽子', '/resources/images/black-hat.jpg', ['男士', '装饰品', '帽子'], 29.99);
}
}
bootstrap(InventoryApp);
此处,我们做了三件事情:
InventoryApp
新增一个构造方法,每次在创建一个InventoryApp
组件时,都会调用构造方法来实例化。所以,我们可以在构造方法中完成一些初始化的动作。InventoryApp
新增一个变量---product
,并指明其类型为Product
类。也就意味着,组件拥有一个Product
类型的属性。InventoryApp
的product
属性做初始化,也就是说,我们在类中通过this.product
就可以访问到初始化完成的变量了。product
我们已经在组件类中增加了一个公有属性,那么现在就可以在视图中使用该公有属性了。代码如下:
@Component({
selector: 'inventory-app',
template: `
<div>
<h2>{{product.name}}</h2>
<span>{{product.sku}}</span>
</div>
`
})
Note:
- 此处的
{{...}}
语法为模板绑定(template binding
)。模板绑定的作用是通知视图,在template
中,会使用{{}}
大括号包含的表达式的值,作为展示的内容。
上述示例中,包含了两处模板绑定语法:
{{product.name}}
{{product.sku}}
表达式中的product
关键字,即为组件InventoryApp
类实例的product
属性。注意,模板绑定中的代码是表达式,也就是说,我们可以在其中做一些运算,或者调用函数,例如:
在inventory app
工程的根目录下,执行命令:
Note:
第一步是下载依赖
第二步是编译
app.ts
,执行成功的话,在工程的根目录下,会出现app.js
和app.js.map
第三步是运行服务
执行成功的话,在浏览器中访问localhost:8080
,应该可以得到如下的页面:
具体代码,可参考github中的实现。
product
在inventory
应用中,我们希望展示一系列的产品,而不仅仅只是一个。因此,在InventoryApp
类中,将原有的product
属性,变更为products
,类型为Product[]
,表示是元素为Product
的数组。也可以定义为Array[Product]
。如下:
class InventoryApp {
products: Product[];
constructor() {
this.products = [
new Product('MyShoes', '黑色运动鞋', '/resources/images/black-shoes.jpg', ['男士', '鞋子', '运动鞋'], 109.99),
new Product('NeatoJacket', '蓝色夹克衫', '/resources/images/blue-jacket.jpg', ['女士', '上衣', '夹克&马甲'], 238.99),
new Product('NiceHat', '一款很不错的黑色帽子', '/resources/images/black-hat.jpg', ['男士', '装饰品', '帽子'], 29.99)
];
}
}
我们在其构造方法中初始化了products
属性,使其包含了三个商品。
我们希望能够在应用中支持用户交互,例如,用户在浏览商品时,可以点击某一个商品,查看商品详情,或者加入购物车。
为此,需要在InventoryApp
类中增加一个方法---productWasSelected
,用来处理当商品选中的事件。如下:
productWasSelected(product: Product): void {
console.log("Select product: " + product);
}
稍后介绍如何触发该事件方法。
我们已经在InventoryApp
中维护了一组商品了,现在需要考虑如何将其展示出来。将新定义一个组件---product-list
,用于展示商品列表,稍后再介绍其对应的实现类ProductList
。先来看下如何使用新组件:
class ProductList {
}
@Component({
selector: 'inventory-app',
directives: [ProductList],
template: `
<div>
<product-list
[productList]="products"
(onProductSelected)="productWasSelected($event)">
</product-list>
</div>
`
})
class InventoryApp {
...
}
Tips: 此处需要先声明
ProductList
类,否则第6行会报错。ProductList
类的具体实现,后续再介绍。
这里,使用了一些新的语法(syntax
)和选项(options
),下面一一介绍:
directives
声明了在当前组件中,需要引用的其他组件列表。是由被引用的组件的实现类名称组成的数组。
在angular2中,使用其他的组件,必须通过directives
的方式声明引用,否则组件将不被解析。也就是说,在angular2中,组件并不是全局的,不像angular1中的指令。
在使用的新组件product-list
中,使用了一组新特性--- input
和 output
,如下:
<product-list
[productList]="products" <!-- input -->
(onProductSelected)="productWasSelected($event)"> <!-- output -->
</product-list>
其中,中括号[]
所声明的是input
,而圆括号()
声明的则是ouput
。
input
的作用是将外部数据引入到组件中,而output
则是将组件中的事件传递出去。
input
中,等号左边,方括号所包含的---[productList]
,是指在组件的实现类ProductList
中,将要使用productList
所引入的外部输入。而在等号右边,引号中所包含的---products
,是指将表达式products
的值传递给新组件product-list
,此处代表的是InventoryApp
中的products
数组属性的值。
output
中,等号左边,圆括号所包含的---(onProductSelected)
,是指所要监听的组件ProductList
的onProductSelected
事件。而等号右边---productWasSelected()
是当onProductSelected
事件发生变化时,当前组件所要处理的方法。$event
是一个特殊的字段,表示output
中所传递的事件。
简而言之,就是通过input
,将InventoryApp
中维护的products
传递给ProductList
组件,用于展示。而在ProductList
中选中某一个商品时,会触发其onProductSelected
事件,该事件透过output
传递给了InventoryApp
,经由productWasSelected
方法处理事件。
下面来介绍下如何实现ProductList
组件。
ProductList
接收InventoryApp
传递的products
数组,其作用是为了展示所有的商品,并且可以允许用户点击某一个商品,并实时跟踪用户当前选择的商品。
我们将分三步来实现ProductList
组件:
@Component
注解ProductList
的Controller方法定义其视图---template
@Component
注解
先看下ProductList
的@Component
注解,如下:
class ProductRow {
}
@Component({
selector: 'product-list',
directives: [ProductRow],
inputs: ['productList'],
outputs: ['onProductSelected']
})
class ProductList {
...
}
Note:
`selector
属性,指明了在将要使用product-list
选择器来引用该组件。
directives
属性,指明将要引用外部组件---ProductRow
,此处同样先定义其类,具体实现后续再介绍。
inputs
属性,指明了从外部接收数据的入口。
outputs
属性,指明了将内部监听事件传递出去的出口。
下面来详细介绍下inputs
和outputs
。
inputs
当声明一个input属性时,也就是意味着当前定义的类中,会有一个属性实例来接收该input所传递的值。例如:
@Component({
selector: 'my-component',
inputs: ['name', 'age']
})
class MyComponent {
name: string;
age: number;
}
inputs
属性中的name
和age
分别对应着实现类中的name
和age
属性实例。那如果需要向组件中传递值呢,就可以通过如下方式:
<my-component [name]="myName" [age]="myAge"></my-component>
这样,MyComponent
类实例中,name
的值就是myName
,age
的值就是myAge
了。
并不是inputs
中的属性值,一定要和类中的属性变量一致的,例如:
<my-component [shortName]="myName" [oldAge]="myAge"></my-component>
为了在组件中对应起来,只需要把组件定义中的inputs
改为:
@Component({
selector: 'my-component',
inputs: ['name: shortName', 'age: oldAge']
})
class MyComponent {
name: string;
age: number;
}
也就是说,inputs
中的定义格式为: 组件属性: 使用字段
。(组件属性
是指组件类中的属性变量,使用字段
是指在使用组件时,所使用的声明字段)
outputs
如果想在组件中,向外界传递数据,则就需要output
绑定(binding
)。这个就像发布-订阅
系统一般,组件A想要将组件内部的数据传递出去,那么A就将数据以事件流的方式发布
出去;组件B想要得到事件流中的数据,则需要监听组件A的事件变化,也就是订阅
这个事件。
下面通过一个常见的例子说明下整个过程:点击按钮。
按钮的点击,是在浏览器中最常见的一种触发事件。点击按钮后,通常会有相应的响应信息,例如“弹出框”,“提交表单”等。
而对于一个组件来说,处理组件中按钮的点击事件,就需要在组件的Controller
中定义一个方法,并将按钮的点击事件产生的output
绑定至该方法上。对应的语法格式为:
(output)="action"
Tips:
output
对应着触发的事件,例如click
;action
对应着Controller
中处理事件的方法。
看下具体的示例:
import {Component} from "angular2/core";
import {bootstrap} from "angular2/platform/browser";
@Component({
selector: "counter",
template: `
当前的值: {{value}}
<button (click)="increase()">增加</button>
<button (click)="decrease()">减少</button>
`
})
class Counter {
value: number;
constructor() {
this.value = 10;
}
increase(): void {
this.value += 1;
}
decrease(): void {
this.value -= 1;
}
}
bootstrap(Counter);
在当前的示例中,有两个按钮,在点击增加
按钮时,会使当前的数值加1;在点击减少
按钮时,会使当前的数值减1。当前的示例中,output
的产生是按钮的click
事件,是属于按钮的内置
(build-in)事件。类似的内置事件还有像db-click
,mouseover
等。
那么如何来自定义监听事件呢?
为了要实现自定义事件,需要引入angular2中的一个组件---EventEmitter
。EventEmitter
是angular2中用于实现监听器模式
(Observer Pattern)的组件,在EventEmitter
中会维护一个或者多个订阅者(subscriber
),然后将事件发布(publish
)给订阅者。
那如何来利用EventEmitter
来实现自定义事件呢?主要包括以下三个方面:
@Component
中声明outputs
属性;EventEmitter
的实例作为outputs
中的一个属性;EventEmitter
实例向外发布事件。下面通过一个具体的例子---信号器发射,来看下具体的实现流程。
首先,实现信号器发射的组件:
import {EventEmitter} from "angular2/core";
import {Component} from "angular2/core";
import {bootstrap} from "angular2/platform/browser";
@Component({
selector: 'single-component',
outputs: ['putRingOnit'],
template: `
<button (click)="liked()">like it?</button>
`
})
class SingleComponent {
putRingOnit: EventEmitter<string>;
constructor() {
this.putRingOnit = new EventEmitter();
}
liked(): void {
this.putRingOnit.emit('oh oh oh...');
}
}
说明:
在
@Component
中,声明了outputs
属性,其中包含了putRingOnit
;在
SingleComponent
类中,将putRingOnit
声明为一个EventEmitter
的实例,此处使用了范型的特性。EventEmitter<string>
意味着,该实例能够发布的消息类型是string
。在类中定义了一个方法---
like()
,会使用EventEmitter
的emit
方法发布消息。而方法是在点击按钮的时候触发的。
现在,我们已经定义了一个组件,其中包含了EventEmitter
的实例,可以向外发布消息了。现在还需要再定义一个组件,来订阅该消息;在按钮触发时,能够接收到消息并处理。
EventEmitter
有一个subscribe()
方法,用来订阅消息的,例如:
let ee = new EventEmitter();
ee.subscribe((name:string) => console.log(`Hello${name}`));
ee.emit("Nate");
运行的结果就是Hello Nate
。
而在angular2中,我们可以不用直接通过subscribe
的方式来订阅消息。在我们将EventEmitter
的实例加入至outputs
属性中时,angular2就已经做了订阅的动作了。也就是说,在新的组件中,只要使用当前组件的outputs
就可以达到订阅该消息的目的了。
订阅消息的组件如下:
@Component({
selector: 'club',
directives: [SingleComponent],
template: `
<div>
<single-component (putRingOnit)="ringWasPlaced($event)"></single-component>
</div>
`
})
class ClubComponent {
ringWasPlaced(msg: string): void {
console.log(`Puts your hands up: ${msg}`);
alert(`Puts your hands up: ${msg}`);
}
}
bootstrap(ClubComponent);
还记得之前提到的outputs
的语法格式吗?---(output)="action"
。
此处的output
为putRingOnit
,来源于组件SingleComponent
;action
呢,是订阅组件ClubComponent
的ringWasPlaced
方法。
看下ringWasPlaced
方法的实现,参数类型是string
,对应着putRingOnit
声明的EventEmitter
实例的参数类型。
在点击like it?
按钮时,将会触发liked()
方法,发布一条消息,内容为oh oh oh...
;该消息被ClubComponent
组件订阅,并被ringWasPlaced()
方法消费。
具体实现参考代码示例。
说明: 以下介绍的组件实现内容较多,可对照参考完整的示例代码。
回到Inventory
应用的例子上。
首先,实现ProductList
的Controller。按照之前的介绍,Controller类中应该维护着三个变量实例:
productList
这个input
中接收数据;output
---onProductSelected
,用来发布商品选中时的事件消息;ProductList
的Controller代码如下:
class ProductList {
/**
* @input 外部传入的商品列表
*/
productList: Product[];
/**
* @output 向外发布当前选中的商品信息
*/
onProductSelected: EventEmitter<Product>;
/**
* @property 跟踪当前选中的商品
*/
currentProduct: Product;
constructor() {
this.onProductSelected = new EventEmitter();
}
}
说明:
productList
为input
,接收主程序传入的products
数组;
onProductSelected
为output
,将选中的商品发布出去;
currentProduct
维护当前选中的商品。
来看下ProductList
的View模板:
@Component({
selector: 'product-list',
directives: [ProductRow],
inputs: ['productList'],
outputs: ['onProductSelected'],
template: `
<div>
<product-row *ngFor="#product of productList"
[product]="product"
(click)="clicked(product)"
[class.active]="isSelected(product)">
</product-row>
</div>
`
})
其中,使用组件ProductRow
来展示每一个商品的信息,稍后会详细介绍该组件的定义,展示效果如下:
*ngFor="#product of productList"
,这里使用ngFor
语法来遍历整个商品数组,将其中的每个商品传递给ProductRow
组件的input
。此处使用了ProductRow
的内置事件click
,在组件的任何区域点击时,会触发ProductList
组件的clicked()
方法,方法的定义如下:
/**
* @function 点击商品时,维护当前选中的商品,同时,将选中的商品发布出去
*/
clicked(product: Product): void {
this.currentProduct = product;
this.onProductSelected.emit(product);
}
这里呢,每次点击商品,都会切换维护的商品信息,并且,将当前商品的信息通过onProductSelected
发布出去。让我们来回顾下InventoryApp
中的实现:
@Component({
selector: 'inventory-app',
directives: [ProductList],
template: `
<div>
<product-list
[productList]="products"
(onProductSelected)="productWasSelected($event)">
</product-list>
</div>
`
})
class InventoryApp {
products: Product[];
...
productWasSelected(product: Product): void {
console.log("Select product: " + product);
}
}
在InventoryApp
中,在触发onProductSelected
事件时,会调用组件的productWasSelected()
方法。前后结合起来,在点击每个ProductRow
时,会将商品最终传递给InventoryApp
组件,完成对商品的逻辑处理。
再回到ProductList
的View模板上,注意下[class.active]="isSelected(product)"
。该段代码是angular2中的语法,意思是在组件ProductList
的isSelected()
方法返回true
时,会在组件ProductRow
的View模板中,增加CSS样式active
。
isSelected()
方法可参考示例代码,active
样式在index.html
中定义。
ProductRow
组件再来看下ProductRow
的实现效果:
ProductRow
同样可以拆分成三个小组件:
ProductImage
组件,用于展示商品图片;ProductSort
组件,用于展示商品的物理类目;PriceDisplay
组件,用于展示商品的价格信息。ProductRow
组件的实现如下:
@Component({
selector: "product-row",
inputs: ["product"],
host: {'class': 'row', 'style': 'margin: 10px;'},
directives: [ProductImage, ProductSort, PriceDisplay],
template: `
<div class="col-sm-2">
<product-image [product]="product"></product-image>
</div>
<div class="col-sm-5">
<h3>{{product.name}}</h3>
<div>SKU #{{product.sku}}</div>
<div>
<product-sort [product]="product"></product-sort>
</div>
</div>
<div class="col-sm-1">
<price-display [price]="product.price"></price-display>
</div>
`
})
class ProductRow {
product: Product;
}
说明:
类中的
product
变量用于接收ProductList
组件传入的商品;注意
ProductRow
类中不需要构造方法,因为product
是通过input
传入的,angualr2在初始化组件时,会自动初始化ProductRow
类。在
@Component
中的host
属性定义了组件的全局样式;
directives
中定义了需要引入的三个组件---ProductImage
,ProductSort
,PriceDisplay
;
ProductRow
通过input
的方式将相关信息传递给三个组件,不再赘述。
ProductImage
组件ProductImage
组件的作用,就是用来展示商品的图片信息的,代码如下:
@Component({
selector: "product-image",
inputs: ["product"],
template: `
<img class="img-thumbnail" [src]="product.imageuri">
`
})
class ProductImage {
product: Product;
}
说明:
- 跟
ProductRow
一样,通过input
的方式接收变量,无需定义构造方法;
注意,img
标签中,并不是使用的常规的src
属性,而是[src]
。为什么这里不能使用src
呢?
在DOM已经加载,而angular还没有运行时,DOM会直接将product.imageuri
字符串当作图片的链接,尝试加载对应的图片,这时就会出现404
的错误。
而使用[src]
属性,意思是,我们会在当前的img
标签上,使用angular的src input
。这样,在angular完成解析product.imageuri
表达式时,会替换img
标签的src
属性。
ProductSort
组件代码如下:
@Component({
selector: "product-sort",
inputs: ["product"],
template: `
<div>
<span *ngFor="#name of product.sortName; #i = index">
<a href="#">{{name}}</a>
<span>{{i < (product.sortName.length - 1) ? '>' : ''}}</span>
</span>
</div>
`
})
class ProductSort {
product: Product;
}
此处,在View模板的实现中,需要注意亮点。
一是,使用了ngFor
的另一个属性index
,用于标识遍历的索引值。
另一就是三元表达式
---i < (product.sortName.length - 1) ? '>' : ''
。
语法格式为:expression ? valueIfTrue : valueIfFalse
。?
之前是个表达式,:
两边分别是表达式为真和为假时对应的返回值。
至于PriceDisplay
就不做介绍了,实现大同小异,具体参考下示例代码。
至此,InventoryApp
就完成了,在工程的根目录下,执行npm run tsc
和npm run go
,并访问http://localhost:8080
,查看具体的实现效果。