[关闭]
@adonia 2016-11-12T12:44:46.000000Z 字数 13982 阅读 533

angular2(5):Application

angular2


Application

在上一章节的示例应用中,体现了Angular2的一大特性---angular2的应用(Application)是由一个或者多个组件(Component)组成的,组件之间以树状结构呈现。而应用本身,就是组件树中的最顶端节点。在浏览器中加载应用的过程,其实就是引导(bootstrap)组件的过程。

Angular2中的组件是可以组装的,也就是说,任何一个大的应用,都可以通过一个个小的组件拼装而成,每个组件各司其职。所有的组件是通过父子树状结构组装的,在加载应用时,会引导顶级的组件,然后逐层遍历加载所有的组件。

下面,通过一个具体的示例来说明。

大家都有过在淘宝上买东西的经历,例如下面的一个电商商品浏览页面:

image_1aund4h7uj4s1t631mtp1l958mq9.png-121.1kB

我们可以根据上图呈现的内容,将其拆分成三大组件:

再将商品列表细分,可以看成是一系列的商品组件(Product Rows Component)组装而成的,如下:
image_1aundehed1ht8p4cklf1t0l1son1t.png-28.8kB

如果,再将商品细分,又可以分成商品图片(Product Image Conponent),类目信息(Product Department component),以及商品价格(Price Display component)。

最终,整个商品应用的结构如下:

image_1aundj40v155m1huo1egq1sou9jo2a.png-67.9kB

最顶层是应用本身---Inventory Management App,在应用的子节点中,包括了导航条,商品列表,以及面包屑。商品列表又是由一个个的商品组成的。而商品自身,包含了商品图片,物理类目信息,以及价格信息。

整个应用的蓝图如下:
image_1aundp6t5lm816vf1qqgltn1q1p2n.png-95.1kB

下面就来一步步地介绍如何构建这个应用。

Product模型

Angular2并没有限制特定的数据模型集合,而是可以灵活支持各种不同的模型和数据结构;而且,这意味着,如何去模拟实现一件事物,完全是在用户的掌控之中。

inventory_app目录下,创建app.ts,在其中定义Product类,代码如下:

  1. class Product {
  2. constructor(
  3. public sku: string,
  4. public name: string,
  5. public imageuri: string,
  6. public sortName: string[],
  7. public price: number
  8. ){
  9. };
  10. }

这里呢,创建了Product类,定义了其构造方法,即类中的constructor方法。构造方法中的参数---public sku: string,传递着两个含义:

Product是用来将页面上的商品信息抽象化的,像这种,就对应着MVC框架中的Model

组件(Component)

组件由三部分组成:

前面提到过,Application是由一个或多个Component组成的,Application自身就是顶级的组件。

Inventory App的大致结构如下:

  1. @Component({
  2. selector: 'inventory-app',
  3. template: `
  4. <div>
  5. (Products will go here soon~)
  6. </div>
  7. `
  8. })
  9. class InventoryApp {
  10. }
  11. 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)

selector的作用是,在加载HTML时,指明自定义的组件如何被识别,作用原理跟CSSJQuery中的selector类似。selector指明HTML中的哪个标签,会与当前的组件相匹配。

当我们说,组件选择器inventory-app时,也就意味着,在HMTL中,与之匹配的是inventory-app标签。而且,无论在何处引用这个标签时,都会具备与之对应的组件的功能。例如,在HTML中定义如下标签:

  1. <inventory-app></inventory-app>

在HTML加载时,就会初始化InventoryApp组件来实现对应的功能。

同样的,也可以通过在常规的div标签中,将组件当作一个属性的形式来使用,如下:

  1. <div inventory-app></div>

视图(template)

@Componenttemplate定义了组件拥有的HTML模板,例如:

  1. @Component({
  2. selector: 'inventory-app',
  3. template: `
  4. <div>
  5. (Products will go here soon~)
  6. </div>
  7. `
  8. })

此处,使用了TypeScript的中模板语法---多行字符串语法。这里只是个示例,定义一个包含了简单文本的div标签。

