Angular 2是一个帮助我们使用HTML和JavaScript构建客户端应用的框架。
这个框架包含几个互相协作的库,一些是核心库,一些是可选库。
我们通过以下几个步骤书写应用:
- 使用Angular自定义标记编写HTML模板
- 编写组建类来处理这些模板
- 在services中添加应用逻辑
- 将顶级根组件传递给Angular引导
Angular接管之后,在浏览器中显示应用内容,并根据我们提供的指令对用户的交互做出响应。
当然不仅仅是这些。我们将在后面内容中详细学习。首先我们总揽全局看一下:
这个架构图展示了8个Angular 2应用的构成块:
- Module(模块)
- Component(组件)
- Template(模板)
- Metadata(元数据)
- Data Binding(数据绑定)
- Directive(指令)
- Service(服务)
- Dependency Injection(依赖注入)
下面就开始学习!
模块
Paste_Image.pngAngular应用是模块化的。
简言之,我们使用许多模块组装成我们的应用。
一个典型的模块是专于一个功能的紧密结合的代码块。一个模块在代码块中输出一些值,一般是像类的值。
模块系统是可选的,并非是Angular必须的,但是强烈推荐使用。模块化开发的优点可以自行体会。
也许我们遇见的第一个模块就是一个输出组件类的模块。组件是Angular的基本构成块之一,我们会讲很多,在下一部分我们会讨论组件。现在知道组件类是我们从模块中输出的最常见的东西就行了。
大部分应用都有一个<code>AppComponent</code>。按照惯例,我们会在名为<code>app.component.ts</code>(本系列教程均使用TypeScript书写。 TypeScript是JavaScript的超集,它可以编译成纯JavaScript,支持大多ES6特性。)的文件中找到它。在这个文件中我们会看到如下<code>export</code>语句。
app/app.component.ts(代码片段)
export class AppComponent { }
这个<code>export</code>语句告诉TypeScript这个模块的<code>AppComponent</code>类是公开的,可以被其他模块使用。
当我们需要引用<code>AppComponent</code>时,我们像下面一样导入即可:
app/main.ts(代码片段)
import { AppComponent } from './app.component';
这个<code>import</code>语句告诉系统它可以从一个位于邻近文件的名为app.component的模块中获得一个<code>AppComponent</code>。模块名经常就是文件名去掉扩展名。
库模块
Paste_Image.png一些模块是其他模块的库。
Angular本身作为一个在几个npm包中的库模块集发布。它们的名字都带有<code>@angular</code>前缀。每个Angular库包含一个barrel模块,事实上就是几个逻辑相关的私有模块的一个公共输出口。
<code>@angular/core</code>库是我们获取所需内容的主要Angular类库。
也有其他Angular类库,例如<code>@angular/common</code>,<code>@angular/router</code>和<code>@angular/http</code>。
我们使用几乎一样的方法从Angular类库引入我们需要的东西。例如,我们从@angular/core模块中引入Angular的Component函数:
import { Component } from '@angular/core';
对比一下我们之前引入AppComponent的语法:
import { AppComponent } from './app.component';
两者不同在于当从Angular模块引用时,只需要模块名即可,不需要路径前缀。
要点总结:
- Angular应用由模块组成
- 模块输出其他模块可以引用的类、函数、值
- 我们喜欢将我们的应用写成一个模块集合,每个模块输出一个东西。
我们写的第一个模块很可能会输出一个组件。
组件
Paste_Image.png一个组件控制着屏幕上一块我们称之为视图的区域。应用整个显示区域、hero列表、hero编辑器……它们都是被组件控制的视图。
我们在一个类中定义组件的应用逻辑(用来支持视图)。类和视图通过一个属性和方法的API进行交互。
例如,一个HeroListComponent可能包含一个heroes属性,它从一个服务获取一个hero数组。它可能还有一个selectHero()方法,当用户点击hero列表中的一个hero时会设置一个selectedHero属性。它可能类似下面的一个类:
app/hero-list.component.ts
export class HeroListComponent implements OnInit {
constructor(private service: HeroService) { }
heroes: Hero[];
selectedHero: Hero;
ngOnInit() {
this.heroes = this.service.getHeroes();
}
selectHero(hero: Hero) { this.selectedHero = hero; }
}
当用户使用一个应用时,Angular不断创建、更新和销毁组件。开发者可以在整个生命周期的任何时间通过可选的生命周期钩子执行一些操作。
模板
Paste_Image.png我们使用模板来定义组件的视图。模板就是以HTML的形式告诉Angular如何渲染组件。
大多数情况下一个模板看起来就像常规的HTML,可能有一些陌生(多一些Angular命令)。
下面是一个HeroList组件的模板:
app/hero-list.component.html
<h2>Hero List</h2>
<p><i>Pick a hero from the list</i></p>
<div *ngFor="let hero of heroes" (click)="selectHero(hero)">
{{hero.name}}
</div>
<hero-detail *ngIf="selectedHero" [hero]="selectedHero"></hero-detail>
我们认识其中的<p>和<div>。但是也有其他一些我们不认识的标记,例如*ngFor, {{ hero.name }}, (click), [hero]和<hero-detail>。这些是Angular模板语法的例子,我们会慢慢习惯甚至喜欢它。之后我们会详细解释。
在解释之前,我们先关注一下最后一行。<hero-detail>标记是一个代表HeroDetailConponent的自定义元素。
HeroDetailConponent和我们之前见过的HeroListComponent不同。HeroDetailConponent展示一个具体的hero的信息,而这个hero是用户在HeroListComponent展示的列表中选择的一个hero。HeroDetailConponent是HeroListComponent的一个子组件。
我们可以看到<hero-detail>和其他我们已知的HTML元素和谐地在一起,我们可以在布局中混合使用自定义组件和原生HTML。
我们会用这种方式编写复杂的组件树以构建一个功能丰富的应用。
Angular Metadata
Metadata告诉Angular如何处理一个类。
回看HeroListComponent,它就是一个类,毫无框架的痕迹,里面也没有Angular。
事实上,它就是一个类。只有我们将它通告给Angular,它才算一个组件。
我们通过将metadata附属到类来告诉Angular HeroListComponent是一个组件。
用TypeScript附加metadata的简单方法是使用一个decorator。下面是HeroListComponent的一些metadata:
app/hero-list.component.ts
@Component({
selector: 'hero-list',
templateUrl: 'app/hero-list.component.html',
directives: [HeroDetailComponent],
providers: [HeroService]
})
export class HeroesComponent { ... }
在以上代码中我们看到@Component decorator将它下面的HeroesComponent类标记成一个组件类。
一个decorator是一个函数,它经常有一个配置参数。@Component decorator接受一个配置对象,其中包含着Angular用来创建和显示组件及其视图的信息。
下面是一些可能的@Component配置选项:
- selector 插入组件的标签名
- templateUrl 组件模板地址
- directives 一个当前模板需要的组件和指令的数组。
- providers 一个组件需要的服务(依赖注入)的数组。
@Component函数接受配置对象并将其转换为metadata,然后附加到组件类定义中。Angular在运行时发现这些metadata并因此指导如何做“正确的事”。
模板、metadata和组件一起描述视图。
我们使用类似的方法应用其他metadata decorator来指导Angular行为。随着我们应用功能的增强,@injectable, @Input, @Output, @RouterConfig是几个我们需要掌握的decorator。
重点:我们必须在代码中添加metadata以让Angular知道做什么。
数据绑定
Angular提供数据绑定,一种协调配合模板部分和组件部分的机制。我们在模板中添加绑定标记以告诉Angular如何连接两部分。
有四种数据绑定的语法。每种都有一个方向:到DOM、从DOM、双向,可以通过下图的箭头方向区别。
我们在下面的例子绑定中可以看到三种数据绑定:
app/hero-list.component.html
<div>{{hero.name}}</div>
<hero-detail [hero]="selectedHero"></hero-detail>
<div (click)="selectHero(hero)"></div>
- 插值 将组件的hero.name属性值显示到<div>标签中
- 属性绑定 将父组件的selectedHero属性传递给子组件的hero属性中
- 事件绑定 当用户点击hero的名字时调用组件的selectHero方法
双向数据绑定是第四种形式,很重要。它使用ngModel指令在一个简单表达中将属性和事件绑定组合到一起。我们在HeroListComponent模板中没有双向数据绑定,下面是HeroDetailComponent模板中的例子。
<input [(ngModel)]="hero.name">
在双向数据绑定中,一个数据属性值通过属性绑定从组件流入到输入框中。用户对数据的改变通过数据绑定又流回到组件中,将属性设为最新值。
Angular在每个JavaScript事件循环中处理一次所有的数据绑定,从应用组件树的根节点开始深度优先遍历。
我们并不知道所有的细节,但是数据绑定在模板和组件之间、父组件和子组件之间通信中的重要作用大家有目共睹。
指令
我们的Angular模板是动态的。当Angular渲染它们时,它根据指令转变DOM。
指令是包含指令元数据的一个类。在TypeScript中我们使用@Directive decorator将元数据添加到类上。
我们已经见过一种指令——组件。一个组件就是包含模板的一个指令,@Component decorator事实上是一个使用面向模板特性扩展的@Directive decorator。
还有两种指令,我们称之为结构指令和属性指令。
它们倾向于在元素标签内部出现,有时以名字的形式,但更多是作为一个赋值或绑定的目标的形式。
结构化指令通过添加、删除和替换DOM来改变布局。
我们在我们的案例模板中见过两个内置结构化指令:
<div *ngFor="let hero of heroes"></div>
<hero-detail *ngIf="selectedHero"></hero-detail>
- *ngFor告诉Angular对heroes列表中的每一个hero构建一个<div>
- *ngIf只在有hero被选中时才包含HeroDetail组件
属性指令改变已有元素的表现和行为。在模板中它们看起来就像普通的HTML属性。ngModel指令就是一个属性指令,它实现了双向数据绑定。
<input [(ngModel)]="hero.name">
它通过设置元素的显示值属性和响应change事件来改变已有元素的行为。
Angular自带一些改变布局结构(例如ngSwitch)或者改变DOM元素和组件方面(例如ngStyle和ngClass)的指令。
当然我们也可以编写自己的指令。
服务
服务是一个很宽泛的范畴,可以包含我们应用需要的任何值、函数和特性。
基本上任何东西都可以是一个服务。一个典型的服务就是一个特定的、设计良好的类。它应该做一些具体的事情,并且做的很好。
例子包含:
- logging服务
- 数据服务
- message bus
- 税计算器
- 应用配置
关于服务Angular没有具体的说明。Angular本身并没有定义服务。并没有一个ServiceBase类。然而服务对于Angular应用是基本的。
下面是一个记录日志到浏览器console的服务类的例子:
app/logger.service.ts(只有类)
export class Logger {
log(msg: any) { console.log(msg); }
error(msg: any) { console.error(msg); }
warn(msg: any) { console.warn(msg); }
}
下面是一个HeroService,它获取heroes并以一个promise的形式返回它。HeroService依赖LoggerService和一个简单用于和服务器通信的BackendService。
app/hero.service.ts(只有类)
export class HeroService {
constructor(
private backend: BackendService,
private logger: Logger) { }
private heroes: Hero[] = [];
getHeroes() {
this.backend.getAll(Hero).then( (heroes: Hero[]) => {
this.logger.log(`Fetched ${heroes.length} heroes.`);
this.heroes.push(...heroes); // fill cache
});
return this.heroes;
}
}
服务遍及各处。
我们的组件就是最大的服务消费者。他们依赖服务来处理大多数事物。它们不从服务器获取数据,不验证用户输入,不直接在console中记录。他们将这些任务都委托给服务。
组件的工作就是增强用户体验,无它。它协调视图(由模板渲染得到)和应用逻辑(经常包含model)。一个好的组件展示用于数据绑定的属性和方法,将其他无关紧要的事委托给服务。
Angular并不强制这些原则。如果你要写一个3000行的“厨房水槽”组件它也不会抱怨。
但Angular让我们易于将应用逻辑包含到服务中,也通过依赖注入让服务易于被使用。
依赖注入
依赖注入是一个为一个类的新实例提供它需要的全部依赖的方法。大部分依赖都是服务。Angular使用依赖注入来向新组件提供它们需要的服务。
在TypeScript中,Angular可以通过查看构造器参数来确认要为一个组件提供哪些服务。例如,我们的HeroListComponent组建需要HeroService:
app/hero-list.component(构造器)
constructor(private service: HeroService) { }
当Angular创建一个组件时,它首先向Injector请求组件需要的服务。
一个Injector维护一个它之前创建的服务实例的容器。如果请求的服务实例在容器中不存在,Injector就创建一个并把它添加到容器中,然后将这个服务返给Angular。当所有请求的服务都被解析并返回,Angular就会调用组件的构造器,并将那些服务作为参数。这就是依赖注入的含义。
HeroService注入的过程类似下图:
如果Injector没有HeroService实例,它怎么知道如何创建一个呢?
简而言之,我们必须事先到Injector注册一个HeroService的provider。一个provider就是一个可以创建并返回一个服务的东西,一般就是服务类自己。
我们可以在应用组件树的任一级注册provider。我们经常在引导应用时在根极元素注册以让同一服务的实例可以随处可用。
app/main.ts(代码片段)
bootstrap(AppComponent, [BackendService, HeroService, Logger]);
当然也可以在组件级注册
app/hero-list.component.ts(代码片段)
@Component({
providers: [HeroService]
})
export class HeroesComponent { ... }
在这个案例中我们在组件的每一个新实例中都获得服务的一个新实例。
在这个概览中我们已经大大简化了依赖注入。我们可以在依赖注入章节看到详细说明。
要点是:
- 依赖注入被绑定到框架中,到处都在用
- Injector是主要机制
- 一个Injector维护一个它创建的服务的容器
- Injector可以使用provider创建新服务
- 一个Provider是创建一个服务的菜谱(说明)
- 我们向injector注册provider。
总结
我们已经学习了Angular应用程序的8个主要构成部分。这只是一个基础,我们还有很多要学。
其他
下面是一个简单的,按字母顺序排列的Angular其他重要特性和服务。它们中的大部分都在开发指南中涉及。
Animation动画——一个现成的动画库,它让开发者不知道太多动画技巧和CSS也能为组件添加动画效果。
Bootstrap引导——一个配置和启动根应用组件的方法。
Change Detection变化检测——学习Angular如何判定一个组件的属性值发生了改变并更新屏幕。学习它如何使用zones来拦截异步活动并运行它的变化检测策略。
Component Router组件路由——使用组件路由服务,用户可以使用熟悉的URL方式在一个多视图应用中导航。
Events(事件)——DOM可以发起事件,组件和服务也可以。Angular提供了发布和注册事件的机制,其中包含一个RxJS Observable proposal的实现。
Forms表单——基于HTML数据验证和脏检查,支持复杂的数据输入场景。
HTTP——使用这个Angular HTTP客户端和服务器通信以获取数据,保存数据,触发服务端action。
Lifecycle Hooks生命周期钩子——我们可以通过实现钩子接口,利用组件生命周期从创建到销毁的每一个关键时刻。
Pipes管道——改变显示值的服务(其实就是过滤器啦!)。我们可以将管道放入模板来改善用户体验。例如,下面的currency管道表达式:
price | currency:'USD':true
这个例子将价格42.33显示为$42.33。
Testing测试——Angular提供了一个测试库用于在和Angular框架进行交互时对我们的应用部分进行单元测试。
网友评论
这是不正确的,很多依赖都是不走注入器的,比如 ElementRef、ViewContainerRef 等等,你只能够通过直接在构造函数声明来获取,没法通过得到注入器再用 injector.get 来拿到。