@adonia
2016-11-12T12:44:46.000000Z
字数 13982
阅读 827
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中定义的内容所代替。
Productinventory 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,查看具体的实现效果。