这样,在HTML中加载inventory-app标签时,该标签的视图就会被template中定义的内容所代替。

增加个Product

inventory app是用来展示商品信息的,下面让我们在创建个商品,代码如下:

  1. let product = new Product('NiceHat', '一款很不错的黑色帽子', '/resources/images/black-hat.jpg', ['男士', '装饰品', '帽子'], 29.99);

此处,使用new关键字来创建类实例,调用Product类的构造方法(constructor),传入对应的5个参数。

为了能够展示商品信息,我们需要把创建的Product实例维护起来,作为InventoryApp组件的一个变量。如下:

  1. class InventoryApp {
  2. product: Product;
  3. constructor() {
  4. this.product = new Product('NiceHat', '一款很不错的黑色帽子', '/resources/images/black-hat.jpg', ['男士', '装饰品', '帽子'], 29.99);
  5. }
  6. }
  7. bootstrap(InventoryApp);

此处,我们做了三件事情:

展示新增的product

我们已经在组件类中增加了一个公有属性,那么现在就可以在视图中使用该公有属性了。代码如下:

  1. @Component({
  2. selector: 'inventory-app',
  3. template: `
  4. <div>
  5. <h2>{{product.name}}</h2>
  6. <span>{{product.sku}}</span>
  7. </div>
  8. `
  9. })

Note:

  • 此处的{{...}}语法为模板绑定(template binding)。模板绑定的作用是通知视图,在template中,会使用{{}}大括号包含的表达式的值,作为展示的内容。

上述示例中,包含了两处模板绑定语法:

表达式中的product关键字,即为组件InventoryApp类实例的product属性。注意,模板绑定中的代码是表达式,也就是说,我们可以在其中做一些运算,或者调用函数,例如:

测试验证

inventory app工程的根目录下,执行命令:

Note:

  • 第一步是下载依赖

  • 第二步是编译app.ts,执行成功的话,在工程的根目录下,会出现app.jsapp.js.map

  • 第三步是运行服务

执行成功的话,在浏览器中访问localhost:8080,应该可以得到如下的页面:

image_1avc7qqsu1sm9t311qb16lj819.png-9.8kB

具体代码,可参考github中的实现。

展示更多的product

inventory应用中,我们希望展示一系列的产品,而不仅仅只是一个。因此,在InventoryApp类中,将原有的product属性,变更为products,类型为Product[],表示是元素为Product的数组。也可以定义为Array[Product]。如下:

  1. class InventoryApp {
  2. products: Product[];
  3. constructor() {
  4. this.products = [
  5. new Product('MyShoes', '黑色运动鞋', '/resources/images/black-shoes.jpg', ['男士', '鞋子', '运动鞋'], 109.99),
  6. new Product('NeatoJacket', '蓝色夹克衫', '/resources/images/blue-jacket.jpg', ['女士', '上衣', '夹克&马甲'], 238.99),
  7. new Product('NiceHat', '一款很不错的黑色帽子', '/resources/images/black-hat.jpg', ['男士', '装饰品', '帽子'], 29.99)
  8. ];
  9. }
  10. }

我们在其构造方法中初始化了products属性,使其包含了三个商品。

选择一个商品

我们希望能够在应用中支持用户交互,例如,用户在浏览商品时,可以点击某一个商品,查看商品详情,或者加入购物车。

为此,需要在InventoryApp类中增加一个方法---productWasSelected,用来处理当商品选中的事件。如下:

  1. productWasSelected(product: Product): void {
  2. console.log("Select product: " + product);
  3. }

稍后介绍如何触发该事件方法。

展示商品列表

我们已经在InventoryApp中维护了一组商品了,现在需要考虑如何将其展示出来。将新定义一个组件---product-list,用于展示商品列表,稍后再介绍其对应的实现类ProductList。先来看下如何使用新组件:

  1. class ProductList {
  2. }
  3. @Component({
  4. selector: 'inventory-app',
  5. directives: [ProductList],
  6. template: `
  7. <div>
  8. <product-list
  9. [productList]="products"
  10. (onProductSelected)="productWasSelected($event)">
  11. </product-list>
  12. </div>
  13. `
  14. })
  15. class InventoryApp {
  16. ...
  17. }

