这一篇来自官网的文章,很系统性的描述整个Angular2大致的各个方面。我英文有限,所以如果单纯翻译恐怕反而无法准确表述;故而,我会按文章的思路结合自己的理解进行描述,对于英文好的人建立直接看官网文档

Angular2框架是帮助构建基于HTML和JavaScript的客户端程序。框架由若干合作库(Material)、核心库(angular/core)、一些可选库(router)组成。

编写应用是由Angular特殊标识的HTML模板、组件类来管理模板、通过services来创建业务逻辑、最后通过根组件 bootstrapper 来引导启动组合而成。当然还有更多内容,后面的章节会介绍到,让我们先来看一张大图:

overview2

整体体系一共包括了八个大的方面:

  1. 模块 Module
  2. 组件 Component
  3. 模板 Template
  4. 元数据 Metadata
  5. 数据绑定
  6. 服务 Service
  7. 指令
  8. 依赖注入

文章所需要的示例代码都在Plunker看得到。

1、 模块(Module)

模块并非是Angular的东西,他实际是ES6标准之一。而对于ES6模块设计思想是尽量静态化,使用编译时就可以确认模块的依赖关系,以及输入和输出变量。

构建组织Angular应用程序都应该是(虽然不强制要求)模块化,在一般情况下,由多个模块来组装我们的应用程序。一个典型的模块是非常有凝聚力的代码块;目的性非常单一。其实从官网首页有对模块化的远景做了大概的描述,目标是希望模块化后的所有功能都是可选、可替代的形式存在,甚至我们可以把代码应用到非Angular当中;你可以从中挑选你喜欢的部分来组装你的应用程序。

模块化本身并没有任何学习成本,无非就是 exportimport 两个语法。

绝大部分应用都有 AppComponent,按照惯例,我们可以查找一个文件为 app.component.ts,里面的代码就有这么使用 export 将AppCompontent做为模块输出的例子:

export class AppComponent { }

在Angular中,一般模块输出的都是一个组件类,因此对于我们需要使用该模块时,需要配合 import 来加载模块。

import {AppComponent} from './app.component';

库模块

一些公共性很强且经常需要使用的模块的集合,Angular把他称为:barrels。这不难理解,当我们加载 angular2/core 模块时,我们可以调用到Angular一些核心类库,比如:元数据、依赖注入等等,源代码里只有几行代码,实际就是把相关的模块重新引入再输出罢了。

库模块除了angular/core,还包括:angular2/http(网络请求相关)、angular2/router(路由相关)、angular2/common(表单、指令等)核心模块。

当我们需要使用Component函数只需要从 angular2/core 加载。

import {Component} from 'angular2/core';

相比一下,我们先前加载自己的模块:

import {AppComponent} from './app.component';

他们有什么不同?

在第一个示例中,我们调用是Angular的库模块,加载的模块ID只是 angular2/core,无须前缀路径。

而当我们需要加载一个自己模块时,需要加上文件路径前缀模块名。示例中 ./ 来指定文件相对路径,这意味者我们的模块是在同一个文件夹中。如果模块在其他文件夹里,我们也可以指定其它路径。换言之,我们关心的是文件的物理路径在哪就行了。

模块关键点

  • Angular应用程序由模块组成。
  • 模块输出可以是:类、函数、值或加载其他模块。
  • 应用程序应该是一个模块集合,每一个模块有着凝聚力非常强的功能。

2、组件(Component)

组件是为了控制如何把模板(HTML)以什么样的形式(Style)置入HTML当中,同时每个组件都配有一个类,用来定义业务能力;通过类的属性和方法与视图进行交互。

HeroListComponent 包含着一个通过 HeroService Service组件在初始化时获取所有 Hero 数组的给 heroes 属性,以及一个用于用户点击选择 Hero 的方法。这属性值与方法都可以直接在模块中使用。

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; }
}

其中 HeroService 你甚至连初始化的动作都没有,这一切都归功于依赖注入,这样的动作自然交给Angular。

而Angular会处理创建、更新、移除组件动作。开发人员也应该清楚的了解生命周期所有细节,关系生命周期我会独立来说明。

3、模板(Template)

定义组件时会指定一个模板,模板是为了告知Angular如何去渲染组件。模板的语法大部分看起来跟HTML语法一样,但是有些奇怪,比如我们来看一段代码:

