Angular 应用的基本构造部分
Angular 是一个使用 HTML 和 由 javascript 或者其他语言例如 Typescript 编译而来的 javascript 来构造客户端应用的框架。
该框架由若干库组成,有些是核心必须的,有些是可选的。
编写带有 angular 标记的 HTML 模板(templates)
,编写 组件类(component class)
来管理 模板(templates)
,在 服务(service)
中添加应用逻辑,并在 模块(modules)
中包装 组件类(component class)
和 调用 服务(service)
。
然后你通过 根模块(root module)
的 引导(bootstrapping)
来启动应用,然后由 angular 来接手程序,在浏览器中呈现您的应用并根据你提供的逻辑来回应用户的互动。
angular 当然不止如此,更多的内容将在下文为你讲述,不过现在将注意力放到下面这张脑图上。
架构图显示了 angular 应用的 8 个主要构造部分。
- 模块(Modules)
- 组件(Components)
- 模板(Templates)
- 元数据(Metadata)
- 数据绑定(Data binding)
- 指令(Directives)
- 服务(Services)
- 依赖注入(Dependency injection)
快点开始学习这些构造部分吧。
模块(modules)
angular 是一个模块化的框架,并且拥有它自己的模块系统,名为 Angular modules
或者 NgModules
angular 的模块系统是一个很大的概念,本文只是简单的介绍模块系统,详细的内容请到 Angular 模块 进行深入了解。
每个 angular 应用至少拥有一个模块类– 根模块(root module)
,通常命名为 AppModule
。
装饰器(Decorators)
是用来修改 javascript 类的功能的,angular 拥有很多装饰器来将元数据附加到类,以便让我们清楚该类的意义和他们如何运行。了解更多关于装饰器的知识。
NgModule
是一个装饰器函数,它接受一个元数据对象,该对象是这个模块类的描述,最主要的属性如下:
declarations
- 属于该模块类的视图类(view classes)
,angular 拥有三种视图类:组件(component)、指令(directives) 和 管道(pipes)。exports
- 在其他模块的组件模板(component templates)
中可见和可用的 declarations 的子集。import
- 在本模块声明(declared)
的组件模板中需要它的导出类的其他模块。providers
- 创建在本模块中全局使用的服务(services)
,它将会在整个应用的所有地方都可以被访问到。bootstrap
- 应用的主视图,在根组件中被调用,只有根模块(root module)
应该设置这个属性。
以下是一个简单的 根模块(root module)
代码:
1 | // src/app/app.module.ts |
这里
AppComponent
的export
只是一个示例,在实际开发中不需要这样子做,因为根模块没有被其他模块调用的可能性和必要。
通过 引导(bootstrapping)
根模块(root module)
来启动应用,在开发环境中你可能会在 main.ts
文件中这样子来引导 AppModule
:
1 | // src/main.ts |
Angular 模块 VS JavaScript 模块
Angular 模块的基本特征:是由 @NgModule
装饰器装饰的类。
JavaScript 同样拥有它自己的模块系统来管理 JavaScript 的对象集合。不过它和 Angular 的模块系统完全不一样而且两者并无关联。
在 JavaScript 中每个文件是一个模块而且所有的所有对象都在属于该模块的文件中被定义。模块通过关键字 export
声明那些公共对象。其他模块使用 import
声明并访问这些模块的这些公共对象。
1 | import { NgModule } from '@angular/core'; |
1 | export class AppModule { } |
这里有两种不一样的附加模块系统,你可以使用它们来制作你的程序。
Angular 库
Angular 装载着许多 JavaScript 模块,你可以把它们视作为模块库。
每个 Angular 库的名字都以 @angular
作为前缀。
你可以使用 npm 来安装和管理它们,然后使用 JavaScript 的 import
声明来使用它们的某一部分。
例如,你可以这样子来从 @angular/core
库中引用 Angular 的 Component
装饰符。
1 | import { Component } from '@angular/core'; |
你也可以使用 import
声明来引用 Angular 库中的模块。
1 | import { BrowserModule } from '@angular/platform-browser'; |
在上文实现的那个简单的根模块例子中,应用程序模块需要 BrowserModule
中的某些材料。为了能够访问该资源,我们需要像这样将它添加到 @NgModule
的元数据中的 imports
。
1 | imports: [ BrowserModule ], |
通过这种方式你能够同时使用 Angular 的模块系统和 JavaScript 的模块系统。
这两个模块系统都有使用我们常见的 import
指令和 export
指令,所以我们难免容易把这两者给搞混了。随着使用时间和经验的增长相信你的疑问会慢慢消除的。
组件(Components)
组件控制屏幕上的一部分,称为视图(view)。
举个例子,下面几个视图都是由组件所控制的:
- 应用根目录下的链接导航栏。
- hero的列表
- hero的编辑器
你要定义组件的应用逻辑–组件所做之事是为视图提供支持–通过组件类。组件类通过由属性和方法所构成的 API 来与视图进行交互。
举个例子,HeroListComponent
拥有 heroes
属性,该属性返回 heroes 数组并且该数组是从 服务(services) 中获得的。HeroListComponent
同样拥有 sekectHero()
方法,该方法在用户点击某个列表中的 hero 的时候更新 selectedHero
属性。
1 | // src/app/hero-list.component.ts(class) |
Angular 在用户操作应用程序的时候创建、更新和摧毁组件,通过使用可选组件 lifecycle hooks 提供的钩子如 ngOnInit()
,你的程序可以在生命周期内的每个时刻中采取不同的动作。
模板(Templates)
你通过定义组件的 template 来制定 视图(view) 的内容,模板是由 HTML 构成的,它告诉 Angular 如何渲染组件。
模板看起来跟通常的 HTML 只有极少的差别,以下是 HeroListComponent
的模板文件。
1 | // src/app/hero-list.component.html |
即使你看到这个模板文件也有使用像 <h2>
,<p>
这样的标签,但区别在于它使用了像 *ngFor
,(click)
,[hero]
和 <hero-detail>
这样的 Angular 模板语法。
在上面的模板文件的最后一行中,<hero-detail>
标签是一个定制标签,它代表了新的组件 HeroDetailComponent
。
HeroDetailComponent
和你一直在看着的 HeroListComponent
是不一样的组件,它呈现的是在 HeroListComponent
的 hero 列表中被用户选中的 hero 的详细信息,HeroDetailComponent
是 HeroListComponent
的子组件。
注意 <hero-detail>
是多么优雅的和其它原生标签相处在一起的。定制标签能够无缝的和原生标签出现在同一个布局中。
元数据(Metadata)
元数据告诉 angular 如何处理一个类。
回顾代码 HeroListComponent
,你可以发现这个组件只是一个类,没有明显的证据显示这个组件类是属于 angular 框架的,无论是引用或是其他什么的证据都没有。
其实为了告诉 angular 框架这是一个组件,我们需要为这个类连接上元数据。
在 Typescript 中,通过使用装饰符来连接元数据。这里是一些 HeroListComponent
的元数据。
1 | // src/app/hero-list.component.ts(metadata) |
这里我们看到装饰符 @Component
,它会识别那些立即在它之后被定义的类作为组件类。
@Component
装饰符接收一个提供 Angular 框架用来表示该组件和它的视图的信息对象作为参数。
以下是一些 @Component
可能用到的配置参数:
moduleId
:设置与模块相对的 URLS 例如templateUrl
的资源的主要地址(module,id
)。selector
:css 选择器,当它找到对应的<hero-list>
标签的时候,告诉 Angular 在这里创建和插入组件的实例。templateUrl
: 模块的模板文件的地址。providers
:该模块的服务(services)的 依赖注入提供者(dependency injection providers) 数组,这是一种方式去告诉 Angular 该模块的构造函数需要HeroService
来获取 hero 列表并显示。
@Component
中的元数据告诉 Angular 如何去构造你所指定的组件。
模板、元数据和组件共同工作来描述视图。
使用其他元数据来影响 Angular 行为的装饰符也是使用类似的方法,例如 @Injectable
,@Input
和 @Output
也是比较常见的装饰符。
构造 angular 应用你必须使用元数据来告诉 Angular 你要做什么。
数据绑定(Data binding)
在不使用框架的情况下,如果你想要为 HTML 节点增加数据和将用户操作转换为操作和更新数据。自己去编写这样的逻辑无疑使乏味、易错和噩梦般的体验。
Angular 支持数据绑定,一种协调部分模板和部分组件的机制。在模板中添加绑定标记来告诉 Angular 如何将两部分连接起来。
如图所示,有四种数据绑定的语法。每种语法代表着不同的数据流方向。
HeroListComponent
例子的模板文件中展示了其中三种。
1 | // src/app/hero-list.component.html(binding) |
- 表示从组件中读取
hero.name
属性来 插值 到语法所在的位置中。 [hero]
表示 属性绑定,传递组件HeroListComponent
的hero
属性到子组件HeroDetailComponent
中。(click)
是 事件绑定,在用户点击该元素的时候调用组件的 selectHero 方法。
双向数据绑定 是第四种也是很重要的一种绑定语法,它通过一个符号来实现了属性和事件绑定,使用 ngModel
指令,以下是一个例子。
1 | // src/app/hero-detail.component.html(ngModel) |
在双向绑定中,来自组件的属性数据像属性绑定一样流向该 input 框,当用户修改了 input 框的数据时,该改变会触发组件的该属性设置为最新的值,这时候的表现就是事件绑定了。
Angular 在每一个 Javascript 的事件周期中遍历所有数据绑定事件,无论是根组件还是各个子组件的数据绑定。
数据绑定在模板和组件的通讯中扮演着重要的角色。
数据绑定在父子组件的通讯中同样很重要。
指令(Directives)
Angular 的模板是动态的,Angular 根据指令(directives)的说明来将模板转换为 DOM。
指令是被 @Directive
修饰符修饰的类,组件是带有指令的模板。@Component
修饰符实际上是 @Directive
指令加上了面向模板特征的功能延生。
组件从技术角度上来看其实是一个指令,由于组件对于 Angular 应用来说十分重要而独特,我们在这篇构造概览中将它从指令的概念中抽离出来讲。
存在其他两种类型的指令:结构指令和属性指令。
他们通常作为标签的属性出现在标签中,有时候以名字出现,更多时候是作为分配和绑定的目标。
结构指令通过增加、移除和替代 DOM 元素来该表布局。
以下例子采用了两个内建的构造指令:
1 | // /src/app/hero-list.component.html(structural) |
*ngFor
告诉 Angular 根据 heroes 数组中的每个 hero 数据遍历生成<li>
元素。*ngIf
如果 selectedHero 为真时引入HeroDetail
组件。
注意 指令改变存在元素的表现或者行为,他们看起来就像普通的 HTML 属性。
代表双向数据绑定的 ngModel
指令就是属性指令的一个例子,它通过设置它显示的属性值和响应它的变更事件来修改一个现有元素(一般是 <input>
)的表现。
1 | // scr/app/hero-detail.component.html(ngModel) |
Angular 还有其他几个结构指令(例如 ngSwitch
)和影响 DOM 元素或者组件方面的指令(如 ngStyle
和 ngClass
)。
当然,你也可以自己创作指令,像 HeroListComponent
这样的组件便是自制指令的一种。
服务(Services)
服务(services)是一个广阔的类别,它涵盖了你的应用程序所需要的任何数值、函数或者功能。
几乎任何东西都能作为服务,服务通常功能明确,业务范围涵盖范围较小。它需要做一些具体的事情并将它做好。
以下是一些例子:
- 记录服务
- 数据服务
- 消息总线
- 税务计算器
- 应用配置
- Angular 的服务没有什么特别之处,没有特定的 Angular 服务定义,也不需在哪个地方去注册服务。
然而,服务是任何 Angular 应用程序的基础,组件是服务的大消费者。
下面是一个显示浏览器 log 的服务类的例子:
1 | // src/app/logger.component.ts(class) |
下面是一个使用 Promise 来更新 heroes 的 HeroService 服务。它依赖于 Logger
服务和另一个处理服务器通讯的 BackendService
服务。
1 | // src/app/hero.service.ts(class) |
服务无处不在。
组件需要遵循以下原则:他们不负责从服务器去更新数据,或者在浏览器显示 log。他们将这些任务委托给服务处理。
组件应解决之事是用户体验,别无二事。它在视图(模板渲染)和应用逻辑(通常与概念模型有关)之间斡旋。一个好的组件类只负责显示属性和数据绑定,其他事务应交给服务来解决。
Angular 不会强制你执行这些原则,不过希望你不会写出 3000 行代码那么长的组件文件。
Angular 可以帮助你遵循这些原则,方便你将应用程序逻辑转化为服务,并通过依赖注入是这些服务可用于组件。
依赖注入(Dependency injection)
依赖注入是一种方法,它将所需的类的新实例进行注入。大部分依赖是服务类,Angular 使用依赖注入来提供他们所需的服务的实例。
Angular 通过查看该组件的构造函数来判断组件需要哪些服务。例如以下 HeroListComponent
组件需要 HeroService
:
1 | // src/app/hero-list-component.ts(constructor) |
在 Angular 创建组件的时候,它首先会向 依赖注射器(injector) 请求组件所需的服务。
依赖注射器负责维护存放服务实例的容器,它会在被调用之前提前创建服务实例。如果被请求的服务实例不在容器中时,依赖注射器会在返回该服务实例之前创建一个该实例并添加到容器中。当所有被请求服务都解决并按要求返回之后,Angular 可以使用这些服务实例作为参数调用组件的构造函数。这就是依赖注入的工作原理。
HeroService 的注入过程大概如下图所示:
如果依赖注射器中没有 HeroService
的实例,它怎么知道创建这个实例的方法呢?
简单来说,你需要提前在 provider 中注册 HeroService
作为服务依赖,provider 可以创建或者返回服务,通常是服务类本身。
你可以在 模块 或者 组件 中注册 provider
。
通常我们会在根模块中进行注册,这样的话,某个服务的同一实例能够在所有地方都被访问到。
1 | // src/app/app.module.ts(module providers) |
或者我们可以在组件的粒度上使用 @Component
装饰符上的元数据来注册 providers 。
1 | // src/app/hero-list.component.ts(component providers) |
在组件上注册 providers 意味着你在每个组件的实例上都会新创建一个服务的实例。
以下是关于依赖注入需要记住的几点:
- 依赖注入在 Angular 应用中使用广泛,几乎无处不在使用。
- 依赖注射器的主要机制是:
- 依赖注射器维护存放它所创建的服务实例的容器
- 依赖注射器根据 provides 所提供的信息创建服务实例
- provider 是创建服务实例的谱单
- 记得注册依赖注射器的 providers