Tips: 此处需要先声明ProductList类,否则第6行会报错。ProductList类的具体实现,后续再介绍。

这里,使用了一些新的语法(syntax)和选项(options),下面一一介绍:

directives声明了在当前组件中,需要引用的其他组件列表。是由被引用的组件的实现类名称组成的数组。

在angular2中,使用其他的组件,必须通过directives的方式声明引用,否则组件将不被解析。也就是说,在angular2中,组件并不是全局的,不像angular1中的指令。

在使用的新组件product-list中,使用了一组新特性--- inputoutput,如下:

  1. <product-list
  2. [productList]="products" <!-- input -->
  3. (onProductSelected)="productWasSelected($event)"> <!-- output -->
  4. </product-list>

其中,中括号[]所声明的是input,而圆括号()声明的则是ouput

input的作用是将外部数据引入到组件中,而output则是将组件中的事件传递出去。

input中,等号左边,方括号所包含的---[productList],是指在组件的实现类ProductList中,将要使用productList所引入的外部输入。而在等号右边,引号中所包含的---products,是指将表达式products的值传递给新组件product-list,此处代表的是InventoryApp中的products数组属性的值。

output中,等号左边,圆括号所包含的---(onProductSelected),是指所要监听的组件ProductListonProductSelected事件。而等号右边---productWasSelected()是当onProductSelected事件发生变化时,当前组件所要处理的方法。$event是一个特殊的字段,表示output中所传递的事件。

简而言之,就是通过input,将InventoryApp中维护的products传递给ProductList组件,用于展示。而在ProductList中选中某一个商品时,会触发其onProductSelected事件,该事件透过output传递给了InventoryApp,经由productWasSelected方法处理事件。

ProductList 组件

下面来介绍下如何实现ProductList组件。

ProductList接收InventoryApp传递的products数组,其作用是为了展示所有的商品,并且可以允许用户点击某一个商品,并实时跟踪用户当前选择的商品。

我们将分三步来实现ProductList组件:

先看下ProductList@Component注解,如下:

  1. class ProductRow {
  2. }
  3. @Component({
  4. selector: 'product-list',
  5. directives: [ProductRow],
  6. inputs: ['productList'],
  7. outputs: ['onProductSelected']
  8. })
  9. class ProductList {
  10. ...
  11. }