<h2>Hero List</h2>
<p><i>Pick a hero from the list</i></p>
<div *ngFor="#hero of heroes" (click)="selectHero(hero)">
  {{hero.name}}
</div>
<hero-detail *ngIf="selectedHero" [hero]="selectedHero"></hero-detail>

h2p 这些是HTML元素我们都认识,但是*ngFor{{hero.name}}(click)又是什么鬼?

Angular模块语法已经有非常详细的介绍,之后我也会进行整理。

现在我们重点关心 <hero-detail>,它是代表 HeroDetailComponent 组件的自定义元素。

HeroDetailComponent 是一个显示hero的详情信息,而之前我们提到的 HeroListComponent 是显示hero列表,用户点击列表某个hero才会显示hero的详情信息。换言之,HeroDetailComponent 是 HeroListComponent 的子组件。来看一下他们的关系:

component-tree

注意到实际上是混合时HTML和Angular扩展HTML元素,特别是Angular扩展HTML元素极大的加强HTML元素本身的能力。以这样的一种方式,每一组件有着单一的功能,组件嵌套组件,而且这些组件都是树状结构组织在一起,我们就是通过这个复杂的组件树来构建我们的应用程序。

4、元数据(Metadata)

元数据是告诉Angular如何来处理一个类(class)。

回头来看 HeroListComponent 他只是很单纯的一个类(class),假如没有元数据的声明,他就是一个类,一个无法单独运行再普通不过的类而已。

而真正让 HeroListComponent 有价值的是这些元数据的定义。而这种特殊语法(@开头)其实是由TypeScript来支持的,我们可以称他为:注解,当然这种形式已经是ES7的提案了。我们也不必担心他是TypeScript所特有,况且Babel转码器也有支持。

@Component({
  selector:    'hero-list',
  templateUrl: 'app/hero-list.component.html',
  directives:  [HeroDetailComponent],
  providers:   [HeroService]
})
export class HeroesComponent { ... }

@Component 构造的注解下方是组件类(用于处理业务),而注解最终其实是调用某个函数(见:Angular2注解与装饰器的区别?)。注解里需要带上一些参数:

  • selector – CSS选择器,为了告知连接组织的位置。
  • templateUrl – 组件的模板地址。
  • directives – 是一个数组,告知模板所用到的指令或组件。
  • providers – 是一个数组,注册哪些Service服务,从而在组件类构造函数中能够正确实例这些Server服务。

@Component 以函数配置化的形式,通过此告知Angular如何去使用该组件。模板、元数据、组件类一起形成完整的视图。

除了我们提到的一些元数据外,还包括一些常用的 @Injectable、@Input、@Output、@RouterConfig 元数据。具体以后我也会有相应的文章来详细说明。

5、数据绑定

相信所有对Angular感兴趣的人,都是因为他的双向数据绑定的妙用,简直就是神一样的东西,极大的简化富客户端开发成本。其实早期Angular1的初衷就是为了简化构建富客户端的成本,把我们从数据与HTML元素之类的推和拉过程释放出来,使用Angular后,我们再也不知道什么是 $().val() 是怎么一个情况,这还不包括一些数据验证的恶心代码。

databinding

Angular数据绑定是一种协调组件与模板之间通信机制,通过HTML元素来描述采用哪种数据绑定形式。

图中表述了Angular2的四种数据绑定的关系,这和Angular1只有一种双向绑定大相径庭,虽然双向绑定非常好用,但是同时也是性能最大的诟病。

在示例模板中使用到三种数据绑定:

<div>{{hero.name}}</div>
<hero-detail [hero]="selectedHero"></hero-detail>
<div (click)="selectHero(hero)"></div>
  • hero.name 属性值插入到 div 标签中。
  • [hero] 属性绑定 selectedHero,子组件 HeroDetailComponent 就可以调用父组件 HeroListComponent 的方法。
  • (click) 事件绑定,即用户点击事件时触发。

最后一种就是双向绑定,这和angular1一个意思。示例当中没有提及,但是它看起来是这样:

<input [(ngModel)]="hero.name">

双向绑定会根据用户录入的值传回组件类 hero.name 属性,反之变更组件类的 hero.name 属性值也会反应至HTML元素中。

