1-Angular入门 1.1-开发环境的搭建 安装最新版本的NodeJS
具体安装教程可参照 https://www.cnblogs.com/zhouyu2017/p/6485265.html
设置镜像
在命令窗口中进行操作:
或者设置淘宝镜像:npm config set registry https://registry.npm.taobao.org/
查看镜像地址:npm config get registry
安装Angular Cli
安装脚手架工具:npm install -g @angular/cli
如果npm安装脚手架工具失败,可先安装cnpm淘宝镜像 npm install –g cnpm –registry=https://[registry.npm.taobao.org](http://registry.npm.taobao.org/)
再使用cnpm安装脚手架工具 cnpm install -g @angular/cli
验证是否安装成功:ng version
安装Visual Studio Code
具体安装教程可参照 https://jingyan.baidu.com/article/59703552b622b78fc007401b.html
安装完成后添加Angular v8 Snippets插件来支持Angular语法;安装Chinese(Simplified) Language Pack for Visual Studio Code插件来汉化Visual Studio Code;安装jslint插件来保持代码风格的一致性。
插件具体安装教程可参照 https://jingyan.baidu.com/article/90808022029213fd91c80f15.html
如果网络环境受到限制无法直接安装,可采用离线安装,具体的操作教程可参考:https://blog.csdn.net/u012814856/article/details/80684376
1.2-新建项目 新建项目
找到你要创建项目的目录,使用“ng new 项目名称
”创建一个Angular项目(如果要创建带路由的项目,则使用“ng new 项目名 --routing
”来创建项目)
进入刚刚创建的项目的目录下,添加项目依赖:npm install 或 cnpm install
启动项目ng serve
(如果使用代理proxy.conf.json,则使用“npm start”来启动项目,此时需要在package,json中做相关配置:“start” : ”ng serve --proxy-config proxy.conf.json”
,)
新建组件
在app目录下新建components
文件夹
新建组件,在命令框中使用命令 “ng g component components/组件名”
创建新组件。创建之后要在app.module.ts
中引入该组件
新建文件夹、各种文件可直接在相应的目录下右击创建
使用ng-zorro
的组件
需要安装ng-zorro-antd相关的依赖,具体教程参见 https://ng.ant.design/docs/getting-started/zh
1.3-常见命令 查看nodejs的版本 node –v
查看npm的版本 npm –v
查看Cli脚手架的版本信息 ng version
设置npm前缀 npm config set prefix "D:\Develop\nodejs\node_global"
(具体路径根据实际情况来定)
查看npm前缀 npm config get prefix
设置npm缓存 npm config set cache "D:\Develop\nodejs\node_cache"
(具体路径根据实际情况来定)
查看npm缓存 npm config get cache
查看npm配置信息列表 npm config list
创建一个Angular项目 ng new 项目名称
创建一个带有路由的Angular项目 ng new 项目名称 --routing
退出某个目录 cd ..
进入某个目录 cd Program Files\Microsoft VS Code\bin (具体路径根据实际情况来定)
转换盘符 d: (具体切换到那个盘根据实际情况来定)
初始化项目依赖 npm install 或者 cnpm install
创建一个组件 ng g component components/组件名
(components是自己新建的文件夹,可根据实际情况改变)
启动项目 ng serve 或 ng serve --open
使用代理时启动项目 npm start
自动完成 ng-zorro-antd 的初始化配置 ng add ng-zorro-antd
1.4-参考网站 Angular 4.x 修仙之路 https://segmentfault.com/a/1190000008754631
Angular中文官网:https://angular.cn/
npm install Angular依赖包下载https://www.npmjs.com/package/package
ng zorro网址https://ng.ant.design/docs/introduce/zh
2-Angular基础 2.1-架构概览 Angular 是一个用 HTML 和 TypeScript 构建客户端应用的平台与框架。 Angular 本身就是用 TypeScript 写成的。它将核心功能和可选功能作为一组 TypeScript 库进行实现,你可以把它们导入你的应用中。
Angular 的基本构造块是 NgModule ,它为组件 提供了编译的上下文环境。 NgModule 会把相关的代码收集到一些功能集中。Angular 应用就是由一组 NgModule 定义出的。 应用至少会有一个用于引导应用的根模块 ,通常还会有很多特性模块 。
组件定义视图 。视图是一组可见的屏幕元素,Angular 可以根据你的程序逻辑和数据来选择和修改它们。 每个应用都至少有一个根组件。 组件使用服务 。服务会提供那些与视图不直接相关的功能。服务提供商可以作为依赖 被注入 到组件中, 这能让你的代码更加模块化、更加可复用、更加高效。 组件和服务都是简单的类,这些类使用装饰器 来标出它们的类型,并提供元数据以告知 Angular 该如何使用它们。
组件类的元数据将组件类和一个用来定义视图的模板 关联起来。 模板把普通的 HTML 和 Angular 指令 与绑定标记(markup) 组合起来,这样 Angular 就可以在呈现 HTML 之前先修改这些 HTML。 服务类的元数据提供了一些信息,Angular 要用这些信息来让组件可以通过依赖注入(DI) 使用该服务。 应用的组件通常会定义很多视图,并进行分级组织。 Angular 提供了 Router
服务来帮助你定义视图之间的导航路径。 路由器提供了先进的浏览器内导航功能。
2.2-基本概念 2.2.1-模块 Angular 定义了 NgModule
,它和 JavaScript(ES2015) 的模块不同而且有一定的互补性。 NgModule 为一个组件集声明了编译的上下文环境,它专注于某个应用领域、某个工作流或一组紧密相关的能力。 NgModule 可以将其组件和一组相关代码(如服务)关联起来,形成功能单元。
每个 Angular 应用都有一个根模块 ,通常命名为 AppModule
。根模块提供了用来启动应用的引导机制。 一个应用通常会包含很多功能模块。
像 JavaScript 模块一样,NgModule 也可以从其它 NgModule 中导入功能,并允许导出它们自己的功能供其它 NgModule 使用。 比如,要在你的应用中使用路由器(Router)服务,就要导入 Router
这个 NgModule。
把你的代码组织成一些清晰的功能模块,可以帮助管理复杂应用的开发工作并实现可复用性设计。 另外,这项技术还能让你获得惰性加载 (也就是按需加载模块)的优点,以尽可能减小启动时需要加载的代码体积。
2.2.2-组件 每个 Angular 应用都至少有一个组件,也就是根组件 ,它会把组件树和页面中的 DOM 连接起来。 每个组件都会定义一个类,其中包含应用的数据和逻辑,并与一个 HTML 模板 相关联,该模板定义了一个供目标环境下显示的视图。
@Component()
装饰器表明紧随它的那个类是一个组件,并提供模板和该组件专属的元数据。
2.2.3-模板、指令和数据绑定 模板会把 HTML 和 Angular 的标记(markup)组合起来,这些标记可以在 HTML 元素显示出来之前修改它们。 模板中的指令 会提供程序逻辑,而绑定标记 会把你应用中的数据和 DOM 连接在一起。 有两种类型的数据绑定:
事件绑定 让你的应用可以通过更新应用的数据来响应目标环境下的用户输入。属性绑定 让你将从应用数据中计算出来的值插入到 HTML 中。在视图显示出来之前,Angular 会先根据你的应用数据和逻辑来运行模板中的指令并解析绑定表达式,以修改 HTML 元素和 DOM。 Angular 支持双向数据绑定 ,这意味着 DOM 中发生的变化(比如用户的选择)同样可以反映回你的程序数据中。
你的模板也可以用管道 转换要显示的值以增强用户体验。比如,可以使用管道来显示适合用户所在地区的日期和货币格式。 Angular 为一些通用的转换提供了预定义管道,你还可以定义自己的管道。
2.2.4-服务与依赖注入 对于与特定视图无关并希望跨组件共享的数据或逻辑,可以创建服务 类。 服务类的定义通常紧跟在 “@Injectable()” 装饰器之后。该装饰器提供的元数据可以让你的服务作为依赖被注入到 客户组件中。
依赖注入 (或 DI)让你可以保持组件类的精简和高效。有了 DI,组件就不用从服务器获取数据、验证用户输入或直接把日志写到控制台,而是会把这些任务委托给服务。
2.2.5-路由 Angular 的 Router
模块提供了一个服务,它可以让你定义在应用的各个不同状态和视图层次结构之间导航时要使用的路径。 它的工作模型基于人们熟知的浏览器导航约定:
在地址栏输入 URL,浏览器就会导航到相应的页面。 在页面中点击链接,浏览器就会导航到一个新页面。 点击浏览器的前进和后退按钮,浏览器就会在你的浏览历史中向前或向后导航。 不过路由器会把类似 URL 的路径映射到视图而不是页面。 当用户执行一个动作时(比如点击链接),本应该在浏览器中加载一个新页面,但是路由器拦截了浏览器的这个行为,并显示或隐藏一个视图层次结构。
如果路由器认为当前的应用状态需要某些特定的功能,而定义此功能的模块尚未加载,路由器就会按需惰性加载 此模块。
路由器会根据你应用中的导航规则和数据状态来拦截 URL。 当用户点击按钮、选择下拉框或收到其它任何来源的输入时,你可以导航到一个新视图。 路由器会在浏览器的历史日志中记录这个动作,所以前进和后退按钮也能正常工作。
要定义导航规则,你就要把导航路径 和你的组件关联起来。 路径(path)使用类似 URL 的语法来和程序数据整合在一起,就像模板语法会把你的视图和程序数据整合起来一样。 然后你就可以用程序逻辑来决定要显示或隐藏哪些视图,以根据你制定的访问规则对用户的输入做出响应。
注: 这些基础部门之间是如何关联的呢?
组件和模板共同定义了 Angular 的视图。组件类上的装饰器为其添加了元数据,其中包括指向相关模板的指针。 组件模板中的指令和绑定标记会根据程序数据和程序逻辑修改这些视图。 依赖注入器会为组件提供一些服务,比如路由器服务就能让你定义如何在视图之间导航。 2.3-模块NgModule NgModule简介 Angular 应用是模块化的,它拥有自己的模块化系统,称作 NgModule 。 一个 NgModule 就是一个容器,用于存放一些内聚的代码块,这些代码块专注于某个应用领域、某个工作流或一组紧密相关的功能。 它可以包含一些组件、服务提供商或其它代码文件,其作用域由包含它们的 NgModule 定义。 它还可以导入一些由其它模块中导出的功能,并导出一些指定的功能供其它 NgModule 使用。
每个 Angular 应用都至少有一个 NgModule 类,也就是根模块 ,它习惯上命名为 AppModule
,并位于一个名叫 app.module.ts
的文件中。引导 这个根模块就可以启动你的应用。
虽然小型的应用可能只有一个 NgModule,不过大多数应用都会有很多特性模块 。应用的根模块 之所以叫根模块,是因为它可以包含任意深度的层次化子模块。
@NgModule 元数据
NgModule 是一个带有 @NgModule()
装饰器的类。@NgModule()
装饰器是一个函数,它接受一个元数据对象,该对象的属性用来描述这个模块。其中最重要的属性如下。
declarations
(可声明对象表) —— 那些属于本 NgModule 的组件 、指令 、管道 。
exports
(导出表) —— 那些能在其它模块的组件模板 中使用的可声明对象的子集。
imports
(导入表) —— 那些导出了本 模块中的组件模板所需的类的其它模块。
providers
—— 本模块向全局服务中贡献的那些服务 的创建器。 这些服务能被本应用中的任何部分使用。(你也可以在组件级别指定服务提供商,这通常是首选方式。)
bootstrap
—— 应用的主视图,称为根组件 。它是应用中所有其它视图的宿主。只有根模块 才应该设置这个 bootstrap
属性。
一个简单的根 NgModule 定义:
import { NgModule } from '@angular/core' ;import { BrowserModule } from '@angular/platform-browser' ;@NgModule ({ imports : [ BrowserModule ], providers : [ Logger ], declarations : [ AppComponent ], exports : [ AppComponent ], bootstrap : [ AppComponent ] }) export class AppModule { }
NgModule 和组件
NgModule 为其中的组件提供了一个编译上下文环境 。根模块总会有一个根组件,并在引导期间创建它。 但是,任何模块都能包含任意数量的其它组件,这些组件可以通过路由器加载,也可以通过模板创建。那些属于这个 NgModule 的组件会共享同一个编译上下文环境。
组件及其模板共同定义视图 。组件还可以包含视图层次结构 ,它能让你定义任意复杂的屏幕区域,可以将其作为一个整体进行创建、修改和销毁。 一个视图层次结构中可以混合使用由不同 NgModule 中的组件定义的视图。
Angular 自带的库
Angular 会作为一组 JavaScript 模块进行加载,你可以把它们看成库模块。每个 Angular 库的名称都带有 @angular
前缀。 使用 npm
包管理器安装 Angular 的库,并使用 JavaScript 的 import
语句导入其中的各个部分。
例如,像下面这样,从 @angular/core
库中导入 Angular 的 Component
装饰器
import { Component } from '@angular/core'
2.4-组件 组件控制屏幕上被称为 视图*的一小片区域。比如,教程 中的下列视图都是由一个个组件所定义和控制的:
你在类中定义组件的应用逻辑,为视图提供支持。 组件通过一些由属性和方法组成的 API 与视图交互。
比如,HeroListComponent
中有一个 名为heroes
的属性,它储存着一个数组的英雄数据。 HeroListComponent
还有一个 selectHero()
方法,当用户从列表中选择一个英雄时,它会设置 selectedHero
属性的值。 该组件会从服务获取英雄列表,它是一个 TypeScript 的构造器参数型属性 。本服务通过依赖注入系统提供给该组件。
export class HeroListComponent implements OnInit { heroes : Hero[]; selectedHero: Hero; constructor (private service: HeroService ) { } ngOnInit ( ) { this .heroes = this .service.getHeroes(); } selectHero (hero: Hero ) { this .selectedHero = hero; } }
当用户在应用中穿行时,Angular 就会创建、更新、销毁一些组件。 你的应用可以通过一些可选的生命周期钩子 (比如ngOnInit()
)来在每个特定的时机采取行动。
组件的元数据
@Component
装饰器会指出紧随其后的那个类是个组件类,并为其指定元数据。 在下面的范例代码中,你可以看到 HeroListComponent
只是一个普通类,完全没有 Angular 特有的标记或语法。 直到给它加上了 @Component
装饰器,它才变成了组件。
组件的元数据告诉 Angular 到哪里获取它需要的主要构造块,以创建和展示这个组件及其视图。 具体来说,它把一个模板 (无论是直接内联在代码中还是引用的外部文件)和该组件关联起来。 该组件及其模板,共同描述了一个视图 。
除了包含或指向模板之外,@Component
的元数据还会配置要如何在 HTML 中引用该组件,以及该组件需要哪些服务等等。
下面的例子中就是 HeroListComponent
的基础元数据:
@Component ({ selector : 'app-hero-list' , templateUrl : './hero-list.component.html' , providers : [ HeroService ] }) export class HeroListComponent implements OnInit {}
这个例子展示了一些最常用的 @Component
配置选项:
selector
:是一个 CSS 选择器,它会告诉 Angular,一旦在模板 HTML 中找到了这个选择器对应的标签,就创建并插入该组件的一个实例。 比如,如果应用的 HTML 中包含 ``,Angular 就会在这些标签中插入一个 HeroListComponent
实例的视图。templateUrl
:该组件的 HTML 模板文件相对于这个组件文件的地址。 另外,你还可以用 template
属性的值来提供内联的 HTML 模板。 这个模板定义了该组件的宿主视图 。providers
:当前组件所需的服务提供商 的一个数组。在这个例子中,它告诉 Angular 该如何提供一个 HeroService
实例,以获取要显示的英雄列表。模板与视图
你要通过组件的配套模板来定义其视图。模板就是一种 HTML,它会告诉 Angular 如何渲染该组件。
视图通常会分层次进行组织,让你能以 UI 分区或页面为单位进行修改、显示或隐藏。 与组件直接关联的模板会定义该组件的宿主视图 。该组件还可以定义一个带层次结构的视图 ,它包含一些内嵌的视图 作为其它组件的宿主。
带层次结构的视图可以包含同一模块(NgModule)中组件的视图,也可以(而且经常会)包含其它模块中定义的组件的视图。
模板语法
模板很像标准的 HTML,但是它还包含 Angular 的模板语法 ,这些模板语法可以根据你的应用逻辑、应用状态和 DOM 数据来修改这些 HTML。 你的模板可以使用数据绑定 来协调应用和 DOM 中的数据,使用管道 在显示出来之前对其进行转换,使用指令 来把程序逻辑应用到要显示的内容上。
比如,下面是本教程中 HeroListComponent
的模板:
<h2 > Hero List</h2 > <p > <i > Pick a hero from the list</i > </p > <ul > <li *ngFor ="let hero of heroes" (click )="selectHero(hero)" > {{hero.name}} </li > </ul > <app-hero-detail *ngIf ="selectedHero" [hero ]="selectedHero" > </app-hero-detail >
这个模板使用了典型的 HTML 元素,比如 和
,还包括一些 Angular 的模板语法元素,如 *ngFor
,{{hero.name}}
,click
、[hero]
和 ``。这些模板语法元素告诉 Angular 该如何根据程序逻辑和数据在屏幕上渲染 HTML。
*ngFor
指令告诉 Angular 在一个列表上进行迭代。{{hero.name}}
、(click)
和 [hero]
把程序数据绑定到及绑定回 DOM,以响应用户的输入。更多内容参见稍后的数据绑定 部分。模板中的 `` 标签是一个代表新组件 HeroDetailComponent
的元素。 HeroDetailComponent
(代码略)定义了 HeroListComponent
的英雄详情子视图。 注意观察像这样的自定义组件是如何与原生 HTML 元素无缝的混合在一起的。 数据绑定
如果没有框架,你就要自己负责把数据值推送到 HTML 控件中,并把来自用户的响应转换成动作和对值的更新。 手动写这种数据推拉逻辑会很枯燥、容易出错,难以阅读 —— 有前端 JavaScript 开发经验的程序员一定深有体会。
Angular 支持双向数据绑定 ,这是一种对模板中的各个部件与组件中的各个部件进行协调的机制。 往模板 HTML 中添加绑定标记可以告诉 Angular 该如何连接它们。
下图显示了数据绑定标记的四种形式。每种形式都有一个方向 —— 从组件到 DOM、从 DOM 到组件或双向。
`HeroListComponent` 模板中的例子使用了其中的三种形式:
<li > {{hero.name}}</li > <app-hero-detail [hero ]="selectedHero" > </app-hero-detail > <li (click )="selectHero(hero)" > </li >
{{hero.name}}
插值表达式 在 `` 标签中显示组件的 hero.name
属性的值。[hero]
属性绑定 把父组件 HeroListComponent
的 selectedHero
的值传到子组件 HeroDetailComponent
的 hero
属性中。当用户点击某个英雄的名字时,(click)
事件绑定 会调用组件的 selectHero
方法。 双向数据绑定 (主要用于模板驱动表单 中),它会把属性绑定和事件绑定组合成一种单独的写法。
`HeroDetailComponent` 模板中的例子通过 `ngModel` 指令使用了双向数据绑定:
<input [(ngModel )]="hero.name" >
在双向绑定中,数据属性值通过属性绑定从组件流到输入框。用户的修改通过事件绑定流回组件,把属性值设置为最新的值。
Angular 在每个 JavaScript 事件循环中处理所有的 数据绑定,它会从组件树的根部开始,递归处理全部子组件。
数据绑定在模板及其组件之间的通讯中扮演了非常重要的角色,它对于父组件和子组件之间的通讯也同样重要。
管道
Angular 的管道可以让你在模板中声明显示值的转换逻辑。 带有 @Pipe
装饰器的类中会定义一个转换函数,用来把输入值转换成供视图显示用的输出值。
Angular 自带了很多管道,比如 date 管道和 currency 管道,完整的列表参见 Pipes API 列表 。你也可以自己定义一些新管道。
要在 HTML 模板中指定值的转换方式,请使用 管道操作符 (|) 。
{{interpolated_value | pipe_name}}
你可以把管道串联起来,把一个管道函数的输出送给另一个管道函数进行转换。 管道还能接收一些参数,来控制它该如何进行转换。
比如,你可以把要使用的日期格式传给 date
管道:
<p > Today is {{today | date}}</p > <p > The date is {{today | date:'fullDate'}}</p > <p > The time is {{today | date:'shortTime'}}</p >
指令
Angular 的模板是动态的 。当 Angular 渲染它们的时候,会根据指令 给出的指示对 DOM 进行转换。 指令就是一个带有 @Directive()
装饰器的类。
组件从技术角度上说就是一个指令,但是由于组件对 Angular 应用来说非常独特、非常重要,因此 Angular 专门定义了 @Component()
装饰器,它使用一些面向模板的特性扩展了 @Directive()
装饰器。
除组件外,还有两种指令:结构型指令 和属性型指令 。 Angular 本身定义了一系列这两种类型的指令,你也可以使用 @Directive()
装饰器来定义自己的指令。
像组件一样,指令的元数据把它所装饰的指令类和一个 selector
关联起来,selector
用来把该指令插入到 HTML 中。 在模板中,指令通常作为属性出现在元素标签上,可能仅仅作为名字出现,也可能作为赋值目标或绑定目标出现。
结构型指令
结构型指令 通过添加、移除或替换 DOM 元素来修改布局。
这个范例模板使用了两个内置的结构型指令来为要渲染的视图添加程序逻辑:
<li *ngFor ="let hero of heroes" > </li > <app-hero-detail *ngIf ="selectedHero" > </app-hero-detail >
*ngFor
是一个迭代器,它要求 Angular 为 heroes
列表中的每个英雄渲染出一个 ``。*ngIf
是个条件语句,只有当选中的英雄存在时,它才会包含 HeroDetail
组件。属性型指令
属性型指令 会修改现有元素的外观或行为。 在模板中,它们看起来就像普通的 HTML 属性一样,因此得名“属性型指令”。
ngModel
指令就是属性型指令的一个例子,它实现了双向数据绑定。 ngModel
修改现有元素(一般是 ``)的行为:设置其显示属性值,并响应 change 事件。
<input [(ngModel )]="hero.name" >
Angular 还有很多预定义指令既不修改布局结构(比如 ngSwitch ),也不修改 DOM 元素和组件的样子(比如 ngStyle 和 ngClass )。
2.5-服务与依赖注入 服务 是一个广义的概念,它包括应用所需的任何值、函数或特性。狭义的服务是一个明确定义了用途的类。它应该做一些具体的事,并做好。
Angular 把组件和服务区分开,以提高模块性和复用性。 通过把组件中和视图有关的功能与其他类型的处理分离开,你可以让组件类更加精简、高效。
理想情况下,组件的工作只管用户体验,而不用顾及其它。 它应该提供用于数据绑定的属性和方法,以便作为视图(由模板渲染)和应用逻辑(通常包含一些模型 的概念)的中介者。
组件应该把诸如从服务器获取数据、验证用户输入或直接往控制台中写日志等工作委托给各种服务。通过把各种处理任务定义到可注入的服务类中,你可以让它被任何组件使用。 通过在不同的环境中注入同一种服务的不同提供商,你还可以让你的应用更具适应性。
Angular 不会强迫 你遵循这些原则。Angular 只会通过依赖注入 来帮你更容易地将应用逻辑分解为服务,并让这些服务可用于各个组件中。
服务案例
下面是一个服务类的范例,用于把日志记录到浏览器的控制台:
export class Logger { log (msg: any ) { console .log(msg); } error (msg: any ) { console .error(msg); } warn (msg: any ) { console .warn(msg); } }
服务也可以依赖其它服务。比如,这里的 HeroService
就依赖于 Logger
服务,它还用 BackendService
来获取英雄数据。BackendService
还可能再转而依赖 HttpClient
服务来从服务器异步获取英雄列表。
export class HeroService { private heroes: Hero[] = []; constructor ( private backend: BackendService, private logger: Logger ) { } getHeroes ( ) { this .backend.getAll(Hero).then( (heroes: Hero[] ) => { this .logger.log(`Fetched ${heroes.length} heroes.` ); this .heroes.push(...heroes); }); return this .heroes; } }
依赖注入
DI 被融入 Angular 框架中,用于在任何地方给新建的组件提供服务或所需的其它东西。 组件是服务的消费者,也就是说,你可以把一个服务注入 到组件中,让组件类得以访问该服务类。
在 Angular 中,要把一个类定义为服务,就要用 @Injectable()
装饰器来提供元数据,以便让 Angular 可以把它作为依赖 注入到组件中。 同样,也要使用 @Injectable()
装饰器来表明一个组件或其它类(比如另一个服务、管道或 NgModule)拥有 一个依赖。
注入器 是主要的机制。Angular 会在启动过程中为你创建全应用级注入器以及所需的其它注入器。你不用自己创建注入器。该注入器会创建依赖、维护一个容器 来管理这些依赖,并尽可能复用它们。 提供商 是一个对象,用来告诉注入器应该如何获取或创建依赖。你的应用中所需的任何依赖,都必须使用该应用的注入器来注册一个提供商,以便注入器可以使用这个提供商来创建新实例。 对于服务,该提供商通常就是服务类本身。
当 Angular 创建组件类的新实例时,它会通过查看该组件类的构造函数,来决定该组件依赖哪些服务或其它依赖项。 比如 HeroListComponent
的构造函数中需要 HeroService
:
constructor (private service: HeroService ) { }
当 Angular 发现某个组件依赖某个服务时,它会首先检查是否该注入器中已经有了那个服务的任何现有实例。如果所请求的服务尚不存在,注入器就会使用以前注册的服务提供商来制作一个,并把它加入注入器中,然后把该服务返回给 Angular。
当所有请求的服务已解析并返回时,Angular 可以用这些服务实例为参数,调用该组件的构造函数。
HeroService
的注入过程如下所示:
提供服务
对于要用到的任何服务,你必须至少注册一个提供商 。服务可以在自己的元数据中把自己注册为提供商,这样可以让自己随处可用。或者,你也可以为特定的模块或组件注册提供商。要注册提供商,就要在服务的 @Injectable()
装饰器中提供它的元数据,或者在@NgModule()
或 @Component()
的元数据中。
默认情况下,Angular CLI 的 ng generate service
命令会在 @Injectable()
装饰器中提供元数据来把它注册到根注入器中。本教程就用这种方法注册了 HeroService 的提供商: content_copy@Injectable({ providedIn: 'root', })
当你在根一级提供服务时,Angular 会为 HeroService 创建一个单一的共享实例,并且把它注入到任何想要它的类中。这种在 @Injectable
元数据中注册提供商的方式还让 Angular 能够通过移除那些从未被用过的服务来优化大小。
当你使用特定的 NgModule 注册提供商时,该服务的同一个实例将会对该 NgModule 中的所有组件可用。要想在这一层注册,请用 @NgModule()
装饰器中的 providers
属性: content_copy@NgModule({ providers: [ BackendService, Logger ], ... })
2.6-工具与技巧 响应式编程工具 客户端与服务器的交互工具 HTTP :用 HTTP 客户端与服务器通讯,以获取数据、保存数据或执行服务端动作。服务端渲染 :Angular Universal 会通过服务端渲染(SSR)技术在服务器上生成静态的应用页面。 这让你可以在服务器上运行 Angular 应用,以提升性能并在手机或低功耗设备上快速显示首屏,并为 Web 爬虫提供帮助(SEO)。Service Worker :借助 Service Worker 来减轻对网络的依赖,你可以显著提升用户体验。特定领域的库 动画 :使用 Angular 的动画库,你可以让组件支持动画行为,而不用深入了解动画技术或 CSS。Forms :通过基于 HTML 的验证和脏数据检查,来支持复杂的数据输入场景。为开发周期提供支持 编译 :Angular 为开发环境提供了 JIT(即时)编译方式,为生产环境提供了 AOT(预先)编译方式。
测试平台 :对应用的各个部件运行单元测试,让它们好像在和 Angular 框架交互一样。
国际化 :Angular 的国际化工具可以帮助你让应用可用于多种语言中。
安全指南 :学习 Angular 对常见 Web 应用的弱点和工具(比如跨站脚本攻击)提供的内置防护措施。
环境搭建、构建与开发配置 CLI 命令参考手册 :Angular CLI 是一个命令行工具,你可以使用它来创建项目、生成应用及库代码,还能执行很多开发任务,比如测试、打包和发布。工作区与文件结构 :理解 Angular 工作区与项目文件夹的结构。npm 包 :Angular 框架、Angular CLI 和 Angular 应用中用到的组件都是用 npm 打包的,并通过 npm 注册服务器进行发布。Angular CLI 会创建一个默认的 package.json
文件,它会指定一组初始的包,它们可以一起使用,共同支持很多常见的应用场景。TypeScript 配置 :TypeScript 是 Angular 应用开发的主要语言。浏览器支持 :学习如何让你的应用能和各种浏览器兼容。构建与运行 :学习为项目定义不同的构建和代理服务器设置的配置方式,比如开发、预生产和生产。部署 :学习把你的 Angular 应用发布到远端服务器的技巧。3-组件与模板 3.1-显示数据 使用插件表达式显示组件属性
要显示组件的属性,最简单的方式就是通过插值表达式 (interpolation) 来绑定属性名。 要使用插值表达式,就把属性名包裹在双花括号里放进视图模板,如 {{myHero}}
。
使用 CLI 命令 ng new displaying-data
创建一个工作空间和一个名叫 displaying-data
的应用。
删除 app.component.html
文件,这个范例中不再需要它了。
然后,到 app.component.ts
文件中修改组件的模板和代码。
修改完之后,它应该是这样的:
import { Component } from '@angular/core' ; @Component ({ selector : 'app-root' , template : ` <h1>{{title}}</h1> <h2>My favorite hero is: {{myHero}}</h2> ` }) export class AppComponent { title = 'Tour of Heroes' ; myHero = 'Windstorm' ; }
再把两个属性 title
和 myHero
添加到之前空白的组件中。
修改完的模板会使用双花括号形式的插值表达式来显示这两个模板属性:
template: ` <h1>{{title}}</h1> <h2>My favorite hero is: {{myHero}}</h2> `
Angular 自动从组件中提取 title
和 myHero
属性的值,并且把这些值插入浏览器中。当这些属性发生变化时,Angular 就会自动刷新显示。
注意 :你没有调用 new 来创建 AppComponent
类的实例,是 Angular 替你创建了它。那么它是如何创建的呢?
注意 :@Component
装饰器中指定的 CSS 选择器 selector
,它指定了一个叫 `` 的元素。 该元素是 index.html
文件里的一个占位符。
src/index.html
<body > <app-root > </app-root > </body >
当你通过 main.ts
中的 AppComponent
类启动时,Angular 在 index.html
中查找一个 元素, 然后实例化一个 `AppComponent`,并将其渲染到
标签中。
运行应用。它应该显示出标题和英雄名:
内联模板还是模板文件?
你可以在两种地方存放组件模板。 你可以使用 template
属性把它定义为内联 的,或者把模板定义在一个独立的 HTML 文件中, 再通过 @Component
装饰器中的 templateUrl
属性, 在组件元数据中把它链接到组件。
到底选择内联 HTML 还是独立 HTML 取决于个人喜好、具体状况和组织级策略。 上面的应用选择内联 HTML ,是因为模板很小,而且没有额外的 HTML 文件显得这个演示简单些。
无论用哪种风格,模板数据绑定在访问组件属性方面都是完全一样的。
默认情况下,Angular CLI 命令 ng generate component
在生成组件时会带有模板文件,你可以通过参数来覆盖它
使用构造函数还是变量初始化?
虽然这个例子使用了变量赋值的方式初始化组件,你还可以使用构造函数来声明和初始化属性。
export class AppComponent { title : string ; myHero: string ; constructor ( ) { this .title = 'Tour of Heroes' ; this .myHero = 'Windstorm' ; } }
使用ngFor显示数组属性
要显示一个英雄列表,先向组件中添加一个英雄名字数组,然后把 myHero
重定义为数组中的第一个名字。
export class AppComponent { title = 'Tour of Heroes' ; heroes = ['Windstorm' , 'Bombasto' , 'Magneta' , 'Tornado' ]; myHero = this .heroes[0 ]; }
接着,在模板中使用 Angular 的 ngFor
指令来显示 heroes
列表中的每一项
template: ` <h1>{{title}}</h1> <h2>My favorite hero is: {{myHero}}</h2> <p>Heroes:</p> <ul> <li *ngFor="let hero of heroes"> {{ hero }} </li> </ul> `
这个界面使用了由 和
标签组成的无序列表。元素里的 `*ngFor` 是 Angular 的“迭代”指令。 它将
元素及其子级标记为“迭代模板”:
<li *ngFor ="let hero of heroes" > {{ hero }} </li >
为数据创建一个类
应用代码直接在组件内部直接定义了数据。 作为演示还可以,但它显然不是最佳实践。
现在使用的是到了一个字符串数组的绑定。在真实的应用中,大多是到一个对象数组的绑定。
要将此绑定转换成使用对象,需要把这个英雄名字数组变成 Hero
对象数组。但首先得有一个 Hero
类。
ng generate class hero
代码如下:
export class Hero { constructor ( public id: number , public name: string ) { }}
你定义了一个类,具有一个构造函数和两个属性:id
和 name
。
它可能看上去不像是有属性的类,但它确实有,利用的是 TypeScript 提供的简写形式 —— 用构造函数的参数直接定义属性。
来看第一个参数:
public id: number,
这个简写语法做了很多:
声明了一个构造函数参数及其类型。 声明了一个同名的公共属性。 当创建该类的一个实例时,把该属性初始化为相应的参数值。 使用Hero类:
导入了 Hero
类之后,组件的 heroes
属性就可以返回一个类型化的 Hero
对象数组了
heroes = [ new Hero(1 , 'Windstorm' ), new Hero(13 , 'Bombasto' ), new Hero(15 , 'Magneta' ), new Hero(20 , 'Tornado' ) ]; myHero = this .heroes[0 ];
接着,修改模板。 现在它显示的是英雄的 id
和 name
。 要修复它,只显示英雄的 name
属性就行了。
template: ` <h1>{{title}}</h1> <h2>My favorite hero is: {{myHero.name}}</h2> <p>Heroes:</p> <ul> <li *ngFor="let hero of heroes"> {{ hero.name }} </li> </ul> `
通过ngIf进行条件显示
有时,应用需要只在特定情况下显示视图或视图的一部分。
来改一下这个例子,如果多于三位英雄,显示一条消息。
Angular 的 ngIf
指令会根据一个布尔条件来显示或移除一个元素。 来看看实际效果,把下列语句加到模板的底部:
<p *ngIf ="heroes.length > 3" > There are many heroes!</p >
双引号中的模板表达式 *ngIf="heros.length > 3"
,外观和行为很象 TypeScript 。 当组件中的英雄列表有三个以上的条目时,Angular 就会把这个段落添加到 DOM 中,于是消息显示了出来。 如果有三个或更少的条目,则 Angular 会省略这些段落,所以不显示消息。
Angular 并不是在显示和隐藏这条消息,它是在从 DOM 中添加和移除这个段落元素。 这会提高性能,特别是在一些大的项目中有条件地包含或排除一大堆带着很多数据绑定的 HTML 时。
3.2-模板语法 Angular 应用管理着用户之所见和所为,并通过 Component 类的实例(组件 )和面向用户的模板交互来实现这一点。
从使用模型-视图-控制器 (MVC) 或模型-视图-视图模型 (MVVM) 的经验中,很多开发人员都熟悉了组件和模板这两个概念。 在 Angular 中,组件扮演着控制器或视图模型的角色,模板则扮演视图的角色。
模板中的HTML
HTML 是 Angular 模板的语言。几乎所有的 HTML 语法都是有效的模板语法。 但值得注意的例外是 元素,它被禁用了,以阻止脚本注入攻击的风险。(实际上,
只是被忽略了。)
有些合法的 HTML 被用在模板中是没有意义的。、
和 `` 元素这个舞台上中并没有扮演有用的角色。剩下的所有元素基本上就都一样用了。
可以通过组件和指令来扩展模板中的 HTML 词汇。它们看上去就是新元素和属性。接下来将学习如何通过数据绑定来动态获取/设置 DOM(文档对象模型)的值。
首先看看数据绑定的第一种形式 —— 插值表达式,它展示了模板的 HTML 可以有多丰富。
插值与模板表达式
插值能让你把计算后的字符串合并到 HTML 元素标签之间和属性赋值语句内的文本中。模板表达式则是用来供你求出这些字符串的。
插值表达式
所谓 “插值” 是指将表达式嵌入到标记文本中。 默认情况下,插值表达式会用双花括号 {{`和 `}}
作为分隔符。
在下面的代码片段中,{{ currentCustomer }}
就是插值表达式的例子。
<h3 > Current customer: {{ currentCustomer }}</h3 >
插值表达式可以把计算后的字符串插入到 HTML 元素标签内的文本或对标签的属性进行赋值
<p > {{title}}</p > <div > <img src ="{{itemImageUrl}}" > </div >
在括号之间的“素材”,通常是组件属性的名字。Angular 会用组件中相应属性的字符串值,替换这个名字。 上例中,Angular 计算 title
和 itemImageUrl
属性的值,并把它们填在空白处。 首先显示粗体的应用标题,然后显示英雄的图片。
一般来说,括号间的素材是一个模板表达式 ,Angular 先对它求值 ,再把它转换成字符串 。 下列插值表达式通过把括号中的两个数字相加说明了这一点:
<p > The sum of 1 + 1 is {{1 + 1}}.</p >
这个表达式可以调用宿主组件的方法,就像下面用的 getVal()
:
<p > The sum of 1 + 1 is not {{1 + 1 + getVal()}}.</p >
Angular 对所有双花括号中的表达式求值,把求值的结果转换成字符串,并把它们跟相邻的字符串字面量连接起来。最后,把这个组合出来的插值结果赋给元素或指令的属性 。
从表面上看,你就像是在元素标签之间插入了结果并对标签的属性进行了赋值。
表达式上下文
典型的表达式上下文 就是这个组件实例 ,它是各种绑定值的来源。 在下面的代码片段中,双花括号中的 recommended
和引号中的 itemImageUrl2
所引用的都是 AppComponent
中的属性。
<h4 > {{recommended}}</h4 > <img [src ]="itemImageUrl2" >
表达式的上下文可以包括组件之外的对象。 比如模板输入变量 (let customer
)和模板引用变量 (#customerInput
)就是备选的上下文对象之一。
<ul > <li *ngFor ="let customer of customers" > {{customer.name}}</li > </ul >
<label > Type something: <input #customerInput > {{customerInput.value}} </label >
表达式中的上下文变量是由模板变量 、指令的上下文变量 (如果有)和组件的成员 叠加而成的。 如果你要引用的变量名存在于一个以上的命名空间中,那么,模板变量是最优先的,其次是指令的上下文变量,最后是组件的成员。
模板语句
模板语句 用来响应由绑定目标(如 HTML 元素、组件或指令)触发的事件 。 模板语句将在事件绑定 一节看到,它出现在 =
号右侧的引号中,就像这样:(event)="statement"
。
<button (click )="deleteHero()" > Delete hero</button >
模板语句有副作用 。 这是事件处理的关键。因为你要根据用户的输入更新应用状态。
响应事件是 Angular 中“单向数据流”的另一面。 在一次事件循环中,可以随意改变任何地方的任何东西。
和模板表达式一样,模板语句 使用的语言也像 JavaScript。 模板语句解析器和模板表达式解析器有所不同,特别之处在于它支持基本赋值 (=
) 和表达式链 (;
和 ,
)。
然而,某些 JavaScript 语法仍然是不允许的:
new
运算符自增和自减运算符:++
和 --
操作并赋值,例如 +=
和 -=
位操作符 |
和 &
模板表达式运算符 语句上下文
和表达式中一样,语句只能引用语句上下文中 —— 通常是正在绑定事件的那个组件实例 。
典型的语句上下文 就是当前组件的实例。 (click)="deleteHero()"
中的 deleteHero 就是这个数据绑定组件上的一个方法。
<button (click)="deleteHero()">Delete hero</button>
语句上下文可以引用模板自身上下文中的属性。 在下面的例子中,就把模板的 $event
对象、模板输入变量 (let hero
)和模板引用变量 (#heroForm
)传给了组件中的一个事件处理器方法。
<button (click )="onSave($event)" > Save</button > <button *ngFor ="let hero of heroes" (click )="deleteHero(hero)" > {{hero.name}}</button > <form #heroForm (ngSubmit )="onSubmit(heroForm)" > ... </form >
模板上下文中的变量名的优先级高于组件上下文中的变量名。在上面的 deleteHero(hero)
中,hero
是一个模板输入变量,而不是组件中的 hero
属性。
模板语句不能引用全局命名空间的任何东西。比如不能引用 window
或 document
,也不能调用 console.log
或 Math.max
。
绑定语法
数据绑定是一种机制,用来协调用户所见和应用数据。 虽然你能往 HTML 推送值或者从 HTML 拉取值, 但如果把这些琐事交给数据绑定框架处理, 应用会更容易编写、阅读和维护。 只要简单地在绑定源和目标 HTML 元素之间声明绑定,框架就会完成这项工作。
绑定的类型可以根据数据流的方向分成三类: 从数据源到视图 、从视图到数据源 以及双向的从视图到数据源再到视图 。
数据方向 语法 绑定类型 单向 从数据源到视图 [target ]=”expression” bind-target=”expression” 插值 属性 Attribute CSS 类 样式 从视图到数据源的单向绑定 (target )=”statement” on-target=”statement”事件 事件 双向 [(target )]=”expression” bindon-target=”expression” 双向
绑定目标
绑定类型 目标 范例 属性 元素的 property 组件的 property 指令的 property <img [src]=”heroImageUrl”> <app-hero-detail [hero]=”currentHero”> <div [ngClass]=”{‘special’: isSpecial}”>
事件 元素的事件 组件的事件 指令的事件 <button (click)=”onSave()”>Save <app-hero-detail (deleteRequest)=”deleteHero()”> <div (myClick)=”clicked=$event” clickable>click me 双向 事件与 property <input [(ngModel )]=”name”> Attribute attribute(例外情况) <button [attr.aria-label]=”help”>help CSS 类 class
property<div [class.special]=”isSpecial”>Special 样式 style
property<button [style.color]=”isSpecial ? ‘red’ : ‘green’”> 内置指令
3.3-用户输入 当用户点击链接、按下按钮或者输入文字时,这些用户动作都会产生 DOM 事件。 本章解释如何使用 Angular 事件绑定语法把这些事件绑定到事件处理器。
绑定到用户输入事件
你可以使用 Angular 事件绑定 机制来响应任何 DOM 事件 。 许多 DOM 事件是由用户输入触发的。绑定这些事件可以获取用户输入。
要绑定 DOM 事件,只要把 DOM 事件的名字包裹在圆括号中,然后用放在引号中的模板语句 对它赋值就可以了。
下例展示了一个事件绑定,它实现了一个点击事件处理器:
<button (click )="onClickMe()" > Click me!</button >
等号左边的 (click)
表示把按钮的点击事件作为绑定目标 。 等号右边引号中的文本是模板语句 ,通过调用组件的 onClickMe
方法来响应这个点击事件。
写绑定时,需要知道模板语句的执行上下文 。 出现在模板语句中的每个标识符都属于特定的上下文对象。 这个对象通常都是控制此模板的 Angular 组件。 上例中只显示了一行 HTML,那段 HTML 片段属于下面这个组件:
@Component({ selector: 'app-click-me', template: ` <button (click )="onClickMe()" > Click me!</button > {{clickMessage}}` }) export class ClickMeComponent { clickMessage = ''; onClickMe() { this.clickMessage = 'You are my hero!'; } }
当用户点击按钮时,Angular 调用 ClickMeComponent
的 onClickMe
方法。
通过 $event 对象取得用户输入
DOM 事件可以携带可能对组件有用的信息。 本节将展示如何绑定输入框的 keyup
事件,在每个敲击键盘时获取用户输入。
下面的代码监听 keyup
事件,并将整个事件载荷 ($event
) 传递给组件的事件处理器。
template: ` <input (keyup)="onKey($event)"> <p>{{values}}</p> `
当用户按下并释放一个按键时,触发 keyup
事件,Angular 在 $event
变量提供一个相应的 DOM 事件对象,上面的代码将它作为参数传递给 onKey()
方法。
export class KeyUpComponent_v1 { values = '' ; onKey (event: any ) { this .values += event.target.value + ' | ' ; } }
$event
对象的属性取决于 DOM 事件的类型。例如,鼠标事件与输入框编辑事件包含了不同的信息。
所有标准 DOM 事件对象 都有一个 target
属性, 引用触发该事件的元素。 在本例中,target
是`` 元素 , event.target.value
返回该元素的当前内容。
在组件的 onKey()
方法中,把输入框的值和分隔符 (|) 追加组件的 values
属性。 使用插值表达式 来把存放累加结果的 values
属性回显到屏幕上。
3.4-生命周期钩子 每个组件都有一个被 Angular 管理的生命周期。
Angular 创建它,渲染它,创建并渲染它的子组件,在它被绑定的属性发生变化时检查它,并在它从 DOM 中被移除前销毁它。
Angular 提供了生命周期钩子 ,把这些关键生命时刻暴露出来,赋予你在它们发生时采取行动的能力。
除了那些组件内容和视图相关的钩子外,指令有相同生命周期钩子。
组件生命周期钩子概览
指令和组件的实例有一个生命周期:当 Angular 新建、更新和销毁它们时触发。 通过实现一个或多个 Angular core
库里定义的生命周期钩子 接口,开发者可以介入该生命周期中的这些关键时刻。
每个接口都有唯一的一个钩子方法,它们的名字是由接口名再加上 ng
前缀构成的。比如,OnInit
接口的钩子方法叫做 ngOnInit
, Angular 在创建组件后立刻调用它,:
export class PeekABoo implements OnInit { constructor (private logger: LoggerService ) { } ngOnInit ( ) { this .logIt(`OnInit` ); } logIt (msg: string ) { this .logger.log(`#${nextId++} ${msg} ` ); } }
没有指令或者组件会实现所有这些接口,并且有些钩子只对组件有意义。只有在指令/组件中定义过的 那些钩子方法才会被 Angular 调用。
生命周期的顺序
当 Angular 使用构造函数新建一个组件或指令后,就会按下面的顺序在特定时刻调用这些生命周期钩子方法:
钩子 用途及时机 ngOnChanges() 当 Angular(重新)设置数据绑定输入属性时响应。 该方法接受当前和上一属性值的 SimpleChanges
对象 在 ngOnInit()
之前以及所绑定的一个或多个输入属性的值发生变化时都会调用。 ngOnInit() 在 Angular 第一次显示数据绑定和设置指令/组件的输入属性之后,初始化指令/组件。 在第一轮 ngOnChanges()
完成之后调用,只调用一次 。 ngDoCheck() 检测,并在发生 Angular 无法或不愿意自己检测的变化时作出反应。 在每个变更检测周期中,紧跟在 ngOnChanges()
和 ngOnInit()
后面调用。 ngAfterContentInit() 当 Angular 把外部内容投影进组件/指令的视图之后调用。 第一次 ngDoCheck()
之后调用,只调用一次。 ngAfterContentChecked() 每当 Angular 完成被投影组件内容的变更检测之后调用。ngAfterContentInit()
和每次 ngDoCheck()
之后调用 ngAfterViewInit() 当 Angular 初始化完组件视图及其子视图之后调用。 第一次 ngAfterContentChecked()
之后调用,只调用一次。 ngAfterViewChecked() 每当 Angular 做完组件视图和子视图的变更检测之后调用。ngAfterViewInit()
和每次 ngAfterContentChecked()
之后调用。 ngOnDestroy() 每当 Angular 每次销毁指令/组件之前调用并清扫。 在这儿反订阅可观察对象和分离事件处理器,以防内存泄漏。 在 Angular 销毁指令/组件之前调用。
3.5-组件之间的交互 通过输入型绑定把数据从父组件传到子组件
HeroChildComponent
有两个***输入型属性***,它们通常带@Input 装饰器 。
import { Component, Input } from '@angular/core' ; import { Hero } from './hero' ; @Component ({ selector : 'app-hero-child' , template : ` <h3>{{hero.name}} says:</h3> <p>I, {{hero.name}}, am at your service, {{masterName}}.</p> ` }) export class HeroChildComponent { @Input () hero: Hero; @Input ('master' ) masterName: string ; }
第二个 @Input
为子组件的属性名 masterName
指定一个别名 master
(译者注:不推荐为起别名,请参见风格指南).
父组件 HeroParentComponent
把子组件的 HeroChildComponent
放到 *ngFor
循环器中,把自己的 master
字符串属性绑定到子组件的 master
别名上,并把每个循环的 hero
实例绑定到子组件的 hero
属性。
import { Component } from '@angular/core' ; import { HEROES } from './hero' ; @Component ({ selector : 'app-hero-parent' , template : ` <h2>{{master}} controls {{heroes.length}} heroes</h2> <app-hero-child *ngFor="let hero of heroes" [hero]="hero" [master]="master"> </app-hero-child> ` }) export class HeroParentComponent { heroes = HEROES; master = 'Master' ; }
运行应用程序会显示三个英雄:
通过 setter 截听输入属性值的变化
使用一个输入属性的 setter,以拦截父组件中值的变化,并采取行动。
子组件 NameChildComponent
的输入属性 name
上的这个 setter,会 trim 掉名字里的空格,并把空值替换成默认字符串。
import { Component, Input } from '@angular/core' ; @Component ({ selector : 'app-name-child' , template : '<h3>"{{name}}"</h3>' }) export class NameChildComponent { private _name = '' ; @Input () set name (name: string ) { this ._name = (name && name.trim()) || '<no name set>' ; } get name (): string { return this ._name; } }
下面的 NameParentComponent
展示了各种名字的处理方式,包括一个全是空格的名字。
import { Component } from '@angular/core' ; @Component ({ selector : 'app-name-parent' , template : ` <h2>Master controls {{names.length}} names</h2> <app-name-child *ngFor="let name of names" [name]="name"></app-name-child> ` }) export class NameParentComponent { names = ['Dr IQ' , ' ' , ' Bombasto ' ]; }
通过ngOnChanges()来截听输入属性值的变化
使用 OnChanges
生命周期钩子接口的 ngOnChanges()
方法来监测输入属性值的变化并做出回应。
当需要监视多个、交互式输入属性的时候,本方法比用属性的 setter 更合适。
这个 VersionChildComponent
会监测输入属性 major
和 minor
的变化,并把这些变化编写成日志以报告这些变化。
import { Component, Input, OnChanges, SimpleChange } from '@angular/core' ; @Component ({ selector : 'app-version-child' , template : ` <h3>Version {{major}}.{{minor}}</h3> <h4>Change log:</h4> <ul> <li *ngFor="let change of changeLog">{{change}}</li> </ul> ` }) export class VersionChildComponent implements OnChanges { @Input () major: number ; @Input () minor: number ; changeLog: string [] = []; ngOnChanges (changes: {[propKey: string ]: SimpleChange} ) { let log: string [] = []; for (let propName in changes) { let changedProp = changes[propName]; let to = JSON .stringify(changedProp.currentValue); if (changedProp.isFirstChange()) { log.push(`Initial value of ${propName} set to ${to} ` ); } else { let from = JSON .stringify(changedProp.previousValue); log.push(`${propName} changed from ${from } to ${to} ` ); } } this .changeLog.push(log.join(', ' )); } }
VersionParentComponent
提供 minor
和 major
值,把修改它们值的方法绑定到按钮上。
import { Component } from '@angular/core' ; @Component ({ selector : 'app-version-parent' , template : ` <h2>Source code version</h2> <button (click)="newMinor()">New minor version</button> <button (click)="newMajor()">New major version</button> <app-version-child [major]="major" [minor]="minor"></app-version-child> ` }) export class VersionParentComponent { major = 1 ; minor = 23 ; newMinor ( ) { this .minor++; } newMajor ( ) { this .major++; this .minor = 0 ; } }
下面是点击按钮的结果。
父组件监听子组件的事件
子组件暴露一个 EventEmitter
属性,当事件发生时,子组件利用该属性 emits
(向上弹射)事件。父组件绑定到这个事件属性,并在事件发生时作出回应。
子组件的 EventEmitter
属性是一个输出属性 ,通常带有@Output 装饰器 ,就像在 VoterComponent
中看到的。
import { Component, EventEmitter, Input, Output } from '@angular/core' ; @Component ({ selector : 'app-voter' , template : ` <h4>{{name}}</h4> <button (click)="vote(true)" [disabled]="didVote">Agree</button> <button (click)="vote(false)" [disabled]="didVote">Disagree</button> ` }) export class VoterComponent { @Input () name: string ; @Output () voted = new EventEmitter<boolean >(); didVote = false ; vote (agreed: boolean ) { this .voted.emit(agreed); this .didVote = true ; } }
点击按钮会触发 true
或 false
(布尔型有效载荷 )的事件。
父组件 VoteTakerComponent
绑定了一个事件处理器(onVoted()
),用来响应子组件的事件($event
)并更新一个计数器。
import { Component } from '@angular/core' ; @Component ({ selector : 'app-vote-taker' , template : ` <h2>Should mankind colonize the Universe?</h2> <h3>Agree: {{agreed}}, Disagree: {{disagreed}}</h3> <app-voter *ngFor="let voter of voters" [name]="voter" (voted)="onVoted($event)"> </app-voter> ` }) export class VoteTakerComponent { agreed = 0 ; disagreed = 0 ; voters = ['Narco' , 'Celeritas' , 'Bombasto' ]; onVoted (agreed: boolean ) { agreed ? this .agreed++ : this .disagreed++; } }
本框架把事件参数(用 $event
表示)传给事件处理方法,该方法会处理它:
父组件与子组件通过本地变量互动
父组件不能使用数据绑定来读取子组件的属性或调用子组件的方法。但可以在父组件模板里,新建一个本地变量来代表子组件,然后利用这个变量来读取子组件的属性和调用子组件的方法,如下例所示。
子组件 CountdownTimerComponent
进行倒计时,归零时发射一个导弹。start
和 stop
方法负责控制时钟并在模板里显示倒计时的状态信息。
import { Component, OnDestroy, OnInit } from '@angular/core' ; @Component ({ selector : 'app-countdown-timer' , template : '<p>{{message}}</p>' }) export class CountdownTimerComponent implements OnInit , OnDestroy { intervalId = 0 ; message = '' ; seconds = 11 ; clearTimer ( ) { clearInterval (this .intervalId); } ngOnInit ( ) { this .start(); } ngOnDestroy ( ) { this .clearTimer(); } start ( ) { this .countDown(); } stop ( ) { this .clearTimer(); this .message = `Holding at T-${this .seconds} seconds` ; } private countDown ( ) { this .clearTimer(); this .intervalId = window .setInterval(() => { this .seconds -= 1 ; if (this .seconds === 0 ) { this .message = 'Blast off!' ; } else { if (this .seconds < 0 ) { this .seconds = 10 ; } this .message = `T-${this .seconds} seconds and counting` ; } }, 1000 ); } }
计时器组件的宿主组件 CountdownLocalVarParentComponent
如下:
import { Component } from '@angular/core' ;import { CountdownTimerComponent } from './countdown-timer.component' ; @Component ({ selector : 'app-countdown-parent-lv' , template : ` <h3>Countdown to Liftoff (via local variable)</h3> <button (click)="timer.start()">Start</button> <button (click)="timer.stop()">Stop</button> <div class="seconds">{{timer.seconds}}</div> <app-countdown-timer #timer></app-countdown-timer> ` , styleUrls : ['../assets/demo.css' ] }) export class CountdownLocalVarParentComponent { }
父组件不能通过数据绑定使用子组件的 start
和 stop
方法,也不能访问子组件的 seconds
属性。
把本地变量(#timer
)放到()标签中,用来代表子组件。这样父组件的模板就得到了子组件的引用,于是可以在父组件的模板中访问子组件的所有属性和方法。
这个例子把父组件的按钮绑定到子组件的 start
和 stop
方法,并用插值表达式来显示子组件的 seconds
属性。
下面是父组件和子组件一起工作时的效果。
父组件调用@ViewChild()
这个本地变量 方法是个简单便利的方法。但是它也有局限性,因为父组件-子组件的连接必须全部在父组件的模板中进行。父组件本身的代码对子组件没有访问权。
如果父组件的类 需要读取子组件的属性值或调用子组件的方法,就不能使用本地变量 方法。
当父组件类 需要这种访问时,可以把子组件作为 ViewChild ,***注入***到父组件里面。
下面是父组件 CountdownViewChildParentComponent
:
import { AfterViewInit, ViewChild } from '@angular/core' ;import { Component } from '@angular/core' ;import { CountdownTimerComponent } from './countdown-timer.component' ; @Component ({ selector : 'app-countdown-parent-vc' , template : ` <h3>Countdown to Liftoff (via ViewChild)</h3> <button (click)="start()">Start</button> <button (click)="stop()">Stop</button> <div class="seconds">{{ seconds() }}</div> <app-countdown-timer></app-countdown-timer> ` , styleUrls : ['../assets/demo.css' ] }) export class CountdownViewChildParentComponent implements AfterViewInit { @ViewChild (CountdownTimerComponent, {static : false }) private timerComponent: CountdownTimerComponent; seconds ( ) { return 0 ; } ngAfterViewInit ( ) { setTimeout (() => this .seconds = () => this .timerComponent.seconds, 0 ); } start ( ) { this .timerComponent.start(); } stop ( ) { this .timerComponent.stop(); } }
把子组件的视图插入到父组件类需要做一点额外的工作。
首先,你必须导入对装饰器 ViewChild
以及生命周期钩子 AfterViewInit
的引用。
接着,通过 @ViewChild
属性装饰器,将子组件 CountdownTimerComponent
注入到私有属性 timerComponent
里面。
组件元数据里就不再需要 #timer
本地变量了。而是把按钮绑定到父组件自己的 start
和 stop
方法,使用父组件的 seconds
方法的插值表达式来展示秒数变化。
这些方法可以直接访问被注入的计时器组件。
ngAfterViewInit()
生命周期钩子是非常重要的一步。被注入的计时器组件只有在 Angular 显示了父组件视图之后才能访问,所以它先把秒数显示为 0.
然后 Angular 会调用 ngAfterViewInit
生命周期钩子,但这时候再更新父组件视图的倒计时就已经太晚了。Angular 的单向数据流规则会阻止在同一个周期内更新父组件视图。应用在显示秒数之前会被迫再等一轮 。
使用 setTimeout()
来等下一轮,然后改写 seconds()
方法,这样它接下来就会从注入的这个计时器组件里获取秒数的值。
父组件和子组件通过服务来通讯
父组件和它的子组件共享同一个服务,利用该服务在组件家族内部实现双向通讯。
该服务实例的作用域被限制在父组件和其子组件内。这个组件子树之外的组件将无法访问该服务或者与它们通讯。
这个 MissionService 把 MissionControlComponent 和多个 AstronautComponent 子组件连接起来。
import { Injectable } from '@angular/core' ;import { Subject } from 'rxjs' ; @Injectable ()export class MissionService { private missionAnnouncedSource = new Subject<string >(); private missionConfirmedSource = new Subject<string >(); missionAnnounced$ = this .missionAnnouncedSource.asObservable(); missionConfirmed$ = this .missionConfirmedSource.asObservable(); announceMission (mission: string ) { this .missionAnnouncedSource.next(mission); } confirmMission (astronaut: string ) { this .missionConfirmedSource.next(astronaut); } }
MissionControlComponent
提供服务的实例,并将其共享给它的子组件(通过 providers
元数据数组),子组件可以通过构造函数将该实例注入到自身。
import { Component } from '@angular/core' ; import { MissionService } from './mission.service' ; @Component ({ selector : 'app-mission-control' , template : ` <h2>Mission Control</h2> <button (click)="announce()">Announce mission</button> <app-astronaut *ngFor="let astronaut of astronauts" [astronaut]="astronaut"> </app-astronaut> <h3>History</h3> <ul> <li *ngFor="let event of history">{{event}}</li> </ul> ` , providers : [MissionService] }) export class MissionControlComponent { astronauts = ['Lovell' , 'Swigert' , 'Haise' ]; history: string [] = []; missions = ['Fly to the moon!' , 'Fly to mars!' , 'Fly to Vegas!' ]; nextMission = 0 ; constructor (private missionService: MissionService ) { missionService.missionConfirmed$.subscribe( astronaut => { this .history.push(`${astronaut} confirmed the mission` ); }); } announce ( ) { let mission = this .missions[this .nextMission++]; this .missionService.announceMission(mission); this .history.push(`Mission "${mission} " announced` ); if (this .nextMission >= this .missions.length) { this .nextMission = 0 ; } } }
AstronautComponent
也通过自己的构造函数注入该服务。由于每个 AstronautComponent
都是 MissionControlComponent
的子组件,所以它们获取到的也是父组件的这个服务实例。
import { Component, Input, OnDestroy } from '@angular/core' ; import { MissionService } from './mission.service' ;import { Subscription } from 'rxjs' ; @Component ({ selector : 'app-astronaut' , template : ` <p> {{astronaut}}: <strong>{{mission}}</strong> <button (click)="confirm()" [disabled]="!announced || confirmed"> Confirm </button> </p> ` }) export class AstronautComponent implements OnDestroy { @Input () astronaut: string ; mission = '<no mission announced>' ; confirmed = false ; announced = false ; subscription: Subscription; constructor (private missionService: MissionService ) { this .subscription = missionService.missionAnnounced$.subscribe( mission => { this .mission = mission; this .announced = true ; this .confirmed = false ; }); } confirm ( ) { this .confirmed = true ; this .missionService.confirmMission(this .astronaut); } ngOnDestroy ( ) { this .subscription.unsubscribe(); } }
History 日志证明了:在父组件 MissionControlComponent
和子组件 AstronautComponent
之间,信息通过该服务实现了双向传递。
3.6-管道 每个应用开始的时候差不多都是一些简单任务:获取数据、转换它们,然后把它们显示给用户。 获取数据可能简单到创建一个局部变量就行,也可能复杂到从 WebSocket 中获取数据流。
一旦取到数据,你就可以把它们原始值的 toString
结果直接推入视图中。 但这种做法很少能具备良好的用户体验。 比如,几乎每个人都更喜欢简单的日期格式,例如1988-04-15,而不是服务端传过来的原始字符串格式 —— Fri Apr 15 1988 00:00:00 GMT-0700 (Pacific Daylight Time)。
显然,有些值最好显示成用户友好的格式。你很快就会发现,在很多不同的应用中,都在重复做出某些相同的变换。 你几乎会把它们看做某种 CSS 样式,事实上,你也确实更喜欢在 HTML 模板中应用它们 —— 就像 CSS 样式一样。
通过引入 Angular 管道(一种编写”从显示到值”转换逻辑的途径),你可以把它声明在 HTML 中。
使用管道
管道把数据作为输入,然后转换它,给出期望的输出。 你要把组件的 birthday
属性转换成对人类更友好的日期格式。
import { Component } from '@angular/core' ;@Component ({ selector : 'app-hero-birthday' , template : `<p>The hero's birthday is {{ birthday | date }}</p>` }) export class HeroBirthdayComponent { birthday = new Date (1988 , 3 , 15 ); }
重点看下组件的模板。
<p > The hero's birthday is {{ birthday | date }}</p >
在这个插值表达式中,你让组件的 birthday
值通过管道操作符 ( | )流动到 右侧的Date 管道 函数中。所有管道都会用这种方式工作。
内置的管道
Angular 内置了一些管道,比如 DatePipe、UpperCasePipe、LowerCasePipe、CurrencyPipe 和 PercentPipe。 它们全都可以直接用在任何模板中。
对管道进行参数化
管道可能接受任何数量的可选参数来对它的输出进行微调。 可以在管道名后面添加一个冒号( : )再跟一个参数值,来为管道添加参数(比如 currency:'EUR'
)。 如果这个管道可以接受多个参数,那么就用冒号来分隔这些参数值(比如 slice:1:5
)。
修改生日模板,来为这个日期管道提供一个格式化参数。 当格式化完该英雄的 4 月 15 日生日之后,它应该被渲染成04/15/88 。
<p > The hero's birthday is {{ birthday | date:"MM/dd/yy" }} </p >
参数值可以是任何有效的模板表达式(参见模板语法 中的模板表达式 部分),比如字符串字面量或组件的属性。 换句话说,借助属性绑定,你也可以像用绑定来控制生日的值一样,控制生日的显示格式。
来写第二个组件,它把管道的格式参数绑定 到该组件的 format
属性。这里是新组件的模板:
template: ` <p>The hero's birthday is {{ birthday | date:format }}</p> <button (click)="toggleFormat()">Toggle Format</button> `
自定义管道
你还可以写自己的自定义管道。 下面就是一个名叫 ExponentialStrengthPipe
的管道,它可以放大英雄的能力:
import { Pipe, PipeTransform } from '@angular/core' ;@Pipe ({name : 'exponentialStrength' })export class ExponentialStrengthPipe implements PipeTransform { transform(value: number , exponent?: number ): number { return Math .pow(value, isNaN (exponent) ? 1 : exponent); } }
在这个管道的定义中体现了几个关键点:
管道是一个带有“管道元数据(pipe metadata)”装饰器的类。 这个管道类实现了 PipeTransform
接口的 transform
方法,该方法接受一个输入值和一些可选参数,并返回转换后的值。 当每个输入值被传给 transform
方法时,还会带上另一个参数,比如你这个管道就有一个 exponent
(放大指数) 参数。 可以通过 @Pipe
装饰器来告诉 Angular:这是一个管道。该装饰器是从 Angular 的 core
库中引入的。 这个 @Pipe
装饰器允许你定义管道的名字,这个名字会被用在模板表达式中。它必须是一个有效的 JavaScript 标识符。 比如,你这个管道的名字是 exponentialStrength
。 4-表单 4.1-表单简介 用表单处理用户输入是许多常见应用的基础功能。 应用通过表单来让用户登录、修改个人档案、输入敏感信息以及执行各种数据输入任务。
Angular 提供了两种不同的方法来通过表单处理用户输入:响应式表单和模板驱动表单。 两者都从视图中捕获用户输入事件、验证用户输入、创建表单模型、修改数据模型,并提供跟踪这些更改的途径。
不过,响应式表单和模板驱动表单在如何处理和管理表单和表单数据方面有所不同。各有优势。
一般来说:
响应式表单 更健壮:它们的可扩展性、可复用性和可测试性更强。 如果表单是应用中的关键部分,或者你已经准备使用响应式编程模式来构建应用,请使用响应式表单。模板驱动表单 在往应用中添加简单的表单时非常有用,比如邮件列表的登记表单。它们很容易添加到应用中,但是不像响应式表单那么容易扩展。如果你有非常基本的表单需求和简单到能用模板管理的逻辑,请使用模板驱动表单。响应式 模板驱动 建立(表单模式) 显式,在组件类中创建。 隐式,由组件创建。 数据模式 结构化 非结构化 可预测性 同步 异步 表单验证 函数 指令 可变性 不可变 可变 可伸缩性 访问底层API 在API之上的抽象
4.2-响应式表单 响应式表单 提供了一种模型驱动的方式来处理表单输入,其中的值会随时间而变化。本文会向你展示如何创建和更新单个表单控件,然后在一个分组中使用多个控件,验证表单的值,以及如何实现更高级的表单。
响应式表单使用显式的、不可变的方式,管理表单在特定的时间点上的状态。对表单状态的每一次变更都会返回一个新的状态,这样可以在变化时维护模型的整体性。响应式表单是围绕 Observable 的流构建的,表单的输入和值都是通过这些输入值组成的流来提供的,它可以同步访问。
响应式表单还提供了一种更直观的测试路径,因为在请求时你可以确信这些数据是一致的、可预料的。这个流的任何一个消费者都可以安全地操纵这些数据。
响应式表单与模板驱动的表单有着显著的不同点。响应式表单通过对数据模型的同步访问提供了更多的可预测性,使用 Observable 的操作符提供了不可变性,并且通过 Observable 流提供了变化追踪功能。 如果你更喜欢在模板中直接访问数据,那么模板驱动的表单会显得更明确,因为它们依赖嵌入到模板中的指令,并借助可变数据来异步跟踪变化。参见表单概览 来了解这两种范式之间的详细比较。
注册 ReactiveFormsModule
要使用响应式表单,就要从 @angular/forms
包中导入 ReactiveFormsModule
并把它添加到你的 NgModule 的 imports
数组中。
import { ReactiveFormsModule } from '@angular/forms' ;@NgModule ({ imports : [ ReactiveFormsModule ], }) export class AppModule { }
生成并导入一个新的表单控件
ng generate component NameEditor
当使用响应式表单时,FormControl
类是最基本的构造块。要注册单个的表单控件,请在组件中导入 FormControl
类,并创建一个 FormControl
的新实例,把它保存在类的某个属性中。
import { Component } from '@angular/core' ;import { FormControl } from '@angular/forms' ;@Component ({ selector : 'app-name-editor' , templateUrl : './name-editor.component.html' , styleUrls : ['./name-editor.component.css' ] }) export class NameEditorComponent { name = new FormControl('' ); }
可以用 FormControl
的构造函数设置初始值,这个例子中它是空字符串。通过在你的组件类中创建这些控件,你可以直接对表单控件的状态进行监听、修改和校验。
在模板中注册该控件
在组件类中创建了控件之后,你还要把它和模板中的一个表单控件关联起来。修改模板,为表单控件添加 formControl 绑定,formControl 是由 ReactiveFormsModule 中的 FormControlDirective 提供的。
<label > Name: <input type ="text" [formControl ]="name" > </label >
4.3-模板驱动表单 开发表单需要设计能力(那超出了本章的范围),而框架支持双向数据绑定、变更检测、验证和错误处理 ,而本章你将会学到它们。
这个页面演示了如何从草稿构建一个简单的表单。这个过程中你将学会如何:
用组件和模板构建 Angular 表单 用 ngModel
创建双向数据绑定,以读取和写入输入控件的值 跟踪状态的变化,并验证表单控件 使用特殊的 CSS 类来跟踪控件的状态并给出视觉反馈 向用户显示验证错误提示,以及启用/禁用表单控件 使用模板引用变量在 HTML 元素之间共享信息 创建 Hero 模型类
ng generate class Hero
代码如下:
export class Hero { constructor ( public id: number , public name: string , public power: string , public alterEgo?: string ) { }}
创建表达组件
ng generate component HeroForm
代码如下:
import { Component } from '@angular/core' ;import { Hero } from '../hero' ;@Component ({ selector : 'app-hero-form' , templateUrl : './hero-form.component.html' , styleUrls : ['./hero-form.component.css' ] }) export class HeroFormComponent { powers = ['Really Smart' , 'Super Flexible' , 'Super Hot' , 'Weather Changer' ]; model = new Hero(18 , 'Dr IQ' , this .powers[0 ], 'Chuck Overstreet' ); submitted = false ; onSubmit ( ) { this .submitted = true ; } get diagnostic () { return JSON .stringify(this .model); } }
修改 app.module.ts
因为模板驱动的表单位于它们自己的模块,所以在使用表单之前,需要将 FormsModule
添加到应用模块的 imports
数组中。
import { NgModule } from '@angular/core' ;import { BrowserModule } from '@angular/platform-browser' ;import { FormsModule } from '@angular/forms' ; import { AppComponent } from './app.component' ;import { HeroFormComponent } from './hero-form/hero-form.component' ; @NgModule ({ imports : [ BrowserModule, FormsModule ], declarations : [ AppComponent, HeroFormComponent ], providers : [], bootstrap : [ AppComponent ] }) export class AppModule { }
修改 app.component.ts
<app-hero-form></app-hero-form>
创建初始 HTML 表单模板
<div class ="container" > <h1 > Hero Form</h1 > <form > <div class ="form-group" > <label for ="name" > Name</label > <input type ="text" class ="form-control" id ="name" required > </div > <div class ="form-group" > <label for ="alterEgo" > Alter Ego</label > <input type ="text" class ="form-control" id ="alterEgo" > </div > <button type ="submit" class ="btn btn-success" > Submit</button > </form > </div >
5-Observable和RxJS 5.1-可观察对象(Observable) 可观察对象支持在应用中的发布者和订阅者之间传递消息。 在需要进行事件处理、异步编程和处理多个值的时候,可观察对象相对其它技术有着显著的优点。
可观察对象是声明式的 —— 也就是说,虽然你定义了一个用于发布值的函数,但是在有消费者订阅它之前,这个函数并不会实际执行。 订阅之后,当这个函数执行完或取消订阅时,订阅者就会收到通知。
可观察对象可以发送多个任意类型的值 —— 字面量、消息、事件。无论这些值是同步发送的还是异步发送的,接收这些值的 API 都是一样的。 由于准备(setup)和清场(teardown)的逻辑都是由可观察对象自己处理的,因此你的应用代码只管订阅并消费这些值就可以了,做完之后,取消订阅。无论这个流是击键流、HTTP 响应流还是定时器,对这些值进行监听和停止监听的接口都是一样的。
基本用法
作为发布者,你创建一个 Observable
的实例,其中定义了一个订阅者(subscriber) 函数。 当有消费者调用 subscribe()
方法时,这个函数就会执行。 订阅者函数用于定义“如何获取或生成那些要发布的值或消息”。
要执行所创建的可观察对象,并开始从中接收通知,你就要调用它的 subscribe()
方法,并传入一个观察者(observer) 。 这是一个 JavaScript 对象,它定义了你收到的这些消息的处理器(handler)。 subscribe()
调用会返回一个 Subscription
对象,该对象具有一个 unsubscribe()
方法。 当调用该方法时,你就会停止接收通知。
下面这个例子中示范了这种基本用法,它展示了如何使用可观察对象来对当前地理位置进行更新。
Observe geolocation updates
const locations = new Observable((observer ) => { const {next, error} = observer; let watchId; if ('geolocation' in navigator) { watchId = navigator.geolocation.watchPosition(next, error); } else { error('Geolocation not available' ); } return {unsubscribe ( ) { navigator.geolocation.clearWatch(watchId); }}; }); const locationsSubscription = locations.subscribe({ next (position ) { console .log('Current Position: ' , position); }, error (msg ) { console .log('Error Getting Location: ' , msg); } }); setTimeout (() => { locationsSubscription.unsubscribe(); }, 10000 );
定义观察者
用于接收可观察对象通知的处理器要实现 Observer
接口。这个对象定义了一些回调函数来处理可观察对象可能会发来的三种通知:
通知类型 说明 next 必要。用来处理每个送达值。在开始执行后可能执行零次或多次。 error 可选。用来处理错误通知。错误会中断这个可观察对象实例的执行过程。 complete 可选。用来处理执行完毕(complete)通知。当执行完毕后,这些值就会继续传给下一个处理器。
观察者对象可以定义这三种处理器的任意组合。如果你不为某种通知类型提供处理器,这个观察者就会忽略相应类型的通知。
订阅
只有当有人订阅 Observable
的实例时,它才会开始发布值。 订阅时要先调用该实例的 subscribe()
方法,并把一个观察者对象传给它,用来接收通知。
下面的例子会创建并订阅一个简单的可观察对象,它的观察者会把接收到的消息记录到控制台中:
const myObservable = of (1 , 2 , 3 ); const myObserver = { next : x => console .log('Observer got a next value: ' + x), error : err => console .error('Observer got an error: ' + err), complete : () => console .log('Observer got a complete notification' ), }; myObservable.subscribe(myObserver);
另外,subscribe()
方法还可以接收定义在同一行中的回调函数,无论 next
、error
还是 complete
处理器。比如,下面的 subscribe()
调用和前面指定预定义观察者的例子是等价的。
myObservable.subscribe( x => console .log('Observer got a next value: ' + x), err => console .error('Observer got an error: ' + err), () => console .log('Observer got a complete notification' ) );
无论哪种情况,next
处理器都是必要的,而 error
和 complete
处理器是可选的。
注意,next()
函数可以接受消息字符串、事件对象、数字值或各种结构,具体类型取决于上下文。 为了更通用一点,我们把由可观察对象发布出来的数据统称为流 。任何类型的值都可以表示为可观察对象,而这些值会被发布为一个流。
创建可观察对象
使用 Observable
构造函数可以创建任何类型的可观察流。 当执行可观察对象的 subscribe()
方法时,这个构造函数就会把它接收到的参数作为订阅函数来运行。 订阅函数会接收一个 Observer
对象,并把值发布给观察者的 next()
方法。
比如,要创建一个与前面的 of(1, 2, 3)
等价的可观察对象,你可以这样做:
function sequenceSubscriber (observer ) { observer.next(1 ); observer.next(2 ); observer.next(3 ); observer.complete(); return {unsubscribe ( ) {}}; } const sequence = new Observable(sequenceSubscriber); sequence.subscribe({ next (num ) { console .log(num); }, complete ( ) { console .log('Finished sequence' ); } });
如果要略微加强这个例子,我们可以创建一个用来发布事件的可观察对象。在这个例子中,订阅函数是用内联方式定义的。
function fromEvent (target, eventName ) { return new Observable((observer ) => { const handler = (e ) => observer.next(e); target.addEventListener(eventName, handler); return () => { target.removeEventListener(eventName, handler); }; }); }
现在,你就可以使用这个函数来创建可发布 keydown
事件的可观察对象了:
const ESC_KEY = 27 ;const nameInput = document .getElementById('name' ) as HTMLInputElement;const subscription = fromEvent(nameInput, 'keydown' ) .subscribe((e: KeyboardEvent ) => { if (e.keyCode === ESC_KEY) { nameInput.value = '' ; } });
多播
典型的可观察对象会为每一个观察者创建一次新的、独立的执行。 当观察者进行订阅时,该可观察对象会连上一个事件处理器,并且向那个观察者发送一些值。当第二个观察者订阅时,这个可观察对象就会连上一个新的事件处理器,并独立执行一次,把这些值发送给第二个可观察对象。
有时候,不应该对每一个订阅者都独立执行一次,你可能会希望每次订阅都得到同一批值 —— 即使是那些你已经发送过的。这在某些情况下有用,比如用来发送 document
上的点击事件的可观察对象。
多播 用来让可观察对象在一次执行中同时广播给多个订阅者。借助支持多播的可观察对象,你不必注册多个监听器,而是复用第一个(next
)监听器,并且把值发送给各个订阅者。
当创建可观察对象时,你要决定你希望别人怎么用这个对象以及是否对它的值进行多播。
来看一个从 1 到 3 进行计数的例子,它每发出一个数字就会等待 1 秒。
function sequenceSubscriber (observer ) { const seq = [1 , 2 , 3 ]; let timeoutId; function doSequence (arr, idx ) { timeoutId = setTimeout (() => { observer.next(arr[idx]); if (idx === arr.length - 1 ) { observer.complete(); } else { doSequence(arr, ++idx); } }, 1000 ); } doSequence(seq, 0 ); return {unsubscribe ( ) { clearTimeout (timeoutId); }}; } const sequence = new Observable(sequenceSubscriber); sequence.subscribe({ next (num ) { console .log(num); }, complete ( ) { console .log('Finished sequence' ); } });
注意,如果你订阅了两次,就会有两个独立的流,每个流都会每秒发出一个数字。代码如下:
sequence.subscribe({ next (num ) { console .log('1st subscribe: ' + num); }, complete ( ) { console .log('1st sequence finished.' ); } }); setTimeout (() => { sequence.subscribe({ next (num ) { console .log('2nd subscribe: ' + num); }, complete ( ) { console .log('2nd sequence finished.' ); } }); }, 500 );
修改这个可观察对象以支持多播,代码如下:
function multicastSequenceSubscriber ( ) { const seq = [1 , 2 , 3 ]; const observers = []; let timeoutId; return (observer ) => { observers.push(observer); if (observers.length === 1 ) { timeoutId = doSequence({ next (val ) { observers.forEach(obs => obs.next(val)); }, complete ( ) { observers.slice(0 ).forEach(obs => obs.complete()); } }, seq, 0 ); } return { unsubscribe ( ) { observers.splice(observers.indexOf(observer), 1 ); if (observers.length === 0 ) { clearTimeout (timeoutId); } } }; }; } function doSequence (observer, arr, idx ) { return setTimeout (() => { observer.next(arr[idx]); if (idx === arr.length - 1 ) { observer.complete(); } else { doSequence(observer, arr, ++idx); } }, 1000 ); } const multicastSequence = new Observable(multicastSequenceSubscriber()); multicastSequence.subscribe({ next (num ) { console .log('1st subscribe: ' + num); }, complete ( ) { console .log('1st sequence finished.' ); } }); setTimeout (() => { multicastSequence.subscribe({ next (num ) { console .log('2nd subscribe: ' + num); }, complete ( ) { console .log('2nd sequence finished.' ); } }); }, 1500 );
错误处理
由于可观察对象会异步生成值,所以用 try/catch
是无法捕获错误的。你应该在观察者中指定一个 error
回调来处理错误。发生错误时还会导致可观察对象清理现有的订阅,并且停止生成值。可观察对象可以生成值(调用 next
回调),也可以调用 complete
或 error
回调来主动结束。
myObservable.subscribe({ next (num ) { console .log('Next num: ' + num)}, error (err ) { console .log('Received an errror: ' + err)} });
5.2-RxJS库 响应式编程是一种面向数据流和变更传播的异步编程范式(Wikipedia )。RxJS(响应式扩展的 JavaScript 版)是一个使用可观察对象进行响应式编程的库,它让组合异步代码和基于回调的代码变得更简单 (RxJS Docs )。
RxJS 提供了一种对 Observable
类型的实现,直到 Observable
成为了 JavaScript 语言的一部分并且浏览器支持它之前,它都是必要的。这个库还提供了一些工具函数,用于创建和使用可观察对象。这些工具函数可用于:
把现有的异步代码转换成可观察对象 迭代流中的各个值 把这些值映射成其它类型 对流进行过滤 组合多个流 创建可观察对象的函数
RxJS 提供了一些用来创建可观察对象的函数。这些函数可以简化根据某些东西创建可观察对象的过程,比如事件、定时器、承诺等等。
比如:
import { from } from 'rxjs' ;const data = from (fetch('/api/endpoint' ));data.subscribe({ next (response ) { console .log(response); }, error (err ) { console .error('Error: ' + err); }, complete ( ) { console .log('Completed' ); } });
import { interval } from 'rxjs' ;const secondsCounter = interval(1000 );secondsCounter.subscribe(n => console .log(`It's been ${n} seconds since subscribing!` ));
操作符
操作符是基于可观察对象构建的一些对集合进行复杂操作的函数。RxJS 定义了一些操作符,比如 map()
、filter()
、concat()
和 flatMap()
。
操作符接受一些配置项,然后返回一个以来源可观察对象为参数的函数。当执行这个返回的函数时,这个操作符会观察来源可观察对象中发出的值,转换它们,并返回由转换后的值组成的新的可观察对象。
下面是一个简单的例子:
import { map } from 'rxjs/operators' ; const nums = of (1 , 2 , 3 ); const squareValues = map((val: number ) => val * val);const squaredNums = squareValues(nums); squaredNums.subscribe(x => console .log(x));
你可以使用管道 来把这些操作符链接起来。管道让你可以把多个由操作符返回的函数组合成一个。pipe()
函数以你要组合的这些函数作为参数,并且返回一个新的函数,当执行这个新函数时,就会顺序执行那些被组合进去的函数。
应用于某个可观察对象上的一组操作符就像一个菜谱 —— 也就是说,对你感兴趣的这些值进行处理的一组操作步骤。这个菜谱本身不会做任何事。你需要调用 subscribe()
来通过这个菜谱生成一个结果。
例子如下:
import { filter, map } from 'rxjs/operators' ; const nums = of (1 , 2 , 3 , 4 , 5 ); const squareOddVals = pipe( filter((n: number ) => n % 2 !== 0 ), map(n => n * n) ); const squareOdd = squareOddVals(nums); squareOdd.subscribe(x => console .log(x));
pipe()
函数也同时是 RxJS 的 Observable
上的一个方法,所以你可以用下列简写形式来达到同样的效果:
import { filter, map } from 'rxjs/operators' ;const squareOdd = of (1 , 2 , 3 , 4 , 5 ) .pipe( filter(n => n % 2 !== 0 ), map(n => n * n) ); squareOdd.subscribe(x => console .log(x));
RxJS 提供了很多操作符,不过只有少数是常用的。 下面是一个常用操作符的列表和用法范例,参见 RxJS API 文档 。
错误处理
除了可以在订阅时提供 error()
处理器外,RxJS 还提供了 catchError
操作符,它允许你在管道中处理已知错误。
假设你有一个可观察对象,它发起 API 请求,然后对服务器返回的响应进行映射。如果服务器返回了错误或值不存在,就会生成一个错误。如果你捕获这个错误并提供了一个默认值,流就会继续处理这些值,而不会报错。
下面是使用 catchError
操作符实现这种效果的例子:
import { ajax } from 'rxjs/ajax' ;import { map, catchError } from 'rxjs/operators' ;const apiData = ajax('/api/data' ).pipe( map(res => { if (!res.response) { throw new Error ('Value expected!' ); } return res.response; }), catchError(err => of ([])) ); apiData.subscribe({ next (x ) { console .log('data: ' , x); }, error (err ) { console .log('errors already caught... will not run' ); } });
5.3-Angular中的可观察对象 Angular 使用可观察对象作为处理各种常用异步操作的接口。比如:
EventEmitter
类派生自 Observable
。HTTP 模块使用可观察对象来处理 AJAX 请求和响应。 路由器和表单模块使用可观察对象来监听对用户输入事件的响应。 事件发送器 EventEmitter
Angular 提供了一个 EventEmitter
类,它用来从组件的 @Output()
属性中发布一些值。EventEmitter
扩展了 Observable
,并添加了一个 emit()
方法,这样它就可以发送任意值了。当你调用 emit()
时,就会把所发送的值传给订阅上来的观察者的 next()
方法。
这种用法的例子参见 EventEmitter 文档。下面这个范例组件监听了 open
和 close
事件:
``
组件的定义如下:
@Component ({ selector : 'zippy' , template : ` <div class="zippy"> <div (click)="toggle()">Toggle</div> <div [hidden]="!visible"> <ng-content></ng-content> </div> </div>` }) export class ZippyComponent { visible = true ; @Output () open = new EventEmitter<any >(); @Output () close = new EventEmitter<any >(); toggle ( ) { this .visible = !this .visible; if (this .visible) { this .open.emit(null ); } else { this .close.emit(null ); } } }
HTTP
Angular 的 HttpClient
从 HTTP 方法调用中返回了可观察对象。例如,http.get(‘/api’)
就会返回可观察对象。相对于基于承诺(Promise)的 HTTP API,它有一系列优点:
可观察对象不会修改服务器的响应(和在承诺上串联起来的 .then()
调用一样)。反之,你可以使用一系列操作符来按需转换这些值。 HTTP 请求是可以通过 unsubscribe()
方法来取消的。 请求可以进行配置,以获取进度事件的变化。 失败的请求很容易重试。 Async管道
AsyncPipe 会订阅一个可观察对象或承诺,并返回其发出的最后一个值。当发出新值时,该管道就会把这个组件标记为需要进行变更检查的(译注:因此可能导致刷新界面)。
下面的例子把 time
这个可观察对象绑定到了组件的视图中。这个可观察对象会不断使用当前时间更新组件的视图。
Component({ selector : 'async-observable-pipe' , template : `<div><code>observable|async</code>: Time: {{ time | async }}</div>` }) export class AsyncObservablePipeComponent { time = new Observable(observer => setInterval (() => observer.next(new Date ().toString()), 1000 ) ); }
路由器 (router)
Router.events
以可观察对象的形式提供了其事件。 你可以使用 RxJS 中的 filter()
操作符来找到感兴趣的事件,并且订阅它们,以便根据浏览过程中产生的事件序列作出决定。
例子如下:
import { Router, NavigationStart } from '@angular/router' ;import { filter } from 'rxjs/operators' ; @Component ({ selector : 'app-routable' , templateUrl : './routable.component.html' , styleUrls : ['./routable.component.css' ] }) export class Routable1Component implements OnInit { navStart : Observable<NavigationStart>; constructor (private router: Router ) { this .navStart = router.events.pipe( filter(evt => evt instanceof NavigationStart) ) as Observable<NavigationStart>; } ngOnInit ( ) { this .navStart.subscribe(evt => console .log('Navigation Started!' )); } }
ActivatedRoute 是一个可注入的路由器服务,它使用可观察对象来获取关于路由路径和路由参数的信息。比如,ActivateRoute.url
包含一个用于汇报路由路径的可观察对象。
例子如下:
import { ActivatedRoute } from '@angular/router' ; @Component ({ selector : 'app-routable' , templateUrl : './routable.component.html' , styleUrls : ['./routable.component.css' ] }) export class Routable2Component implements OnInit { constructor (private activatedRoute: ActivatedRoute ) {} ngOnInit ( ) { this .activatedRoute.url .subscribe(url => console .log('The URL changed to: ' + url)); } }
6-NgModule NgModules 用于配置注入器和编译器,并帮你把那些相关的东西组织在一起。
NgModule 是一个带有 @NgModule
装饰器的类。 @NgModule
的参数是一个元数据对象,用于描述如何编译组件的模板,以及如何在运行时创建注入器。 它会标出该模块自己的组件、指令和管道,通过 exports
属性公开其中的一部分,以便外部组件使用它们。 NgModule
还能把一些服务提供商添加到应用的依赖注入器中。
6.1-启动过程 NgModule 用于描述应用的各个部分如何组织在一起。 每个应用有至少一个 Angular 模块,根 模块就是你用来启动此应用的模块。 按照惯例,它通常命名为 AppModule
。
如果你使用 Angular CLI 来生成一个应用,其默认的 AppModule
是这样的:
import { BrowserModule } from '@angular/platform-browser' ;import { NgModule } from '@angular/core' ;import { FormsModule } from '@angular/forms' ;import { HttpClientModule } from '@angular/common/http' ; import { AppComponent } from './app.component' ; @NgModule ({ declarations : [ AppComponent ], imports : [ BrowserModule, FormsModule, HttpClientModule ], providers : [], bootstrap : [AppComponent] }) export class AppModule { }
在 import
语句之后,是一个带有 @NgModule
装饰器 的类。
@NgModule
装饰器表明 AppModule
是一个 NgModule
类。 @NgModule
获取一个元数据对象,它会告诉 Angular 如何编译和启动本应用。
declarations —— 该应用所拥有的组件。imports —— 导入 BrowserModule
以获取浏览器特有的服务,比如 DOM 渲染、无害化处理和位置(location)。providers —— 各种服务提供商。bootstrap —— 根 组件,Angular 创建它并插入 index.html
宿主页面。Angular CLI 创建的默认应用只有一个组件 AppComponent
,所以它会同时出现在 declarations
和 bootstrap
数组中。
declarations 数组
该模块的 declarations
数组告诉 Angular 哪些组件属于该模块。 当你创建更多组件时,也要把它们添加到 declarations
中。
每个组件都应该(且只能)声明(declare)在一个 NgModule
类中。 如果你使用了未声明过的组件,Angular 就会报错。
declarations
数组只能接受可声明对象。可声明对象包括组件、指令 和管道 。 一个模块的所有可声明对象都必须放在 declarations
数组中。 可声明对象必须只能属于一个模块,如果同一个类被声明在了多个模块中,编译器就会报错。
这些可声明的类在当前模块中是可见的,但是对其它模块中的组件是不可见的 —— 除非把它们从当前模块导出, 并让对方模块导入本模块
下面是哪些类可以添加到 declarations
数组中的例子:
declarations: [ YourComponent, YourPipe, YourDirective ],
每个可声明对象都只能属于一个模块,所以只能把它声明在一个 @NgModule
中。当你需要在其它模块中使用它时,就要在那里导入包含这个可声明对象的模块。
只有 @NgModule
可以出现在 imports
数组中。
imports 数组模块的 imports
数组只会出现在 @NgModule
元数据对象中。 它告诉 Angular 该模块想要正常工作,还需要哪些模块。
列表中的模块导出了本模块中的各个组件模板中所引用的各个组件、指令或管道。在这个例子中,当前组件是 AppComponent
,它引用了导出自 BrowserModule
、FormsModule
或 HttpClientModule
的组件、指令或管道。 总之,组件的模板中可以引用在当前模块中声明的或从其它模块中导入的组件、指令、管道。
providers 数组
providers
数组中列出了该应用所需的服务。当直接把服务列在这里时,它们是全应用范围的。 当你使用特性模块和惰性加载时,它们是范围化的。
bootstrap 数组
应用是通过引导根模块 AppModule
来启动的,根模块还引用了 entryComponent
。 此外,引导过程还会创建 bootstrap
数组中列出的组件,并把它们逐个插入到浏览器的 DOM 中。
每个被引导的组件都是它自己的组件树的根。 插入一个被引导的组件通常触发一系列组件的创建并形成组件树。
虽然也可以在宿主页面中放多个组件,但是大多数应用只有一个组件树,并且只从一个根组件开始引导。
这个根组件通常叫做 AppComponent
,并且位于根模块的 bootstrap
数组中。
6.2-常用模块 Angular模块化
模块是组织应用和使用外部库扩展应用的最佳途径。
Angular 自己的库都是 NgModule,比如 FormsModule
、HttpClientModule
和 RouterModule
。 很多第三方库也是 NgModule,比如 Material Design 、 Ionic 和 AngularFire2 。
NgModule 把组件、指令和管道打包成内聚的功能块,每个模块聚焦于一个特性区域、业务领域、工作流或通用工具。
模块还可以把服务加到应用中。 这些服务可能是内部开发的(比如你自己写的),或者来自外部的(比如 Angular 的路由和 HTTP 客户端)。
模块可以在应用启动时急性加载,也可以由路由器进行异步的惰性加载。
NgModule 的元数据会做这些:
声明某些组件、指令和管道属于这个模块。 公开其中的部分组件、指令和管道,以便其它模块中的组件模板中可以使用它们。 导入其它带有组件、指令和管道的模块,这些模块中的元件都是本模块所需的。 提供一些供应用中的其它组件使用的服务。 每个 Angular 应用都至少有一个模块,也就是根模块。 你可以引导 那个模块,以启动该应用。
对于那些只有少量组件的简单应用,根模块就是你所需的一切。 随着应用的成长,你要把这个根模块重构成一些特性模块 ,它们代表一组密切相关的功能集。 然后你再把这些模块导入到根模块中。
常用模块
7-依赖注入 依赖注入(DI)是一种重要的应用设计模式。 Angular 有自己的 DI 框架,在设计应用时常会用到它,以提升它们的开发效率和模块化程度。
依赖,是当类需要执行其功能时,所需要的服务或对象。 DI 是一种编码模式,其中的类会从外部源中请求获取依赖,而不是自己创建它们。
在 Angular 中,DI 框架会在实例化该类时向其提供这个类所声明的依赖项。本指南介绍了 DI 在 Angular 中的工作原理,以及如何借助它来让你的应用更灵活、高效、健壮,以及可测试、可维护。
7.1-创建和注册可注入的服务 DI 框架让你能从一个可注入的服务 类(独立文件)中为组件提供数据。为了演示,我们还会创建一个用来提供英雄列表的、可注入的服务类,并把它注册为该服务的提供商。
创建可注册的服务类
Angular CLI 可以用下列命令在 src/app/heroes
目录下生成一个新的 HeroService
类。
ng generate service heroes/hero
下列命令会创建 HeroService
的骨架。
import { Injectable } from '@angular/core' ;@Injectable ({ providedIn : 'root' , }) export class HeroService { constructor ( ) { } }
@Injectable()
是每个 Angular 服务定义中的基本要素。该类的其余部分导出了一个 getHeroes
方法,它会返回像以前一样的模拟数据。(真实的应用可能会从远程服务器中异步获取这些数据,不过这里我们先忽略它,专心实现服务的注入机制。)
import { Injectable } from '@angular/core' ;import { HEROES } from './mock-heroes' ;@Injectable ({ providedIn : 'root' , }) export class HeroService { getHeroes ( ) { return HEROES; } }
用服务提供商配置注入器
我们创建的类提供了一个服务。@Injectable()
装饰器把它标记为可供注入的服务,不过在你使用该服务的 provider 提供商配置好 Angular 的依赖注入器 之前,Angular 实际上无法将其注入到任何位置。
该注入器负责创建服务实例,并把它们注入到像 HeroListComponent
这样的类中。 你很少需要自己创建 Angular 的注入器。Angular 会在执行应用时为你创建注入器,第一个注入器是根注入器 ,创建于启动过程 中。
提供商会告诉注入器如何创建该服务 。 要想让注入器能够创建服务(或提供其它类型的依赖),你必须使用某个提供商配置好注入器。
提供商可以是服务类本身,因此注入器可以使用 new
来创建实例。 你还可以定义多个类,以不同的方式提供同一个服务,并使用不同的提供商来配置不同的注入器。
你可以在三种位置之一设置元数据,以便在应用的不同层级使用提供商来配置注入器:
在服务本身的 @Injectable()
装饰器中。 在 NgModule 的 @NgModule()
装饰器中。 在组件的 @Component()
装饰器中。 @Injectable()
装饰器具有一个名叫 providedIn
的元数据选项,在那里你可以指定把被装饰类的提供商放到 root
注入器中,或某个特定 NgModule 的注入器中。
@NgModule()
和 @Component()
装饰器都有用一个 providers
元数据选项,在那里你可以配置 NgModule 级或组件级的注入器。
注入服务
HeroListComponent
要想从 HeroService
中获取英雄列表,就得要求注入 HeroService
,而不是自己使用 new
来创建自己的 HeroService
实例。
你可以通过制定带有依赖类型的构造函数参数 来要求 Angular 在组件的构造函数中注入依赖项。下面的代码是 HeroListComponent
的构造函数,它要求注入 HeroService
。
constructor(heroService: HeroService)
7.2-依赖注入实战 下面的例子往 AppComponent
里声明它依赖 LoggerService
和 UserContext
。
src/app/app.component.ts
constructor (logger: LoggerService, public userContext: UserContextService ) { userContext.loadUser(this .userId); logger.logInfo('AppComponent initialized' ); }
UserContext
转而依赖 LoggerService
和 UserService
(这个服务用来收集特定用户信息)。
user-context.service.ts (injection)
@Injectable ({ providedIn : 'root' }) export class UserContextService { constructor (private userService: UserService, private loggerService: LoggerService ) { } }
当 Angular 新建 AppComponent
时,依赖注入框架会先创建一个 LoggerService
的实例,然后创建 UserContextService
实例。 UserContextService
也需要框架刚刚创建的这个 LoggerService
实例,这样框架才能为它提供同一个实例。UserContextService
还需要框架创建过的 UserService
。 UserService
没有其它依赖,所以依赖注入框架可以直接 new
出该类的一个实例,并把它提供给 UserContextService
的构造函数。
父组件 AppComponent
不需要了解这些依赖的依赖。 只要在构造函数中声明自己需要的依赖即可(这里是 LoggerService
和 UserContextService
),框架会帮你解析这些嵌套的依赖。
当所有的依赖都就位之后,AppComponent
就会显示该用户的信息。
8-HttpClient 大多数前端应用都需要通过 HTTP 协议与后端服务器通讯。现代浏览器支持使用两种不同的 API 发起 HTTP 请求:XMLHttpRequest
接口和 fetch()
API。
@angular/common/http
中的 HttpClient
类为 Angular 应用程序提供了一个简化的 API 来实现 HTTP 客户端功能。它基于浏览器提供的 XMLHttpRequest
接口。 HttpClient
带来的其它优点包括:可测试性、强类型的请求和响应对象、发起请求与接收响应时的拦截器支持,以及更好的、基于可观察(Observable)对象的 API 以及流式错误处理机制。
要想使用 HttpClient
,就要先导入 Angular 的 HttpClientModule
。大多数应用都会在根模块 AppModule
中导入它。
import { NgModule } from '@angular/core' ;import { BrowserModule } from '@angular/platform-browser' ;import { HttpClientModule } from '@angular/common/http' ;@NgModule ({ imports : [ BrowserModule, HttpClientModule, ], declarations : [ AppComponent, ], bootstrap : [ AppComponent ] }) export class AppModule {}
在 AppModule
中导入 HttpClientModule
之后,你可以把 HttpClient
注入到应用类中,就像下面的 ConfigService
例子中这样。
import { Injectable } from '@angular/core' ;import { HttpClient } from '@angular/common/http' ;@Injectable ()export class ConfigService { constructor (private http: HttpClient ) { } }
8.1-获取json数据 应用通常会从服务器上获取 JSON 数据。 比如,该应用可能要从服务器上获取配置文件 config.json
,其中指定了一些特定资源的 URL。
{ "heroesUrl" : "api/heroes" , "textfile" : "assets/textfile.txt" }
ConfigService
会通过 HttpClient
的 get()
方法取得这个文件。
configUrl = 'assets/config.json' ; getConfig ( ) { return this .http.get(this .configUrl); }
像 ConfigComponent
这样的组件会注入 ConfigService
,并调用其 getConfig
方法。
showConfig ( ) { this .configService.getConfig() .subscribe((data: Config ) => this .config = { heroesUrl : data['heroesUrl' ], textfile : data['textfile' ] }); }
这个服务方法返回配置数据的 Observable
对象,所以组件要订阅(subscribe) 该方法的返回值。 订阅时的回调函数会把这些数据字段复制到组件的 config
对象中,它会在组件的模板中绑定,以供显示。
8.2-发起http请求 post请求
addHero (hero: Hero): Observable<Hero> { return this .http.post<Hero>(this .heroesUrl, hero, httpOptions) .pipe( catchError(this .handleError('addHero' , hero)) ); }
HttpClient.post()
方法像 get()
一样也有类型参数(你会希望服务器返回一个新的英雄对象),它包含一个资源 URL。
它还接受另外两个参数:
hero
- 要 POST
的请求体数据。httpOptions
- 这个例子中,该方法的选项指定了所需的请求头 。当然,它捕获错误的方式很像前面描述的 操作方式。
HeroesComponent
通过订阅该服务方法返回的 Observable
发起了一次实际的 POST
操作。
this .heroesService.addHero(newHero) .subscribe(hero => this .heroes.push(hero));
当服务器成功做出响应时,会带有这个新创建的英雄,然后该组件就会把这个英雄添加到正在显示的 heroes
列表中。
delete请求
该应用可以把英雄的 id 传给 HttpClient.delete
方法的请求 URL 来删除一个英雄。
deleteHero (id: number ): Observable<{}> { const url = `${this .heroesUrl} /${id} ` ; return this .http.delete(url, httpOptions) .pipe( catchError(this .handleError('deleteHero' )) ); }
当 HeroesComponent
订阅了该服务方法返回的 Observable
时,就会发起一次实际的 DELETE
操作。
this .heroesService.deleteHero(hero.id).subscribe();
该组件不会等待删除操作的结果,所以它的 subscribe (订阅)中没有回调函数。不过就算你不关心结果,也仍然要订阅它。调用 subscribe()
方法会执行 这个可观察对象,这时才会真的发起 DELETE 请求。
9-路由与导航 在用户使用应用程序时,Angular 的***路由器***能让用户从一个视图 导航到另一个视图。
浏览器具有熟悉的导航模式:
在地址栏输入 URL,浏览器就会导航到相应的页面。 在页面中点击链接,浏览器就会导航到一个新页面。 点击浏览器的前进和后退按钮,浏览器就会在你的浏览历史中向前或向后导航。 Angular 的 Router
(即“路由器”)借鉴了这个模型。它把浏览器中的 URL 看做一个操作指南, 据此导航到一个由客户端生成的视图,并可以把参数传给支撑视图的相应组件,帮它决定具体该展现哪些内容。 你可以为页面中的链接绑定一个路由,这样,当用户点击链接时,就会导航到应用中相应的视图。 当用户点击按钮、从下拉框中选取,或响应来自任何地方的事件时,你也可以在代码控制下进行导航。 路由器还在浏览器的历史日志中记录下这些活动,这样浏览器的前进和后退按钮也能照常工作。
9.1-配置 每个带路由的 Angular 应用都有一个*Router
(路由器)*服务的单例对象。 当浏览器的 URL 变化时,路由器会查找对应的 Route
(路由),并据此决定该显示哪个组件。
路由器需要先配置才会有路由信息。 下面的例子创建了五个路由定义,并用 RouterModule.forRoot()
方法来配置路由器, 并把它的返回值添加到 AppModule
的 imports
数组中。
const appRoutes: Routes = [ { path : 'crisis-center' , component : CrisisListComponent }, { path : 'hero/:id' , component : HeroDetailComponent }, { path : 'heroes' , component : HeroListComponent, data : { title : 'Heroes List' } }, { path : '' , redirectTo : '/heroes' , pathMatch : 'full' }, { path : '**' , component : PageNotFoundComponent } ]; @NgModule ({ imports : [ RouterModule.forRoot( appRoutes, { enableTracing : true } ) ], ... }) export class AppModule { }
这里的路由数组 appRoutes
描述如何进行导航。 把它传给 RouterModule.forRoot()
方法并传给本模块的 imports
数组就可以配置路由器。
每个 Route
都会把一个 URL 的 path
映射到一个组件。 注意,path
不能以斜杠(/
) 开头。 路由器会为解析和构建最终的 URL,这样当你在应用的多个视图之间导航时,可以任意使用相对路径和绝对路径。
第二个路由中的 :id
是一个路由参数的令牌(Token)。比如 /hero/42
这个 URL 中,“42”就是 id
参数的值。 此 URL 对应的 HeroDetailComponent
组件将据此查找和展现 id
为 42 的英雄。 在本章中稍后的部分,你将会学习关于路由参数的更多知识。
第三个路由中的 data
属性用来存放于每个具体路由有关的任意信息。该数据可以被任何一个激活路由访问,并能用来保存诸如 页标题、面包屑以及其它静态只读数据。本章稍后的部分,你将使用resolve 守卫 来获取动态数据。
第四个路由中的空路径(''
)表示应用的默认路径,当 URL 为空时就会访问那里,因此它通常会作为起点。 这个默认路由会重定向到 URL /heroes
,并显示 HeroesListComponent
。
最后一个路由中的 **
路径是一个通配符 。当所请求的 URL 不匹配前面定义的路由表中的任何路径时,路由器就会选择此路由。 这个特性可用于显示“404 - Not Found”页,或自动重定向到其它路由。
这些路由的定义顺序 是刻意如此设计的。路由器使用先匹配者优先 的策略来匹配路由,所以,具体路由应该放在通用路由的前面。在上面的配置中,带静态路径的路由被放在了前面,后面是空路径路由,因此它会作为默认路由。而通配符路由被放在最后面,这是因为它能匹配上每一个 URL ,因此应该只有在 前面找不到其它能匹配的路由时才匹配它。
如果你想要看到在导航的生命周期中发生过哪些事件,可以使用路由器默认配置中的 enableTracing 选项。它会把每个导航生命周期中的事件输出到浏览器的控制台。 这应该只用于调试 。你只需要把 enableTracing: true
选项作为第二个参数传给 RouterModule.forRoot()
方法就可以了。
9.2-路由出口 RouterOutlet
是一个来自路由模块中的指令,它的用法类似于组件。 它扮演一个占位符的角色,用于在模板中标出一个位置,路由器将会把要显示在这个出口处的组件显示在这里。
<router-outlet > </router-outlet >
有了这份配置,当本应用在浏览器中的 URL 变为 /heroes
时,路由器就会匹配到 path
为 heroes
的 Route
,并在宿主视图中的*RouterOutlet
*之后显示 HeroListComponent
组件。
9.3-路由链接 现在,你已经有了配置好的一些路由,还找到了渲染它们的地方,但又该如何导航到它呢?固然,从浏览器的地址栏直接输入 URL 也能做到,但是大多数情况下,导航是某些用户操作的结果,比如点击一个 A 标签。
考虑下列模板:
<h1 > Angular Router</h1 > <nav > <a routerLink ="/crisis-center" routerLinkActive ="active" > Crisis Center</a > <a routerLink ="/heroes" routerLinkActive ="active" > Heroes</a > </nav > <router-outlet > </router-outlet >
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Lemon-CS !