kittencup / angular2-ama-cn

angular2 随便问
692 stars 101 forks source link

Angular 2 如何运作 #34

Open kittencup opened 8 years ago

kittencup commented 8 years ago

该issue关闭讨论,如有问题请去 https://github.com/kittencup/angular2-ama-cn/issues/43 提问

目录

在本章,我们将讨论Angular 2中得高级概念,我的想法是,通过一步一步的方式,可以让你看到所有的组件是如何组合在一起的。

如果你有使用过Angular 1,您会注意到Angular 2以一个新的思维模式来构建应用程序。别慌!对于Angular 1用户来讲Angular 2既简单又熟悉。在本书的后面章节,我们将专门讨论如何从Angular 1转换应用到Angular 2

在接下来的章节中,我们将深入了解每个概念,但在这里我们只给出一个概述,并解释其基本思路。

第一个大的思想是一个Angular 2应用是由组件(Component)构成。组件的一种方式是教浏览器来认识新标签,如果你有Angular 1的使用背景,组件是类似于Angular 1指令(事实证明,Angular 2也有指令,但是以后我们会进一步讨论这个区别)。

然而 Angular 2组件相对于Angular 1指令有一些显著的优势。我们将在下面讨论。首先,让我们从头开始:应用程序。

kittencup commented 8 years ago

应用

Angular 2应用无非就是一个是组件树。

在树的根部,最顶层的组件就是应用的本身,这就是浏览器将引导的(bootstrapping)的应用

其中一个有关组件强大的是,他们是可组合。这意味着,我们可以从较小组件来建立更大的组件。应用只是一个简单的渲染其他组件的组件。

因为组件是一个父/子树的结构,每个组件渲染时,将递归地渲染其子组件。

例如,让我们来讨论一个简单的库存管理应用程序,下面是它的页面模型:

image

鉴于这种模型,编写这个应用程序我们要做的第一件事就是把它分成单个组件(组织成一个树)。在这个例子中,我们可以组页面分成三个相同级别的组件

导航组件

该组件将渲染导航部分。这将允许用户访问应用的其他页面

image

面包屑组件

这将渲染表示应用程序中的用户目前在哪里。

image

产品列表组件

这将是一个表示产品的集合。

image

把这个组件分解成更小的下一个级别组件,也就是说,产品列表组件是有多个产品列组成的

image

当然,我们可以继续更进一步,将每个产品行分解成更小的片段:

最后,把它全部连成一个树表示,我们最终得到如下图:

image

在顶部,我们看到库存管理app:这是我们的顶层应用

在应用下面,我们有导航,面包屑和产品列表的组件。产品列表每行为一个产品行组件。

而这个产品行组件由分类,图片,价格组件组成的。

需要注意的重要一点是,每个应用只能有一个顶层组件。

让我们来共同构建这个应用。

你可以在 下载文件目录how_angular_works/inventory_app中找到该应用全部的代码

下面是当我们完成应用时的截图:

image

kittencup commented 8 years ago

产品模型

关于Angular 的一个关键的事情是,它没有规定一个特定的模型库。

Angular 是足够灵活的,可以支持许多不同类型的模型(和数据结构)。然而,这意味着选择是留给用户自己来确定如何实现这些东西。

在接下去的章节中,我们将有很多关于数据架构的讨论。现在,我们的模型则是简单的JavaScript对象。

/**
 * Provides a `Product` object
 */
class Product {
  constructor(
    public sku: string,
    public name: string,
    public imageUrl: string,
    public department: string[],
    public price: number) {

  } 
}

如果你还未接触过ES6/TypeScript,这个语法可能会有点陌生。

我们创建一个新的Product类,constructor有5个参数,当我们写上public sku:string,它会做2件事

如果你已经熟悉JavaScript,你可以在这里learnxinyminutes迅速赶上一些差异,包括public constructor简写等

这个产品类在Angular没有任何依赖关系,它只是一个模型,将用在我们的应用上。

kittencup commented 8 years ago

组件

正如我们之前提到,组件是Angular 2应用基本构建块。"应用"本身就是顶层组件。然后我们把应用划分为更细粒度的子组件。

提示:当建立一个新的Angular应用,先设计实体模型,然后把它分解成组件。

我们将使用它们,所以值得更仔细地看着它们。每个组件都是由三部分组成:

为了说明我们需要了解有关组件的主要概念,让我们专注于产品列表中的子组件:

image

下面是一个最基础的顶层InventoryApp

@Component({
  selector: 'inventory-app',
  template: `
  <div class="inventory-app">
    (Products will go here soon)
  </div>
  `
})
class InventoryApp {

}
bootstrap(InventoryApp);

如果你一直使用Angular 1的语法则看起来很奇怪!但思想很相似,所以让我们一步一步来看他们:

@Component是一个装饰器,它为紧随其后(InventoryApp)的类增加了元数据。 @Component注解指定了

让我们更详细的看看每一部分

kittencup commented 8 years ago

组件装饰器

@Component装饰器用于配置组件。@Component将配置外界如何与你的组件进行交互。

有许多可用于配置组件的选项,我们将在组件这章具体讨论。在本章中,我们只是要触及一些基本知识。

组件选择器(Selector)

selector键,表示当你渲染HTML模板时如果使你得组件被识别。这个想法是类似于CSS或XPath选择器。选择器将HTML元素匹配该组件。在 selector:'inventory- app'这种情况下, 是要匹配inventory- app标签,也就是说每当我们使用这个组件,那么这个新的标签就被定义了新的内置功能,例如,当我们HTML为

<inventory-app></inventory-app>

Angular 将使用InventoryApp组件来实现这功能。

另外,我们也可以使用常规的div,指定的组件将作为一个属性:

<div inventory-app></div>

组件模板

你可以把视图看作为组件的可视化的部分,在@Component中配置template选项,声明后该组件将具有HTML模板。

@Component({
  selector: 'inventory-app',
  template: `
  <div class="inventory-app">
    (Products will go here soon)
  </div>
  `
})

对于这个模板,请注意,我们使用的是TypeScript反引号“'多行字符串语法。我们的模板到目前为止是相当简单的:只有一些占位文字。

添加一个产品

没有产品视图的应用不是很有趣。现在让我们添加一些。

我们可以创建一个新的产品:

let newProduct = new Product(
  'NICEHAT', 
  'A Nice Black Hat', 
  '/resources/images/products/black-hat.jpg', 
  ['Men', 'Accessories', 'Hats'],
  29.99);

我们的Product constructor需要提供5个参数,通过使用new关键字来创建新的Product对象

通常我可能不会传递给函数超过5个参数,另一种选择是,通过传递一个Object给ProductConstructor,那么我们就不必记住参数的顺序,也就是说,Product可以改为这样:

new Product({sku: "MYHAT", name: "A green hat"})

我们希望能够展示这个Product的视图,为了使模板可以访问它们,我们将它们添加到组件的实例变量中。

例如,如果我们想在视图中访问newProduct,我们可能会这样写:

class InventoryApp { product: Product;
  constructor() {
    let newProduct = new Product(
            'NICEHAT', 'A Nice Black Hat',
            '/resources/images/products/black-hat.jpg',
            ['Men', 'Accessories', 'Hats'],
            29.99);
    this.product = newProduct; 
  }
}

或更简洁:

class InventoryApp { 
  product: Product;
  constructor() {
  this.product = new Product(
            'NICEHAT', 'A Nice Black Hat',
            '/resources/images/products/black-hat.jpg',
            ['Men', 'Accessories', 'Hats'],
            29.99);
  } 
}

注意我们在这里做了三件事:

  1. 添加了constructor方法 - 当Angular为该组件创建一个实例时,会调用constructor函数,在这里,我们可以为初始化点数据。
  2. 声明了一个实例变量 - 在InventoryApp上,当写上product:Product,表示InventoryApp实例有一个product属性,类型是Product对象
  3. 为product赋值 - 在constructor中,我们创建了Product实例,并赋值给product属性

模板绑定

前面为product属性分配了值,现在我们就可以在视图中使用该变量。改变我们的模板如下:

@Component({
  selector: 'inventory-app',
  template: `
  <div class="inventory-app">
    <h1>{{ product.name }}</h1>
    <span>{{ product.sku }}</span>
  </div>
  `
})

使用{{...}}语法称为模板绑定。

所以在这种情况下,我们有两个绑定:

product变量来自于我们InventoryApp组件实例中的product属性

{{}}中的代码是一个表达式,这意味着,你可以做这样的事情:

在第一种情况下,我们使用一个操作符来改变count的显示值。在第二种情况下,我们可以利用函数MyFunction(myArguments)的值替换表达式。使用模板绑定标签是你在应用中显示数据的主要方式。

添加更多产品

我们其实不想在我们的应用程序中显示一个单一的产品-我们实际上要显示一个完整的产品列表。让我们改变InventoryApp来存储产品数组而不是一个单一的产品:

class InventoryApp {
  products: Product[];
  constructor() {
    this.products = [];
  }
}

注意:我们把product重命名为products,并且将类型改变成Product[]product[]字符表明我们想要的products是一个数组类型,里面的每一个元素都是Product对象,我们还可以写成Array<Product>.

现在,我们的InventoryApp拥有一个Products数组。让我们在构造函数中创建一些产品:

class InventoryApp {
    products:Product[];

    constructor() {
        this.products = [
            new Product(
                'MYSHOES', 'Black Running Shoes', '/resources/images/products/black-shoes.jpg', ['Men', 'Shoes', 'Running Shoes'],
                109.99),
            new Product(
                'NEATOJACKET', 'Blue Jacket', '/resources/images/products/blue-jacket.jpg', ['Women', 'Apparel', 'Jackets & Vests'], 238.99),
            new Product(
                'NICEHAT', 'A Nice Black Hat', '/resources/images/products/black-hat.jpg', ['Men', 'Accessories', 'Hats'],
                29.99)
        ];
    }
}

这段代码将会给我们一些产品为我们的应用工作。

选择一个产品

我们希望应用支持用户交互,例如,用户可以选择一个特定的产品来查看关于产品更多的信息,或把它添加到购物车等。

让我们来添加一些功能,当一个新产品被选中时在我们在nventoryApp中处理一些事情,要做到这一点,我们需要定义了一个新的函数,productWasSelected

productWasSelected(product: Product): void { 
  console.log('Product clicked: ', product);
}

产品列表

现在,我们有顶层的InventoryApp组件,我们需要添加一个新组件用来渲染产品列表,在下一章节我们会创建并实现ProductList组件用来匹配products-list选择器,在我们深入实现细节之前,我们先看看如何使用这个新组件:

@Component({
selector: 'inventory-app', directives: [ProductsList], template: `
<div class="inventory-app">
    <products-list
      [productList]="products"
(onProductSelected)="productWasSelected($event)"> </products-list>
</div>
` })

在这里看到有一些新的语法和配置项,所以让我们来谈谈它们:

directives选项

注意在我们@Component配置中添加了directives选项,这定义了我们在视图中想要使用的其他组件是什么。

不像Angular 1,所有的指令本质上是全局,在Angular 2,你必须明确说打算使用哪个指令。在这里,我们要使用的ProductList指令。

input和output

当使用的product-list时,我们要使用Angular的关键特性:inputoutput

<products-list
[productList]="products" <!-- input --> 
(onProductSelected)="productWasSelected($event)"> <!-- output -->
</products-list>

[squareBrackets]传入输入,(parenthesis)处理输出

数据通过input流入组件,通过output流出组件

可以认为 input+ouput 是你组件定义的公开API

[squareBrackets] 传入输入

在Angular 可以使用input将数据传入到子组件

 <products-list
      [productList]="products"

这个属性分为2块

左侧的[productList]表示我们在ProductsList组件接受的输入名为productList

右侧的products 表示要传递的值的表达式,也就是说InventoryApp类中的this.products数组

(parens) 处理输出

在Angular ,你可以使用outputs向组件外发送数据

<products-list
      ...
  (onProductSelected)="productWasSelected($event)">

我们从ProductsList组件监听output的onProductSelected

就是说:

现在,我们还没有谈到如何定义input或output,但我们很快会在我们ProductsList组件中定义。

完整InventoryApp代码

@Component({
    selector: 'inventory-app', directives: [ProductsList], template: `
        <div class="inventory-app">
            <products-list
              [productList]="products"
        (onProductSelected)="productWasSelected($event)"> </products-list>
        </div>
`
})
class InventoryApp {
    products:Product[];