Note:

  • `selector属性,指明了在将要使用product-list选择器来引用该组件。

  • directives属性,指明将要引用外部组件---ProductRow,此处同样先定义其类,具体实现后续再介绍。

  • inputs属性,指明了从外部接收数据的入口。

  • outputs属性,指明了将内部监听事件传递出去的出口。

下面来详细介绍下inputsoutputs

inputs

当声明一个input属性时,也就是意味着当前定义的类中,会有一个属性实例来接收该input所传递的值。例如:

  1. @Component({
  2. selector: 'my-component',
  3. inputs: ['name', 'age']
  4. })
  5. class MyComponent {
  6. name: string;
  7. age: number;
  8. }

inputs属性中的nameage分别对应着实现类中的nameage属性实例。那如果需要向组件中传递值呢,就可以通过如下方式:

  1. <my-component [name]="myName" [age]="myAge"></my-component>

这样,MyComponent类实例中,name的值就是myNameage的值就是myAge了。

并不是inputs中的属性值,一定要和类中的属性变量一致的,例如:

  1. <my-component [shortName]="myName" [oldAge]="myAge"></my-component>

为了在组件中对应起来,只需要把组件定义中的inputs改为:

  1. @Component({
  2. selector: 'my-component',
  3. inputs: ['name: shortName', 'age: oldAge']
  4. })
  5. class MyComponent {
  6. name: string;
  7. age: number;
  8. }

也就是说,inputs中的定义格式为: 组件属性: 使用字段。(组件属性是指组件类中的属性变量,使用字段是指在使用组件时,所使用的声明字段)

outputs

如果想在组件中,向外界传递数据,则就需要output绑定(binding)。这个就像发布-订阅系统一般,组件A想要将组件内部的数据传递出去,那么A就将数据以事件流的方式发布出去;组件B想要得到事件流中的数据,则需要监听组件A的事件变化,也就是订阅这个事件。

下面通过一个常见的例子说明下整个过程:点击按钮。

按钮的点击,是在浏览器中最常见的一种触发事件。点击按钮后,通常会有相应的响应信息,例如“弹出框”,“提交表单”等。

而对于一个组件来说,处理组件中按钮的点击事件,就需要在组件的Controller中定义一个方法,并将按钮的点击事件产生的output绑定至该方法上。对应的语法格式为:

  1. (output)="action"

Tips:output对应着触发的事件,例如clickaction对应着Controller中处理事件的方法。

看下具体的示例:

  1. import {Component} from "angular2/core";
  2. import {bootstrap} from "angular2/platform/browser";
  3. @Component({
  4. selector: "counter",
  5. template: `
  6. 当前的值: {{value}}
  7. <button (click)="increase()">增加</button>
  8. <button (click)="decrease()">减少</button>
  9. `
  10. })
  11. class Counter {
  12. value: number;
  13. constructor() {
  14. this.value = 10;
  15. }
  16. increase(): void {
  17. this.value += 1;
  18. }
  19. decrease(): void {
  20. this.value -= 1;
  21. }
  22. }
  23. bootstrap(Counter);

在当前的示例中,有两个按钮,在点击增加按钮时,会使当前的数值加1;在点击减少按钮时,会使当前的数值减1。当前的示例中,output的产生是按钮的click事件,是属于按钮的内置(build-in)事件。类似的内置事件还有像db-clickmouseover等。

那么如何来自定义监听事件呢?

触发自定义事件

为了要实现自定义事件,需要引入angular2中的一个组件---EventEmitterEventEmitter是angular2中用于实现监听器模式(Observer Pattern)的组件,在EventEmitter中会维护一个或者多个订阅者(subscriber),然后将事件发布(publish)给订阅者。

那如何来利用EventEmitter来实现自定义事件呢?主要包括以下三个方面:

下面通过一个具体的例子---信号器发射,来看下具体的实现流程。

首先,实现信号器发射的组件:

  1. import {EventEmitter} from "angular2/core";
  2. import {Component} from "angular2/core";
  3. import {bootstrap} from "angular2/platform/browser";
  4. @Component({
  5. selector: 'single-component',
  6. outputs: ['putRingOnit'],
  7. template: `
  8. <button (click)="liked()">like it?</button>
  9. `
  10. })
  11. class SingleComponent {
  12. putRingOnit: EventEmitter<string>;
  13. constructor() {
  14. this.putRingOnit = new EventEmitter();
  15. }
  16. liked(): void {
  17. this.putRingOnit.emit('oh oh oh...');
  18. }
  19. }

说明:

  • @Component中,声明了outputs属性,其中包含了putRingOnit

  • SingleComponent类中,将putRingOnit声明为一个EventEmitter的实例,此处使用了范型的特性。EventEmitter<string>意味着,该实例能够发布的消息类型是string

  • 在类中定义了一个方法---like(),会使用EventEmitteremit方法发布消息。而方法是在点击按钮的时候触发的。

现在,我们已经定义了一个组件,其中包含了EventEmitter的实例,可以向外发布消息了。现在还需要再定义一个组件,来订阅该消息;在按钮触发时,能够接收到消息并处理。

EventEmitter有一个subscribe()方法,用来订阅消息的,例如:

  1. let ee = new EventEmitter();
  2. ee.subscribe((name:string) => console.log(`Hello${name}`));
  3. ee.emit("Nate");

运行的结果就是Hello Nate

而在angular2中,我们可以不用直接通过subscribe的方式来订阅消息。在我们将EventEmitter的实例加入至outputs属性中时,angular2就已经做了订阅的动作了。也就是说,在新的组件中,只要使用当前组件的outputs就可以达到订阅该消息的目的了。

订阅消息的组件如下:

  1. @Component({
  2. selector: 'club',
  3. directives: [SingleComponent],
  4. template: `
  5. <div>
  6. <single-component (putRingOnit)="ringWasPlaced($event)"></single-component>
  7. </div>
  8. `
  9. })
  10. class ClubComponent {
  11. ringWasPlaced(msg: string): void {
  12. console.log(`Puts your hands up: ${msg}`);
  13. alert(`Puts your hands up: ${msg}`);
  14. }
  15. }
  16. bootstrap(ClubComponent);

还记得之前提到的outputs的语法格式吗?---(output)="action"
此处的outputputRingOnit,来源于组件SingleComponentaction呢,是订阅组件ClubComponentringWasPlaced方法。

看下ringWasPlaced方法的实现,参数类型是string,对应着putRingOnit声明的EventEmitter实例的参数类型。

在点击like it?按钮时,将会触发liked()方法,发布一条消息,内容为oh oh oh...;该消息被ClubComponent组件订阅,并被ringWasPlaced()方法消费。

具体实现参考代码示例

实现ProductList组件

说明: 以下介绍的组件实现内容较多,可对照参考完整的示例代码

回到Inventory应用的例子上。

首先,实现ProductList的Controller。按照之前的介绍,Controller类中应该维护着三个变量实例:

ProductList的Controller代码如下:

  1. class ProductList {
  2. /**
  3. * @input 外部传入的商品列表
  4. */
  5. productList: Product[];
  6. /**
  7. * @output 向外发布当前选中的商品信息
  8. */
  9. onProductSelected: EventEmitter<Product>;
  10. /**
  11. * @property 跟踪当前选中的商品
  12. */
  13. currentProduct: Product;
  14. constructor() {
  15. this.onProductSelected = new EventEmitter();
  16. }
  17. }

说明:

  • productListinput,接收主程序传入的products数组;

  • onProductSelectedoutput,将选中的商品发布出去;

  • currentProduct维护当前选中的商品。

来看下ProductList的View模板:

  1. @Component({
  2. selector: 'product-list',
  3. directives: [ProductRow],
  4. inputs: ['productList'],
  5. outputs: ['onProductSelected'],
  6. template: `
  7. <div>
  8. <product-row *ngFor="#product of productList"
  9. [product]="product"
  10. (click)="clicked(product)"
  11. [class.active]="isSelected(product)">
  12. </product-row>
  13. </div>
  14. `
  15. })

其中,使用组件ProductRow来展示每一个商品的信息,稍后会详细介绍该组件的定义,展示效果如下:
image_1b1c6813bti3ufn2vf6jr559.png-62.9kB

*ngFor="#product of productList",这里使用ngFor语法来遍历整个商品数组,将其中的每个商品传递给ProductRow组件的input。此处使用了ProductRow的内置事件click,在组件的任何区域点击时,会触发ProductList组件的clicked()方法,方法的定义如下:

  1. /**
  2. * @function 点击商品时,维护当前选中的商品,同时,将选中的商品发布出去
  3. */
  4. clicked(product: Product): void {
  5. this.currentProduct = product;
  6. this.onProductSelected.emit(product);
  7. }

这里呢,每次点击商品,都会切换维护的商品信息,并且,将当前商品的信息通过onProductSelected发布出去。让我们来回顾下InventoryApp中的实现:

  1. @Component({
  2. selector: 'inventory-app',
  3. directives: [ProductList],
  4. template: `
  5. <div>
  6. <product-list
  7. [productList]="products"
  8. (onProductSelected)="productWasSelected($event)">
  9. </product-list>
  10. </div>
  11. `
  12. })
  13. class InventoryApp {
  14. products: Product[];
  15. ...
  16. productWasSelected(product: Product): void {
  17. console.log("Select product: " + product);
  18. }
  19. }

InventoryApp中,在触发onProductSelected事件时,会调用组件的productWasSelected()方法。前后结合起来,在点击每个ProductRow时,会将商品最终传递给InventoryApp组件,完成对商品的逻辑处理。

再回到ProductList的View模板上,注意下[class.active]="isSelected(product)"。该段代码是angular2中的语法,意思是在组件ProductListisSelected()方法返回true时,会在组件ProductRow的View模板中,增加CSS样式active

isSelected()方法可参考示例代码,active样式在index.html中定义。

ProductRow组件

再来看下ProductRow的实现效果:

image_1b1c6813bti3ufn2vf6jr559.png-62.9kB

ProductRow同样可以拆分成三个小组件:

ProductRow组件的实现如下:

  1. @Component({
  2. selector: "product-row",
  3. inputs: ["product"],
  4. host: {'class': 'row', 'style': 'margin: 10px;'},
  5. directives: [ProductImage, ProductSort, PriceDisplay],
  6. template: `
  7. <div class="col-sm-2">
  8. <product-image [product]="product"></product-image>
  9. </div>
  10. <div class="col-sm-5">
  11. <h3>{{product.name}}</h3>
  12. <div>SKU #{{product.sku}}</div>
  13. <div>
  14. <product-sort [product]="product"></product-sort>
  15. </div>
  16. </div>
  17. <div class="col-sm-1">
  18. <price-display [price]="product.price"></price-display>
  19. </div>
  20. `
  21. })
  22. class ProductRow {
  23. product: Product;
  24. }

说明:

  • 类中的product变量用于接收ProductList组件传入的商品;

  • 注意ProductRow类中不需要构造方法,因为product是通过input传入的,angualr2在初始化组件时,会自动初始化ProductRow类。

  • @Component中的host属性定义了组件的全局样式;

  • directives中定义了需要引入的三个组件---ProductImageProductSortPriceDisplay

  • ProductRow通过input的方式将相关信息传递给三个组件,不再赘述。

ProductImage组件

ProductImage组件的作用,就是用来展示商品的图片信息的,代码如下:

  1. @Component({
  2. selector: "product-image",
  3. inputs: ["product"],
  4. template: `
  5. <img class="img-thumbnail" [src]="product.imageuri">
  6. `
  7. })
  8. class ProductImage {
  9. product: Product;
  10. }

说明:

  • ProductRow一样,通过input的方式接收变量,无需定义构造方法;

注意img标签中,并不是使用的常规的src属性,而是[src]。为什么这里不能使用src呢?

在DOM已经加载,而angular还没有运行时,DOM会直接将product.imageuri字符串当作图片的链接,尝试加载对应的图片,这时就会出现404的错误。

而使用[src]属性,意思是,我们会在当前的img标签上,使用angular的src input。这样,在angular完成解析product.imageuri表达式时,会替换img标签的src属性。

ProductSort组件

代码如下:

  1. @Component({
  2. selector: "product-sort",
  3. inputs: ["product"],
  4. template: `
  5. <div>
  6. <span *ngFor="#name of product.sortName; #i = index">
  7. <a href="#">{{name}}</a>
  8. <span>{{i < (product.sortName.length - 1) ? '>' : ''}}</span>
  9. </span>
  10. </div>
  11. `
  12. })
  13. class ProductSort {
  14. product: Product;
  15. }

此处,在View模板的实现中,需要注意亮点。
一是,使用了ngFor的另一个属性index,用于标识遍历的索引值。
另一就是三元表达式---i < (product.sortName.length - 1) ? '>' : ''

语法格式为:expression ? valueIfTrue : valueIfFalse?之前是个表达式,:两边分别是表达式为真和为假时对应的返回值。

至于PriceDisplay就不做介绍了,实现大同小异,具体参考下示例代码。

至此,InventoryApp就完成了,在工程的根目录下,执行npm run tscnpm run go,并访问http://localhost:8080,查看具体的实现效果。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注