对于Angualr2的数据绑定原理我可能无法说得更多,但是我会以后单独文章来说明。

6、指令(Directive)

Angular模板是一个动态性的,在渲染时会按照DOM中的指令来进行转换。一个指令也是由元数据和一个类,只不过使用 @Directive 元数据来表述指令。

指令和组件功能其实有一点类似,但他们的同时存在有本质的区别,指令更注重对HTML元素功能扩展;而组件是一个据有具体业务功能。

Angular有两种常见的指令,分别:structural和attribute指令。

Structural

结构性指令用来改变DOM的增加、修改、删除、替换。

<div *ngFor="#hero of heroes"></div>
<hero-detail *ngIf="selectedHero"></hero-detail>
  • *ngFor 循环一个 div 元素。
  • *ngIf 判断语法。

Attribute

属性指令是用来改变DOM的外观和行为。

比如常用的 ngModel 指令来实现一个双向绑定示例:

<input [(ngModel)]="hero.name">

Angular本身已经提供了诸多常用指令,比如用于改变布局(如:ngSwitch)或修改DOM元素外观属性(如:ngClass、ngStyle),当然还包括我们自定义的指令。

7、服务(Service)

“Service” 使用非常广泛,所有我们应用程序需要的值、函数都可以是Service。服务通常应该是一个目的性非常明确的类,比如:

  • 日志Service
  • 数据Service
  • 应用程序配置信息
  • 个人所得税计算器

Angular没有对Service有着特殊定义,没有诸如 ServiceBase 类做为基础类,他是一个非常简单的类。

但是,Service服务又是构建Angular应用程序的基础。

一个日志服务Server类,把日志打印在浏览器的控制台上:

export class Logger {
  log(msg: any)   { console.log(msg); }
  error(msg: any) { console.error(msg); }
  warn(msg: any)  { console.warn(msg); }
}

另一个示例 HeroService,是返回所有hero列表的promise对象。同时又依赖 LoggerServiceBackendService 来处理其他事务。

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;
  }
}

Service 无处不在。而且组件是Service最大的消费者,组件都是靠服务来处理大部分事务,组件不会直接从服务器获取数据、不验证用户输入的值,所有这些都是委派给服务来完成。

一个好的组件设计只提供数据绑定的属性和方法,而把其他业务逻辑的事交给Service来处理;虽然 Angular 并不强制要求执行这种原则。但 Angular 更推荐我们去遵循原则:把应用程序逻辑转化成服务(Service),并通过依赖注入提供给组件来消费服务。

8、依赖注入(Dependency Injection)

依赖注入是一种对类实例的新方法,它完全依赖于它所需要的依赖关系。大都数是依赖于服务Service。Angular就是通过依赖注入为组件提供所需要的服务。

在TypeScript只需要在构造函数的参数定义时,告知哪个服务需要依赖注入到组件当中。比如 HeroListComponent 需要 HeroService 服务,只需要这么写:

constructor(private _service: HeroService){ }

当Angular去创建组件时,会先告诉 Injector 需要 HeroService 服务。

Injector是用于维护创建服务实例的容器,如果请求的实例不在容器当中,会先通过Injector来创建一个新实例放入容器当中,当需要的所有服务都已经创建并返回结果后,Angular就会以参数传递形式传递给构造函数。

HeroService 处理过程就像这样:

injector-injects

如果Injector没有 HeroService 实例,那它是如何去创建?

总之,我们必须预先在启用时来注册 HeroService 到 Injector,我们可以把服务注册到任何一个级别的应用程序组件树当中。比如我们经常这么使用,当我们引导应用程序启动时,我们就希望能够实例一个服务,比如:

bootstrap(AppComponent, [BackendService, HeroService, Logger]);

或者,我们可以组件级别里注册服务。

@Component({
  providers:   [HeroService]
})
export class HeroesComponent { ... }

Angular依赖注入无处不在,这里并没有给出非常细节上的描述,我相信想要深入了解Angular2依赖注入是必备的知识之一。

总结

以上叙述的八个方法算是整体Angular2体系概况,但非常的基础,但是阅读此文可以让我们对整体Angular2有个整体了解,这对于学习Angular2非常有帮助。

本文并没有完全按照官网直接翻译,更多的建立自己理解的语言之上。有兴趣可以直接阅读原文