    constructor() {
        this.products = [
            new Product(
                'MYSHOES', 'Black Running Shoes',
                '/resources/images/products/black-shoes.jpg',
                ['Men', 'Shoes', 'Running Shoes'],
                109.99),
            new Product(
                'NEATOJACKET', 'Blue Jacket',
                '/resources/images/products/blue-jacket.jpg',
                ['Women', 'Apparel', 'Jackets & Vests'],
                238.99),
            new Product(
                'NICEHAT', 'A Nice Black Hat',
                '/resources/images/products/black-hat.jpg',
                ['Men', 'Accessories', 'Hats'],
                29.99)
        ];
    }

    productWasSelected(product:Product):void {
        console.log('Product clicked: ', product);
    }
}
bootstrap(InventoryApp);
kittencup commented 8 years ago

ProductsList组件

kittencup commented 8 years ago

ProductRow组件

image

我们的ProductRow用来展示Product.ProductRow productrow将有它自己的模板,但也会被分割成三个更小的组件:

这里有一个更直观的图来显示ProductRow使用的三个组件:

image

ProductRow 组件的配置

/**
 * @ProductRow: A component for the view of single Product
 */
@Component({
  selector: 'product-row',
  inputs: ['product'],
  host: {'class': 'item'},
  directives: [ProductImage, ProductDepartment, PriceDisplay],

我们先定义selectorproduct-row,这个selector我们已经看到过很多次了,这个组件将匹配的product-row标签。

接下去我们定义这个product-row需要一个product输入,这这个product将通过父组件传入进来

第3个host选项是一个新出现的。host 选项可以让我们在host element上设置属性,在这里,我们使用Semantic Ui Item class,当我们设置host:{'class':'item'} 表示我们想要在host element上附加上class item

使用host是不错的,因为这意味着我。可以在组件中来配置我们的host element.这是非常棒的,因为否则我们会要求host element来指定CSS标签,这是不好的,是因为在使用组件时还需要包含上所对应的CSS类部分

接下来,我们指定了我们将要在模板中使用的指令。现在我们还没有定义这些指令,但我们会在稍后定义。

ProductRow 组件类

该ProductRow组件定义类很简单:

class ProductRow {
   product: Product;
}

这里我们指定了ProductRow将会有一个product实例变量,因为我们指定了inputproduct。当Angular创建该组件实例时,会自动将inputproduct分配给ProductRow中的product,我们不需要手动来做,我们不需要一个构造函数。

ProductRow 模板

现在,让我们来看看模板:

template: `
<product-image [product]="product"></product-image> <div class="content">
<div class="header">{{ product.name }}</div> <div class="meta">
<div class="product-sku">SKU #{{ product.sku }}</div> </div>
<div class="description">
<product-department [product]="product"></product-department>
    </div>
  </div>
<price-display [price]="product.price"></price-display> `

我们的模板中没有什么新的概念。

在模板第一行,我们使用product-image指令,我们通过input将product传入到ProductImage组件里,我们以相同的方式使用product-department指令

我们使用price-display指令略有不同,我们通过product.price,而不是直接使用product.

模板的其余部分是标准的HTML元素的自定义CSS类和一些模板绑定

ProductRow完整代码

这里是ProductRow组件的所有代码

/**
 * @ProductRow: A component for the view of single Product
 */
@Component({
selector: 'product-row',
inputs: ['product'],
host: {'class': 'item'},
directives: [ProductImage, ProductDepartment, PriceDisplay], template: `
<product-image [product]="product"></product-image>
<div class="content">
<div class="header">{{ product.name }}</div> <div class="meta">
<div class="product-sku">SKU #{{ product.sku }}</div> </div>
<div class="description">
<product-department [product]="product"></product-department>
    </div>
  </div>
<price-display [price]="product.price"></price-display>
` })
class ProductRow {
  product: Product;
}

现在让我们谈谈我们使用的三个组件。他们很简单

kittencup commented 8 years ago

ProductImage组件

/**
 * @ProductImage: A component to show a single Product's image 
 */
@Component({
  selector: 'product-image',
  host: {class: 'ui small image'},
  inputs: ['product'],
  template: `
  <img class="product-image" [src]="product.imageUrl">
  `
})
class ProductImage {
  product: Product;
}

这里要注意的是关于img标签的问题,我们使用[src]来给img输入值。我们本来可以这个写的:

<!--错误,不要这样做 -->
<img src="{{ product.imageUrl }}">

为什么是错误的?因为在这种情况下,浏览器在运行Angular前就已加载这个模板,浏览器将尝试加载src为{ { product.imageurl } }的图像然后将得到一个404页面,它会在运行Angular前显示一个破碎的图像。

通过使用[src]属性,我们告诉Angular,我们想使用[src]为这个img标签来输入值,一旦表达式被运行,Angular会替换成src属性

kittencup commented 8 years ago

PriceDisplay组件

接下去,让我们来看看PriceDisplay

/**
 * @PriceDisplay: A component to show the price of a
 * Product
 */
@Component({
selector: 'price-display',
inputs: ['price'],
template: `
<div class="price-display">\${{ price }}</div> `
})
class PriceDisplay {
  price: number;
}

这里简单的,但有一点要注意的是,我们现在要输出$,而在es6多行字符串里,${{}} 有替换变量的作用,所以在这里我们在$之前添加了一个 '\' 用来转义

kittencup commented 8 years ago

ProductDepartment组件

/**
 * @ProductDepartment: A component to show the breadcrumbs to a 
 * Product's department
 */
@Component({
  selector: 'product-department',
  inputs: ['product'],
  template: `
  <div class="product-department">
    <span *ngFor="#name of product.department; #i=index">
      <a href="#">{{ name }}</a>
      <span>{{i < (product.department.length-1) ? '>' : ''}}</span>
    </span>
  </div>
  `
})
class ProductDepartment {
  product: Product;
}

需要注意的是ProductDepartment组件的ngFor和span标签

ngFor循环product.department,并将每一个分类分配给name,新出现的部分是第二个表达式#i=index,这是从ngFor中获取当前迭代的索引值。

span标签,我们使用i来决定是否显示 '>' 符号

这里将显示每一个分类的字符串

Women > Apparel > Jackets & Vests

表达式{{i < (product.department.length-1) ? '>' : ''}}表示 如果不是最后一个分类,就显示一个 '>' 符号,如果是最后一个分类就显示空字符串

这种语法test ? valueIfTrue : valueIfFalse称为 三元运算符

kittencup commented 8 years ago

完成的项目

现在我们有了项目所需要的所有部分。 当我们完成后它看起来像这样:

image

你可以在目录how_angular_works/inventory_app下找到示例代码,详见README

现在,您可以通过点击来选择一个特定的产品,当选择后它会显示一个漂亮的紫色轮廓。如果您在代码中增加新的产品,你会看到他们被渲染。

kittencup commented 8 years ago

数据架构上的信息

如果我们开始添加更多的功能到该应用,你可能想知道我们如何管理数据流

例如,假设我们想增加一个购物车的视图,然后我们将项目添加到购物车。我们应该怎么实现它?

我们已经讨论过的唯一工具是发出output时间,当我们点击add-to-cart,我们简单的冒泡addedToCart时间到根组件上进行处理?这感觉有点别扭。

数据结构是一个很大话题,有很多见解。值得庆幸的是,Angular是可以足够灵活处理各种各样的数据架构,这就意味着你必须自己决定使用哪一个。

在Angular 1,默认的选择是双向的数据绑定。双向数据绑定是超级容易上手:你的控制器有数据,表单直接操作数据,和视图显示数据。

双向数据绑定的问题是,随着你的项目增长,它往往会导致整个应用的级联效应,并很难跟踪你的数据流

双向数据绑定的另一个问题是,因为它常常迫使你的“数据布局树”与你的“DOM视图树”匹配。在实践中,这两者应该是分开的。

你可能处理这种情况,就是创建一个ShoppingCartService,这将是一个单例,保存当前购物车中商品的列表。此服务可以在购物车更改时通知任何感兴趣的对象。

这个想法是很容易的,但在实践中有很多的细节需要解决。

在Angular2,并且很多现代的Web框架(如React)是采用单向数据绑定模式,也就是说,你的数据流只能向下流入组件,如果你需要进行数据变化,你可以发射导致变化的事件到顶部,然后在往下流入组件

单向数据绑定可能在开始的时候增加了一些性能开销,但它节省了大量的各地复杂的变化检测,它使您的系统更容易推算。

值得庆幸的是有两个主要的对手,用于管理您的数据架构:

1. 使用 Observables-based架构,像RxJs
2. 使用 Flux-based架构

在这本书的后面,我们将讨论如何为您的应用实现一个可扩展的数据架构。