Open kittencup opened 8 years ago
Angular 2应用无非就是一个是组件树。
在树的根部,最顶层的组件就是应用的本身,这就是浏览器将引导的(bootstrapping)的应用
其中一个有关组件强大的是,他们是可组合。这意味着,我们可以从较小组件来建立更大的组件。应用只是一个简单的渲染其他组件的组件。
因为组件是一个父/子树的结构,每个组件渲染时,将递归地渲染其子组件。
例如,让我们来讨论一个简单的库存管理应用程序,下面是它的页面模型:
鉴于这种模型,编写这个应用程序我们要做的第一件事就是把它分成单个组件(组织成一个树)。在这个例子中,我们可以组页面分成三个相同级别的组件
该组件将渲染导航部分。这将允许用户访问应用的其他页面
这将渲染表示应用程序中的用户目前在哪里。
这将是一个表示产品的集合。
把这个组件分解成更小的下一个级别组件,也就是说,产品列表组件是有多个产品列组成的
当然,我们可以继续更进一步,将每个产品行分解成更小的片段:
最后,把它全部连成一个树表示,我们最终得到如下图:
在顶部,我们看到库存管理app:这是我们的顶层应用
在应用下面,我们有导航,面包屑和产品列表的组件。产品列表每行为一个产品行组件。
而这个产品行组件由分类,图片,价格组件组成的。
需要注意的重要一点是,每个应用只能有一个顶层组件。
让我们来共同构建这个应用。
你可以在 下载文件目录
how_angular_works/inventory_app
中找到该应用全部的代码
下面是当我们完成应用时的截图:
关于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没有任何依赖关系,它只是一个模型,将用在我们的应用上。
正如我们之前提到,组件是Angular 2应用基本构建块。"应用"本身就是顶层组件。然后我们把应用划分为更细粒度的子组件。
提示:当建立一个新的Angular应用,先设计实体模型,然后把它分解成组件。
我们将使用它们,所以值得更仔细地看着它们。每个组件都是由三部分组成:
为了说明我们需要了解有关组件的主要概念,让我们专注于产品列表中的子组件:
下面是一个最基础的顶层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
注解指定了
selector
,告诉Angular 要怎么匹配元素template
,定义了视图让我们更详细的看看每一部分
该@Component
装饰器用于配置组件。@Component
将配置外界如何与你的组件进行交互。
有许多可用于配置组件的选项,我们将在组件这章具体讨论。在本章中,我们只是要触及一些基本知识。
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给
Product
的Constructor
,那么我们就不必记住参数的顺序,也就是说,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);
}
}
注意我们在这里做了三件事:
constructor
方法 - 当Angular为该组件创建一个实例时,会调用constructor
函数,在这里,我们可以为初始化点数据。InventoryApp
上,当写上product:Product
,表示InventoryApp
实例有一个product
属性,类型是Product
对象constructor
中,我们创建了Product实例,并赋值给product属性前面为product属性分配了值,现在我们就可以在视图中使用该变量。改变我们的模板如下:
@Component({
selector: 'inventory-app',
template: `
<div class="inventory-app">
<h1>{{ product.name }}</h1>
<span>{{ product.sku }}</span>
</div>
`
})
使用{{...}}
语法称为模板绑定。
所以在这种情况下,我们有两个绑定:
{{ product.name }}
{{ product.sku }}
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
指令。
当使用的product-list
时,我们要使用Angular的关键特性:input
和output
<products-list
[productList]="products" <!-- input -->
(onProductSelected)="productWasSelected($event)"> <!-- output -->
</products-list>
[squareBrackets]传入输入,(parenthesis)处理输出
数据通过input流入组件,通过output流出组件
可以认为 input+ouput 是你组件定义的公开API
在Angular 可以使用input将数据传入到子组件
<products-list
[productList]="products"
这个属性分为2块
[productList]
(左侧) "products"
(右侧)左侧的[productList]
表示我们在ProductsList组件接受的输入名为productList
右侧的products 表示要传递的值的表达式,也就是说InventoryApp类中的this.products
数组
在Angular ,你可以使用outputs向组件外发送数据
<products-list
...
(onProductSelected)="productWasSelected($event)">
我们从ProductsList组件监听output的onProductSelected
就是说:
现在,我们还没有谈到如何定义input或output,但我们很快会在我们ProductsList组件中定义。
@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);
我们的ProductRow用来展示Product.ProductRow
productrow将有它自己的模板,但也会被分割成三个更小的组件:
这里有一个更直观的图来显示ProductRow
使用的三个组件:
ProductRow 组件的配置
/**
* @ProductRow: A component for the view of single Product
*/
@Component({
selector: 'product-row',
inputs: ['product'],
host: {'class': 'item'},
directives: [ProductImage, ProductDepartment, PriceDisplay],
我们先定义selector
为product-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组件定义类很简单:
class ProductRow {
product: Product;
}
这里我们指定了ProductRow
将会有一个product
实例变量,因为我们指定了input
为product
。当Angular创建该组件实例时,会自动将input
的product
分配给ProductRow
中的product
,我们不需要手动来做,我们不需要一个构造函数。
现在,让我们来看看模板:
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: 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;
}
现在让我们谈谈我们使用的三个组件。他们很简单
/**
* @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属性
接下去,让我们来看看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多行字符串里,${{}}
有替换变量的作用,所以在这里我们在$
之前添加了一个 '\' 用来转义
/**
* @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
称为 三元运算符
现在我们有了项目所需要的所有部分。 当我们完成后它看起来像这样:
你可以在目录
how_angular_works/inventory_app
下找到示例代码,详见README
现在,您可以通过点击来选择一个特定的产品,当选择后它会显示一个漂亮的紫色轮廓。如果您在代码中增加新的产品,你会看到他们被渲染。
如果我们开始添加更多的功能到该应用,你可能想知道我们如何管理数据流
例如,假设我们想增加一个购物车的视图,然后我们将项目添加到购物车。我们应该怎么实现它?
我们已经讨论过的唯一工具是发出output时间,当我们点击add-to-cart,我们简单的冒泡addedToCart时间到根组件上进行处理?这感觉有点别扭。
数据结构是一个很大话题,有很多见解。值得庆幸的是,Angular是可以足够灵活处理各种各样的数据架构,这就意味着你必须自己决定使用哪一个。
在Angular 1,默认的选择是双向的数据绑定。双向数据绑定是超级容易上手:你的控制器有数据,表单直接操作数据,和视图显示数据。
双向数据绑定的问题是,随着你的项目增长,它往往会导致整个应用的级联效应,并很难跟踪你的数据流
双向数据绑定的另一个问题是,因为它常常迫使你的“数据布局树”与你的“DOM视图树”匹配。在实践中,这两者应该是分开的。
你可能处理这种情况,就是创建一个ShoppingCartService,这将是一个单例,保存当前购物车中商品的列表。此服务可以在购物车更改时通知任何感兴趣的对象。
这个想法是很容易的,但在实践中有很多的细节需要解决。
在Angular2,并且很多现代的Web框架(如React)是采用单向数据绑定模式,也就是说,你的数据流只能向下流入组件,如果你需要进行数据变化,你可以发射导致变化的事件到顶部,然后在往下流入组件
单向数据绑定可能在开始的时候增加了一些性能开销,但它节省了大量的各地复杂的变化检测,它使您的系统更容易推算。
值得庆幸的是有两个主要的对手,用于管理您的数据架构:
1. 使用 Observables-based架构,像RxJs
2. 使用 Flux-based架构
在这本书的后面,我们将讨论如何为您的应用实现一个可扩展的数据架构。
该issue关闭讨论,如有问题请去 https://github.com/kittencup/angular2-ama-cn/issues/43 提问
目录
在本章,我们将讨论Angular 2中得高级概念,我的想法是,通过一步一步的方式,可以让你看到所有的组件是如何组合在一起的。
在接下来的章节中,我们将深入了解每个概念,但在这里我们只给出一个概述,并解释其基本思路。
第一个大的思想是一个Angular 2应用是由组件(Component)构成。组件的一种方式是教浏览器来认识新标签,如果你有Angular 1的使用背景,组件是类似于Angular 1指令(事实证明,Angular 2也有指令,但是以后我们会进一步讨论这个区别)。
然而 Angular 2组件相对于Angular 1指令有一些显著的优势。我们将在下面讨论。首先,让我们从头开始:应用程序。