AngularJS学习笔记

AngularJS学习笔记

官网:https://angular.io/css

中文社区:https://angular.cn/html

JavaScript速查:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects前端

ECMA2015语法:http://es6.ruanyifeng.com/
node

使用IDE:WebStorm2017。启动时,能够鼠标选中package.json中scripts的标签,例如start,而后执行F5react

WebStorm,没有Angular语法提示。git

  1. 在 head 里面添加:
    <script src="http://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js"></script>
  2. 鼠标放到这行 URL 上面后,你会发现这行代码的开头会出现一个灯泡,点击它以后会出现 download 选项,下载完成以后这个库就会自动添加到当前项目

目前为止更新版本是 Angular v6.0程序员

特色

AngularJS,目标是一套框架、多种平台同时适用手机和桌面es6

跨平台开发:github

  学习如何基于Angular构建应用程序,并复用代码和技能来构建适用于全部平台的应用。web

速度与性能:

  经过Web Worker和服务端渲染,达到在现在的Web平台上所能达到的最高速。

  Angular让你有效掌握可伸缩性。基于RxJS、Immutable.js和其余推送模型。

  能适用海量数据需求。

美妙的工具:

  使用简单声明式模板,快速实现各类特性。使用自定义组件和大量现有组件,扩展模板语言。

Angular CLI

一个命令行界面,能够建立项目、添加文件以及执行一大堆开发任务、好比测试,打包和发布。

首先须要安装 Nodejs 和 npm

而后全局安装Angular CLI:npm install -g @angular/cli

使用 ng new my-app 能够建立新的app。ng help 能够查看帮助

会等待一段时间,而后建立完成。

此时 ng serve --open。能够执行,并在浏览器中打开。

若是提示有modules找不到,能够将modules文件夹删除,而后npm install。

命令行修改端口号
ng server --port 4201
(ng serve --host localhost --port 4201)

项目文件概览

Angular CLI项目是作快速试验和开发企业解决方案的基础。

你首先要看的文件是README.md。 它提供了一些如何使用CLI命令的基础信息。 若是你想了解 Angular CLI 的工做原理,请访问 Angular CLI 的仓库及其 Wiki

有些生成的文件你可能以为陌生。接下来咱们就讲讲它们。

src文件夹

你的应用代码位于src文件夹中。 全部的Angular组件、模板、样式、图片以及你的应用所需的任何东西都在那里。 这个文件夹以外的文件都是为构建应用提供支持用的。

文件

用途

app/app.component.{ts,html,css,spec.ts}

使用HTML模板、CSS样式和单元测试定义AppComponent组件。 它是组件,随着应用的成长它会成为一棵组件树的根节点。

app/app.module.ts

定义AppModule,这个根模块会告诉Angular如何组装该应用。 目前,它只声明了AppComponent。 稍后它还会声明更多组件。

assets/*

这个文件夹下你能够放图片等任何东西,在构建应用时,它们全都会拷贝到发布包中。

environments/*

这个文件夹中包括为各个目标环境准备的文件,它们导出了一些应用中要用到的配置变量。 这些文件会在构建应用时被替换。 好比你可能在产品环境中使用不一样的API端点地址,或使用不一样的统计Token参数。 甚至使用一些模拟服务。 全部这些,CLI都替你考虑到了。

favicon.ico

每一个网站都但愿本身在书签栏中能好看一点。 请把它换成你本身的图标。

index.html

这是别人访问你的网站是看到的主页面的HTML文件。 大多数状况下你都不用编辑它。 在构建应用时,CLI会自动把全部jscss文件添加进去,因此你没必要在这里手动添加任何 <script> 或 <link> 标签。

main.ts

这是应用的主要入口点。 使用JIT compiler编译器编译本应用,并启动应用的根模块AppModule,使其运行在浏览器中。 你还能够使用AOT compiler编译器,而不用修改任何代码 —— 只要给ng build 或 ng serve 传入 --aot 参数就能够了。

polyfills.ts

不一样的浏览器对Web标准的支持程度也不一样。 填充库(polyfill)能帮咱们把这些不一样点进行标准化。 你只要使用core-js 和 zone.js一般就够了,不过你也能够查看浏览器支持指南以了解更多信息。

styles.css

这里是你的全局样式。 大多数状况下,你会但愿在组件中使用局部样式,以利于维护,不过那些会影响你整个应用的样式你仍是须要集中存放在这里。

test.ts

这是单元测试的主要入口点。 它有一些你不熟悉的自定义配置,不过你并不须要编辑这里的任何东西。

tsconfig.{app|spec}.json

TypeScript编译器的配置文件。tsconfig.app.json是为Angular应用准备的,而tsconfig.spec.json是为单元测试准备的。

根目录

src/文件夹是项目的根文件夹之一。 其它文件是用来帮助你构建、测试、维护、文档化和发布应用的。它们放在根目录下,和src/平级。

文件

用途

e2e/

e2e/下是端到端(End-to-End)测试。 它们不在src/下,是由于端到端测试实际上和应用是相互独立的,它只适用于测试你的应用而已。 这也就是为何它会拥有本身的tsconfig.json

node_modules/

Node.js建立了这个文件夹,而且把package.json中列举的全部第三方模块都放在其中。

.angular-cli.json

Angular CLI的配置文件。 在这个文件中,咱们能够设置一系列默认值,还能够配置项目编译时要包含的那些文件。 要了解更多,请参阅它的官方文档。

.editorconfig

给你的编辑器看的一个简单配置文件,它用来确保参与你项目的每一个人都具备基本的编辑器配置。 大多数的编辑器都支持.editorconfig文件,详情参见 http://editorconfig.org 。

.gitignore

一个Git的配置文件,用来确保某些自动生成的文件不会被提交到源码控制系统中。

karma.conf.js

Karma的单元测试配置,当运行ng test时会用到它。

package.json

npm配置文件,其中列出了项目使用到的第三方依赖包。 你还能够在这里添加本身的自定义脚本

protractor.conf.js

Protractor使用的端到端测试配置文件,当运行ng e2e的时候会用到它。

README.md

项目的基础文档,预先写入了CLI命令的信息。 别忘了用项目文档改进它,以便每一个查看此仓库的人都能据此构建出你的应用。

tsconfig.json

TypeScript编译器的配置,你的IDE会借助它来给你提供更好的帮助。

tslint.json

TSLintCodelyzer用的配置信息,当运行ng lint时会用到。 Lint功能能够帮你保持代码风格的统一。

官方教程-英雄编辑器

源码:https://angular.cn/tutorial/toh-pt1

项目只要启动ng start,当你对项目进行任何修改,都会同步到页面上。

ng generate component heroes 能够经过命令行建立新组件

使用ng --help \ ng generate --help能够查看具体指令。会自动建立并添加引用

架构概览

Angular是一个用HTML和TypeScript构建客户端应用的平台与框架。自己使用TypeScript写成的。将核心功能和可选功能做为一组TypeScript库进行实现。

Angular的基本构造快是NgModule,为组件提供了编译的上下文环境。NgModule会把相关的代码收集到一些功能集中。Angular应用就是由一组NgModule定义的。至少会有一个用于引导应用的跟模块,一般还会有多个特性模块。

  • 组件定义视图。视图是一组可见的屏幕元素,Angular能够根据你的程序逻辑和数据来选择和修改它们。每一个应用都至少有一个根组件。
  • 组件使用服务。服务会提供哪些与视图不直接相关的功能。服务提供商能够做为依赖被注入到组件中,让你的代码更加模块化、可服用、并且高效。

组件和服务都是简单的类,使用装饰器来标出它们的类型,并提供元数据告知Angular该如何使用

  • 组件的元数据将组件和一个用来定义视图的模板关联起来。模板把普通的HTML和指令与绑定标记组合起来。这样Angular就能够在呈现HTML以前先修改这些HTML。
  • 服务的元数据提供了一些信息,Angular要用这些信息来让组件能够经过依赖注入使用该服务。

应用的组件一般会定义不少视图,并进行分级组织。Angular提供了Router服务来帮助你定义视图之间的导航路径。路由器提供了现金的浏览器内导航功能。

模块(Module Component)

Angular应用是模块化的,有本身的模块系统,称为Angular模块或NgModules。

NgModule 能够将其组件和一组相关代码(如服务)关联起来,造成功能单元。

Angular应用至少有一个模块(根模块),习惯命名AppModule

export class AppModule { }  //app.module.ts

根模块在小型应用中多是惟一的模块,大多数应用会有不少特性模块,每一个模块都是一个内聚的代码块。

像 JavaScript 模块同样,NgModule 也能够从其它 NgModule 中导入功能,并容许导出它们本身的功能供其它 NgModule 使用。 好比,要在你的应用中使用路由器(Router)服务,就要导入 Router 这个 NgModule。

模板、指令和数据绑定

模板会把 HTML 和 Angular 的标记(markup)组合起来,这些标记能够在 HTML 元素显示出来以前修改它们。 模板中的指令会提供程序逻辑,而绑定标记会把你应用中的数据和 DOM 链接在一块儿。

  • 事件绑定让你的应用能够经过更新应用的数据来响应目标环境下的用户输入。

  • 属性绑定让你将从应用数据中计算出来的值插入到 HTML 中。

在视图显示出来以前,Angular 会先根据你的应用数据和逻辑来运行模板中的指令并解析绑定表达式,以修改 HTML 元素和 DOM。 Angular 支持双向数据绑定,这意味着 DOM 中发生的变化(好比用户的选择)一样能够反映回你的程序数据中。

你的模板也能够用管道转换要显示的值以加强用户体验。好比,能够使用管道来显示适合用户所在地区的日期和货币格式。 Angular 为一些通用的转换提供了预约义管道,你还能够定义本身的管道。

模板(Template)

经过自带的模板来定义组件视图,以HTML形式存在,告诉Angular如何渲染

模板除了标准的Html元素,还能够使用Angular的模板语法,例如*ngFor、{{hero.name}}

元数据(Metadata)

元数据告诉Angular如何处理一个类。使用装饰器@Component来附加元数据。

@Component装饰器能接受一个配置对象,Angular会基于这些信息建立和展现组件及其视图。

@Component配置项包括

selector:标签选择器,告诉Angular在HTML中查找对应名字的标签,建立并插入该组件。

templateUrl:组件HTML模板相对地址

providers:组件所需服务的依赖注入模块数组。告诉Angular该组件的构造须要一个服务

@Component({
  selector: 'hero-search',
  templateUrl: './hero-search.component.html',
  styleUrls: ['./hero-search.component.css'],
  providers: [HeroSearchService]
})

服务于依赖注入

对于与特定视图无关并但愿跨组件共享的数据或逻辑,能够建立服务类。 服务类的定义一般紧跟在 “@Injectable” 装饰器以后。该装饰器提供的元数据可让你的服务做为依赖被注入到客户组件中。

依赖注入(或 DI)让你能够保持组件类的精简和高效。有了 DI,组件就不用从服务器获取数据、验证用户输入或直接把日志写到控制台,而是会把这些任务委托给服务。

路由

Angular 的 Router 模块提供了一个服务,它可让你定义在应用的各个不一样状态和视图层次结构之间导航时要使用的路径。

  • 在地址栏输入 URL,浏览器就会导航到相应的页面。

  • 在页面中点击连接,浏览器就会导航到一个新页面。

  • 点击浏览器的前进和后退按钮,浏览器就会在你的浏览历史中向前或向后导航。

不过路由器会把相似 URL 的路径映射到视图而不是页面。 当用户执行一个动做时(好比点击连接),本应该在浏览器中加载一个新页面,可是路由器拦截了浏览器的这个行为,并显示或隐藏一个视图层次结构。

若是路由器认为当前的应用状态须要某些特定的功能,而定义此功能的模块还没有加载,路由器就会按需惰性加载此模块。

路由器会根据你应用中的导航规则和数据状态来拦截 URL。 当用户点击按钮、选择下拉框或收到其它任何来源的输入时,你能够导航到一个新视图。 路由器会在浏览器的历史日志中记录这个动做,因此前进和后退按钮也能正常工做。

要定义导航规则,你就要把导航路径和你的组件关联起来。 路径(path)使用相似 URL 的语法来和程序数据整合在一块儿,就像模板语法会把你的视图和程序数据整合起来同样。 而后你就能够用程序逻辑来决定要显示或隐藏哪些视图,以根据你制定的访问规则对用户的输入作出响应。

overview

  • 组件和模板共同定义了 Angular 的视图。

    • 组件类上的装饰器为其添加了元数据,其中包括指向相关模板的指针。

    • 组件模板中的指令和绑定标记会根据程序数据和程序逻辑修改这些视图。

  • 依赖注入器会为组件提供一些服务,好比路由器服务就能让你定义如何在视图之间导航。

数据绑定

若是没有框架,就须要本身数据推送到HTML控件中,并把用户反馈动做和值更新。

Angular支持数据绑定,让模板的各部分与组件的各部分相互合做的机制。咱们往HTML添加绑定标记,告诉Angular如何把二者联系起来。

数据绑定的语法有四种方式,每种形式都有一个方向,绑定到DOM、绑定自DOM、以及双向绑定。

<li>{{hero.name}}</li>
<app-hero-detail [hero]="selectedHero"></app-hero-detail>
<li (click)="selectHero(hero)"></li>
  • {{hero.name]}插值表达式在li标签中显示组件hero.name的值
  • [hero]属性表达式把父组件的值传到子组件的属性中
  • (click)事件表达式在用户点击英雄的名字时调用组件的selectHero方法
  • 双向绑定是第四种绑定形式,使用ngModel指令组合属性绑定和事件绑定的功能。<input [(ngModel)]="hero.name">

注意:双向绑定必定要加载FormsModule,在app.module.ts中的import暴露出去

import {FormsModule} from '@angular/forms';

指令(directive)

Angular模板是动态的,当Angular渲染他们的时候,会根据指令提供的操做对DOM进行转换。

组件是一个带模板的指令,@Component装饰器实际上就是@Directive装饰器,只是扩展了一些面向模板的特性。

还有两种其余的指令

1.属性指令:像属性同样出如今元素中,偶尔会以名字的形式出现,多数时候做为赋值目标或绑定目标出现。例如

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

ngModel修改现有元素的行为,设置其显示属性值、响应change事件

2.结构型指令:经过在DOM中添加、移除、替换元素来修改布局。例如:

<li *ngFor="let hero of heroes"></li>
<app-hero-detail *ngIf="selectedHero"></app-hero-detail>

*ngFor告诉Angular为heroes列表的每一个英雄生成一个<li>标签

*ngIf表示只有在选择英雄存在时,才会包含HeroDetail组件

服务(Service)

服务是一个广义范畴、包括:值、函数、应用所需的特性

任何东西都是一个服务,典型的服务是一个类,具备专一、明确的用途,应该作一件特定的事情,并把它作好。

服务并无什么Angular特性。好比Logger服务,就是以下所示

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

依赖注入

提供类的新实例的一种方式,负责处理好类所需的所有依赖。

大多数依赖都是服务,使用依赖注入来提供新数组以及组件所需的服务。

Angular经过查看构造函数的参数类型得知组件须要哪些服务。例以下方代码就是须要HeroService服务

constructor(private service: HeroService) { }

当Angular建立组件时,会首先为组件所需的服务请求一个注入器(injector)

注入器维护了一个服务实例的容器,存放着之前建立的实例。若是所请求的服务实例不在容器中,注入器就会建立一个服务实例,而且添加到容器中,而后返回给Angular。

当全部请求的服务都被解析完成并返回时,Angular会以这些服务为参数去调用组件的构造函数。这就是依赖注入。

一般咱们会把须要注入的类放到根模块上,以便统一化

providers: [
  BackendService,
  HeroService,
  Logger
],

或者也能够在@Commonent元数据中的providers属性把它注册在组件层。

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

依赖注入的要点是:

1.渗透在整个Angular框架中,被处处使用

2.注入器(injector)是本机制的核心。

  注入器负责维护一个容器,用于存放它建立过的服务实例

  注入器能使用提供商建立的一个新的服务实例

3.提供商是一个用于建立服务的配方。

4.把提供商注册到注入器中。

模块简介

Module

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 的组件会共享同一个编译上下文环境。

Component compilation context

组件及其模板共同定义视图。组件还能够包含视图层次结构,它能让你定义任意复杂的屏幕区域,能够将其做为一个总体进行建立、修改和销毁。 一个视图层次结构中能够混合使用由不一样 NgModule 中的组件定义的视图。 这种状况很常见,特别是对一些 UI 库来讲。

View hierarchy

当你建立一个组件时,它直接与一个叫作宿主视图的视图关联起来。 宿主视图能够是视图层次结构的根,该视图层次结构能够包含一些内嵌视图,这些内嵌视图又是其它组件的宿主视图。 这些组件能够位于相同的 NgModule 中,也能够从其它 NgModule 中导入。 树中的视图能够嵌套到任意深度。

Angular模块库(Library Module) 

Angular提供了一组JS模块,能够看作是库模块。

每一个库都是@angular前缀,能够经过npm安装,并经过import进行导入。

例如:import { Component } from '@angular/core';

若是应用模块须要Component的某些素材,就得把他加入@NgModule元数据的import中。

组件简介

组件控制屏幕上被称为视图的一小片区域。好比,教程中的下列视图都是由一个个组件所定义和控制的:

  • 带有导航连接的应用根组件。

  • 英雄列表。

  • 英雄编辑器。

你在类中定义组件的应用逻辑,为视图提供支持。 组件经过一些由属性和方法组成的 API 与视图交互。

好比,HeroListComponent 有一个 heroes 属性,它会返回一个从服务中取到的英雄数组。HeroListComponent 还有一个 selectHero() 方法,当用户从列表中选择一个英雄时,它会设置 selectedHero 属性的值。

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 中包含 <app-hero-list></app-hero-list>,Angular 就会在这些标签中插入一个 HeroListComponent 实例的视图。

  • templateUrl:该组件的 HTML 模板文件相对于这个组件文件的地址。 另外,你还能够用 template 属性的值来提供内联的 HTML 模板。 这个模板定义了该组件的宿主视图。

  • providers 是当前组件所需的依赖注入提供商的一个数组。在这个例子中,它告诉 Angular,该组件的构造函数须要一个 HeroService 实例,以获取要显示的英雄列表。

模板与视图

你要经过组件的配套模板来定义其视图。模板就是一种 HTML,它会告诉 Angular 如何渲染该组件。

视图一般会分层次进行组织,让你能以 UI 分区或页面为单位进行修改、显示或隐藏。 与组件直接关联的模板会定义该组件的宿主视图。该组件还能够定义一个带层次结构的视图,它包含一些内嵌的视图做为其它组件的宿主。

Component tree

带层次结构的视图能够包含同一模块(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 元素,好比 <h2> 和 <p>,还包括一些 Angular 的模板语法元素,如 *ngFor{{hero.name}}click[hero] 和 <app-hero-detail>。这些模板语法元素告诉 Angular 该如何根据程序逻辑和数据在屏幕上渲染 HTML。

  • *ngFor 指令告诉 Angular 在一个列表上进行迭代。

  • {{hero.name}}(click) 和 [hero] 把程序数据绑定到及绑定回 DOM,以响应用户的输入。更多内容参见稍后的数据绑定部分。

  • 模板中的 <app-hero-detail> 标签是一个表明新组件 HeroDetailComponent 的元素。HeroDetailComponent(代码略)是 HeroListComponent 的一个子组件,它定义了英雄详情视图。 注意观察像这样的自定义组件是如何与原生 HTML 元素无缝的混合在一块儿的。

数据绑定

若是没有框架,你就要本身负责把数据值推送到 HTML 控件中,并把来自用户的响应转换成动做和对值的更新。 手动写这种数据推拉逻辑会很枯燥、容易出错,难以阅读 —— 用过 jQuery 的程序员必定深有体会。

Angular 支持双向数据绑定,这是一种对模板中的各个部件与组件中的各个部件进行协调的机制。 往模板 HTML 中添加绑定标记能够告诉 Angular 该如何链接它们。

下图显示了数据绑定标记的四种形式。每种形式都有一个方向 —— 从组件到 DOM、从 DOM 到组件或双向。

Data Binding

这个来自 HeroListComponent 模板中的例子使用了其中的三种形式:

<li>{{hero.name}}</li>
<app-hero-detail [hero]="selectedHero"></app-hero-detail>
<li (click)="selectHero(hero)"></li>
  • {{hero.name}}插值表达式在 <li> 标签中显示组件的 hero.name 属性的值。
  • [hero]属性绑定把父组件 HeroListComponent 的 selectedHero 的值传到子组件 HeroDetailComponent 的 hero 属性中。
  • 当用户点击某个英雄的名字时,(click)事件绑定会调用组件的 selectHero 方法

第四种重要的绑定形式是双向数据绑定,它把属性绑定和事件绑定组合成一种单独的写法。下面这个来自 HeroDetailComponent 模板中的例子经过 ngModel 指令使用了双向数据绑定:

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

在双向绑定中,数据属性值经过属性绑定从组件流到输入框。用户的修改经过事件绑定流回组件,把属性值设置为最新的值。

Angular 在每一个 JavaScript 事件循环中处理全部的数据绑定,它会从组件树的根部开始,递归处理所有子组件。

Data Binding

数据绑定在模板及其组件之间的通信中扮演了很是重要的角色,它对于父组件和子组件之间的通信也一样重要。

Parent/Child binding 

管道

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 装饰器。

除组件外,还有两种指令:结构型指令和属性型指令。和组件同样,指令的元数据把指令类和一个 selector 关联起来,selector 用来把该指令插入到 HTML 中。 在模板中,指令一般做为属性出如今元素标签上,可能仅仅做为名字出现,也可能做为赋值目标或绑定目标出现。

结构型指令

结构型指令经过添加、移除或替换 DOM 元素来修改布局。 这个范例模板使用了两个内置的结构型指令来为要渲染的视图添加程序逻辑:

<li *ngFor="let hero of heroes"></li>
<app-hero-detail *ngIf="selectedHero"></app-hero-detail>
  • *ngFor 是一个迭代器,它要求 Angular 为 heroes 列表中的每一个 <li> 渲染出一个 <li>

  • *ngIf 是个条件语句,只有当选中的英雄存在时,它才会包含 HeroDetail 组件。

属性型指令

属性型指令会修改现有元素的外观或行为。 在模板中,它们看起来就像普通的 HTML 属性同样,所以得名“属性型指令”。

ngModel 指令就是属性型指令的一个例子,它实现了双向数据绑定。 ngModel 修改现有元素(通常是 <input>)的行为:设置其显示属性值,并响应 change 事件。

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

Angular 还有不少预约义指令,它们或者修改布局结构(好比 ngSwitch),或者修改 DOM 元素和组件的某些方面(好比 ngStyle 和 ngClass)。

你还能够写本身的指令。像 HeroListComponent 这样的组件就是一种自定义指令,你还能够建立自定义的结构型指令和属性型指令。

服务是一个广义的概念,它包括应用所需的任何值、函数或特性。狭义的服务是一个明肯定义了用途的类。它应该作一些具体的事,并作好。

服务与依赖注入简介

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); // fill cache
    });
    return this.heroes;
  }
}

依赖注入(dependency injection)

组件是服务的消费者,也就是说,你能够把一个服务注入到组件中,让组件类得以访问该服务类。

在 Angular 中,要把一个类定义为服务,就要用 @Injectable 装饰器来提供元数据,以便让 Angular 能够把它做为依赖注入到组件中。

一样,也要使用 @Injectable 装饰器来代表一个组件或其它类(好比另外一个服务、管道或 NgModule)拥有一个依赖。 依赖并没必要然是服务,它也多是函数或值等等。

依赖注入(一般简称 DI)被引入到 Angular 框架中,而且处处使用它,来为新建的组件提供所需的服务或其它东西。

  • 注入器是主要的机制。你不用本身建立 Angular 注入器。Angular 会在启动过程当中为你建立全应用级注入器。

  • 该注入器维护一个包含它已建立的依赖实例的容器,并尽量复用它们。

  • 提供商是一个建立依赖的菜谱。对于服务来讲,它一般就是这个服务类自己。你在应用中要用到的任何类都必须使用该应用的注入器注册一个提供商,以便注入器能够使用它来建立新实例。

当 Angular 建立组件类的新实例时,它会经过查看该组件类的构造函数,来决定该组件依赖哪些服务或其它依赖项。 好比 HeroListComponent 的构造函数中须要 HeroService

constructor(private service: HeroService) { }

当 Angular 发现某个组件依赖某个服务时,它会首先检查是否该注入器中已经有了那个服务的任何现有实例。若是所请求的服务尚不存在,注入器就会使用之前注册的服务提供商来制做一个,并把它加入注入器中,而后把该服务返回给 Angular。

当全部请求的服务已解析并返回时,Angular 能够用这些服务实例为参数,调用该组件的构造函数。

HeroService 的注入过程以下所示:

Service

提供服务

对于要用到的任何服务,你必须至少注册一个提供商。你能够在模块中或者组件中注册这些提供商。

  • 当你往根模块中添加服务提供商时,服务的同一个实例会服务于你应用中的全部组件。

providers: [
  BackendService,
  HeroService,
  Logger
],
  • 当你在组件级注册提供商时,你会为该组件的每个新实例提供该服务的一个新实例。 要在组件级注册,就要在 @Component 元数据的 providers 属性中注册服务提供商。
@Component({
  selector:    'app-hero-list',
  templateUrl: './hero-list.component.html',
  providers:  [ HeroService ]
})

模板与数据绑定

显示数据

标签选择器:是 app-root。在index.html中就会做为占位符显示。

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
<body>
  <app-root></app-root>
</body>

当经过main.ts的AppComponent类启动时,Angular在index.html中查找一个<app-root>元素,而后实例化AppComponent渲染其中。 

在@Component中,模板分为两种类型

template:内联模板

templateUrl:独立模板

选择内联仍是独立取决于我的喜爱、具体情况、组织及策略。若是模板很小能够选择内联。

使用构造函数来进行初始化属性,或者使用生命周期钩子OnInit

*ngFor

模板中使用*ngFor指令来显示一个数组中的每一项。*号必不可少

<ul>
    <li *ngFor="let hero of heroes">
      {{ hero }}
    </li>
  </ul>

优先选择建立实体类,而后export导出使用

export class Hero {
  constructor(
    public id: number,
    public name: string) { }
}

导入后,直接使用实体类会更加方便

heroes = [
  new Hero(1, 'Windstorm'),
  new Hero(13, 'Bombasto'),
  new Hero(15, 'Magneta'),
  new Hero(20, 'Tornado')
];
myHero = this.heroes[0];

*NgIf

ngIf指令会根据一个布尔值来显示或移出一个元素。*号必不可少。

<p *ngIf="heroes.length > 3">There are many heroes!</p>

模板中的HTML

HTML 是 Angular 模板的语言。几乎全部的 HTML 语法都是有效的模板语法。 但值得注意的例外是 <script> 元素,它被禁用了,以阻止脚本注入攻击的风险。

有些合法的 HTML 被用在模板中是没有意义的。<html><body> 和 <base> 元素这个舞台上中并无扮演有用的角色。剩下的全部元素基本上就都同样用了。

插值表达式

{{myHero}} 。Angular会自动从组件中提取myHero属性的值,而且插入到浏览器中。

属性变化,会自动刷新。插入表达式通常为相应属性,也能够调用宿主组件的方法。

<p>The sum of 1 + 1 is not {{1 + 1 + getVal()}}</p>

会先进行计算,在转换为字符串,因此{{1+1}} 内容为2.

模板表达式

{{1+1}}所表明的模板表达式是1+1。在属性绑定中会再次看到模板表达式。

例如:[property]="expression"

JavaScript 中那些具备或可能引起反作用的表达式是被禁止的,包括:

  • 赋值 (=+=-=, ...)

  • new 运算符

  • 使用 ; 或 , 的链式表达式

  • 自增和自减运算符:++ 和 --

和 JavaScript 语 法的其它显著不一样包括:

表达式上下文

表达式上下文就是组件实例,是各类绑定值的来源。例以下面的插入表达式和属性表达式,表达式上下文就是export实体

{{title}}
<span [hidden]="isUnchanged">changed</span>

除了组件实体以外,上下文还能够包括组件以外的对象。

<div *ngFor="let hero of heroes">{{hero.name}}</div>
<input #heroInput> {{heroInput.value}}

表达式中的上下文变量是由模板变量、指令的上下文变量和组件的成员叠加而成。

若是要引用的变量名存在一个以上的命名空间中,模板变量最优先,其次是指令的上下文、最后是组件的成员。

例如,上面代码。若是组件实例包含hero属性,而*ngFor也声明了一个叫hero的模板变量,在表达式中实际引用的是模板变量。

表达式指南 

模板表达式就能成就或毁掉一个应用,请遵循下列指南。

1. 没有可见的反作用

模板表达式除了目标属性的值之外,不该该改变应用的任何状态。

Angular单向数据流策略的基础,不用担忧读取组件值可能改变另外的显示值。单独渲染中,视图应老是稳定的。

2. 执行迅速

Angular执行模板表达式会很频繁,表达式应该快速结束。当计算代价较高时,应该考虑缓存。

3. 很是简单

常规是属性名+方法的调用,不要写的过于繁琐。

4. 幂等性

最好使用幂等的表达式,由于没有反作用,还能提高Angular变动检测的性能。

幂等性含义

任意屡次执行所产生的影响均与一次执行的影响相同。指能够使用相同参数重复执行,并能得到相同结果的函数。

这些函数不会影响系统状态,也不用担忧重复执行会对系统形成改变。

模板语句

模板语句用来响应绑定目标(HTML元素、组件、指令)触发的事件。出如今=号右侧。

<button (click)="deleteHero()">Delete hero</button>

模板语句有反作用

响应事件是 Angular 中“单向数据流”的另外一面。 在一次事件循环中,能够随意改变任何地方的任何东西。

和模板表达式同样,模板语句使用的语言也像 JavaScript。某些 JavaScript 语法仍然是不容许的

  • new 运算符

  • 自增和自减运算符:++ 和 --

  • 操做并赋值,例如 += 和 -=

  • 位操做符 | 和 &

  • 模板表达式运算符

语句上下文

模板自身上下文中的属性。下面例子分别是:传递$event对象、模板输入变量、模板引用变量给组件中的一个事件处理方法。

<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参数就是let的参数。

模板语句不能引用全局命名空间的任何东西。好比不能引用 window 或 document,也不能调用 console.log 或 Math.max

语句指南

避免复杂的模板语句,常规是函数调用或者属性赋值。

绑定语法:概览

数据绑定是一种机制,用来协调用户所见和应用数据。

绑定的类型能够根据数据流的方向分为三种:从数据源到视图、从视图到数据源、双向绑定

单向从数据源到视图(插值表达式、属性、Attribute、CSS样式

{{expression}}
[target]="expression"

从视图到数据源的单向绑定(事件

(target)="statement"
on-target="statement"

双向绑定

[(target)]="expression"
bindon-target="expression"

除了插值表达式外,等号左边是目标名,也就是属性(Property)的名字。

新的思惟模型

数据绑定的威力和容许用自定义标记扩展 HTML 词汇的能力,会让你把模板 HTML 当成 HTML+

在正常的 HTML 开发过程当中,你使用 HTML 元素来建立视觉结构, 经过把字符串常量设置到元素的 attribute 来修改那些元素。

<div class="special">Mental Model</div>
<img src="assets/images/hero.png">
<button disabled>Save</button>

在 Angular 模板中,你仍使用一样的方式建立结构和初始化 attribute 值。

而后,用封装了 HTML 的组件建立新元素,并把它们看成原生 HTML 元素在模板中使用。

<div class="special">Mental Model</div>
<app-hero-detail></app-hero-detail>

这就是HTML+

<button [disabled]="isUnchanged">Save</button>

直觉告诉你,你正在绑定按钮的 disabled attribute。 并把它设置为组件的 isUnchanged 属性的当前值。

但你的直觉是错的!实际上,一旦开始数据绑定,就再也不跟 HTML attribute 打交道了。 这里不是设置 attribute,而是设置 DOM 元素、组件和指令的 property。

HTML attribute 与 DOM property 的对比

attribute 是由 HTML 定义的。property 是由 DOM (Document Object Model) 定义的。

  • 少许 HTML attribute 和 property 之间有着 1:1 的映射,如 id

  • 有些 HTML attribute 没有对应的 property,如 colspan

  • 有些 DOM property 没有对应的 attribute,如 textContent

  • 大量 HTML attribute 看起来映射到了 property

attribute 初始化 DOM property。property 的值能够改变;attribute 的值不能改变。

例如,当浏览器渲染 <input type="text" value="Bob"> 时,它将建立相应 DOM 节点, 它的 value 这个 property 被初始化为 “Bob”。

当用户在输入框中输入 “Sally” 时,DOM 元素的 value 这个 property 变成了 “Sally”。 可是该 HTML 的 value 这个 attribute 保持不变。若是你读取 input 元素的 attribute,就会发现确实没变: input.getAttribute('value') // 返回 "Bob"

HTML 的 value 这个 attribute 指定了初始值;DOM 的 value 这个 property 是当前值。

disabled 这个 attribute 是另外一种特例。按钮的 disabled 这个 property 是 false,由于默认状况下按钮是可用的。 当你添加 disabled 这个 attribute 时,只要它出现了按钮的 disabled 这个 property 就初始化为 true,因而按钮就被禁用了。

添加或删除 disabled 这个 attribute 会禁用或启用这个按钮。但 attribute 的值可有可无,这就是你为何无法经过 <button disabled="false">仍被禁用</button> 这种写法来启用按钮。

设置按钮的 disabled 这个 property(如,经过 Angular 绑定)能够禁用或启用这个按钮。 这就是 property 的价值。

模板绑定是经过 property 和事件来工做的,而不是 attribute。

绑定目标

数据绑定的目标是DOM中的某些东西,多是属性、事件、元素属性。汇总以下

绑定类型

目标

范例

属性

元素的 property

组件的 property

指令的 property

<img [src]="heroImageUrl"> <app-hero-detail [hero]="currentHero"></app-hero-detail>
<button [disabled]="isUnchanged">Cancel is disabled</button> <div [ngClass]="{'test':true,'test2':true}">[ngClass] 选择绑定的class样式(test\test2)</div>
<div [ngStyle]="{'background-color':'red','font-size':'1em'}">[ngStyle] 选择绑定的style样式</div>

事件

元素的事件

组件的事件

指令的事件

<button (click)="onSave()">Save</button> <app-hero-detail (deleteRequest)="deleteHero()"></app-hero-detail> <div (myClick)="clicked=$event" clickable>click me</div>

双向

事件与 property

 <input [(ngModel)]="name">
Attribute

attribute(例外状况)

   <button [attr.aria-label]="help">help</button>

CSS 类

class属性

<div [class.special]="isSpecial">Special</div>
推荐使用ngClass

样式

style属性

<button [style.font-size.em]="1" [style.color]="isSpecial ? 'red' : 'green'">
推荐使用ngStyle

属性绑定([属性名])

在方括号之中的元素属性名标记着目标属性,例如 [src],就是src属性。也能够写成bind-src。

使用ngClass能够绑定元素拥有的class样式。

不能在属性绑定表达式中对任何东西赋值、不能使用自增、自减运算符。

模板表达式应当返回目标属性所需类型的值,若是属性想要字符串就返回字符串,想要对象就返回对象。

<app-hero-detail [hero]="currentHero"></app-hero-detail>

方括号告诉Angular要计算模板表达式,若是忘了加方括号,就当作字符串常量来看待。

一次性字符串初始化

当知足下列条件是,应当省略方括号

1.目标属性接受字符串

2.字符串是固定不变的,能够直接合并到模块中。

3.初始值永远不变

<app-hero-detail prefix="You are my" [hero]="currentHero"></app-hero-detail>

属性绑定和插值表达式效果一致。若是数据类型不是字符串,必须使用属性绑定!!!

无论是插值表达式仍是属性表达式,都不容许带有script标签的HTML

$event和事件处理语句

事件绑定中,AngularJS会为目标事件设置事件处理器。

当事件发生时,处理器会执行模板语句,一般涉及到响应事件执行动做的接收器。

绑定会经过名叫$event的事件对象传递关于此事件的信息(包括数值),若是目标是原生DOM元素事件,$event就是DOM事件对象,有target和target.value

<input [value]="currentHero.name"
       (input)="currentHero.name=$event.target.value" >

上面代码把输入框的value属性绑定到currentHero.name属性上,要监听对值的修改,代码绑定到输入框input事件。

用户形成更改时,input事件被触发,并包含在DOM事件对象($event)的上下文中执行这条语句。

要更新currentHero.name属性,就须要经过$event.target.value来获取更改后的值。

<input [value]="name" (input)="name=$event.target.value"> 
等于
<input [(ngModel)]="name">

EventEmitter实现自定义事件

指令使用Angular EventEmitter来触发自定义事件。建立一个EventEmitter实例,而且把它做为属性暴露出来。

指令调用EventEmitter.emit(payload)来触发事件,能够传入任何东西做为信息载体。

父指令经过绑定到这个属性来监听事件,并经过$event对象来访问载荷。

首先须要定义EventEmitter实例,而后设置调用规则。注意:@Output()必须存在,否则不起做用。

@Component({
  selector: 'app-componet-del',
  template: `
    <div>This is Componet</div>
    <input [(ngModel)]="name">
    <button (click)="del()">Del</button>
  `
})

export class Componet {
  public name: string;
  @Output() deleteRequest = new EventEmitter<string>();
  del(): void {
    this.deleteRequest.emit(this.name);
  }
}

父组件页面直接定义自定义事件和触发方法便可。

<app-componet-del (deleteRequest)="Coo($event)"></app-componet-del>

Coo(event) {
    debugger;
    console.log(`123`);
  }

双向数据绑定

须要显示数据属性,而且在用户做出更改时更新此属性。

Angular提供一种特殊的双向绑定语法:[(x)]。语法结合了属性绑定[]、事件绑定()。盒子里头有香蕉

下面是一个自定义事件的双向绑定示例。会把fontSizePx赋值给size属性,而后自定义事件触发修改。

import {Component, EventEmitter, Input, Output} from '@angular/core';

@Component({
  selector: 'app-sizer',
  template: `
    <div>
      <button (click)="dec()" title="smaller">-</button>
      <button (click)="inc()" title="bigger">+</button>
      <label [style.font-size.px]="size">FontSize: {{size}}px</label>
    </div>`
})
export class SizerComponent {
  @Input() size: number | string;
  @Output() sizeChange = new EventEmitter<number>();

  dec() {
    this.resize(-1);
  }

  inc() {
    this.resize(+1);
  }

  resize(delta: number) {
    this.size = Math.min(40, Math.max(8, +this.size + delta));
    this.sizeChange.emit(this.size);
  }
}
<app-sizer [(size)]="fontSizePx"></app-sizer>
<app-sizer [size]="fontSizePx" (sizeChange)="fontSizePx=$event"></app-sizer>

<div [style.font-size.px]="fontSizePx">Resizable Text</div>

实际上[(size)],就是[size] (sizeChange)的语法糖。

$event里面就是sizeChange的参数,点击按钮时,Angular将$event赋值给fontSizePx。

比起单独绑定属性和事件,双向绑定语法显得很是方便。可是双向绑定须要遵循x值和xChange事件的模式,原生HTML不支持。

幸运的是,Angular和ngModel指令为桥梁,容许在表单元素上使用双向绑定。ngModel会本身设定xChange事件

内置属性型指令

属性指令会监听和修改其它HTML元素或组件的行为、元素属性、DOM属性。一般会做为HTML属性的名称应用在元素上。

NgClass

删除单个类最佳作法

<div [class.special]="isSpecial">The class binding is special</div>

动态添加或删除Class类的方法,能够同时添加或移出多个类。

ngClass须要绑定到一个Key:Value对象上,key是css类名,vlaue是true。

ngClass的值也能够绑定属性,只要属性是对象形式便可。

this.currentClasses =  {
    'saveable': this.canSave,
    'modified': !this.isUnchanged,
    'special':  this.isSpecial
  };

<div [ngClass]="currentClasses">This div is initially</div> 

NgStyle

设置单同样式值最佳作法

<div [style.font-size]="isSpecial ? 'x-large' : 'smaller'" >
This div is x-large or smaller.
</div>

能够绑定多个内联样式。

ngStyle须要绑定一个Key:Value对象上,key是样式名,value是用于这个样式的值。

ngStyle的值也能够绑定属性,只要属性是对象形式便可。

this.currentStyles = {
    'font-style':  this.canSave      ? 'italic' : 'normal',
    'font-weight': !this.isUnchanged ? 'bold'   : 'normal',
    'font-size':   this.isSpecial    ? '24px'   : '12px'
  };

<div [ngStyle]="currentStyles">
  This div is initially italic, normal weight, and extra large (24px).
</div>

NgModel

表单操做时,既要显示数据属性又要根据用户的更改去修改那个属性。

使用ngModel能够进行简化双向绑定的操做。注意:使用ngModel时须要FormModule

<input [(ngModel)]="currentHero.name">
<input [ngModel]="currentHero.name" (ngModelChange)="currentHero.name=$event">

ngModel指令经过本身的输入属性ngModel和输出属性ngModelChange隐藏了事件细节。

若是咱们须要特殊的事件处理,也能够写成以下形式

<input
  [ngModel]="currentHero.name"
  (ngModelChange)="setUppercaseName($event)">

内置结构型命令

结构型命令的职责是HTML布局,塑造或重塑DOM的结构,一般是经过添加、移出它们所附加到的宿主元素来实现的。

NgIf

*ngIf指令应用到元素上,能够往DOM中添加或者移出这个元素。*号必不可少

<app-hero-detail *ngIf="isActive"></app-hero-detail>

NgFor

重复器指令,自定义数据显示的一种方法。绑定到元素上,会渲染列表中的每个条目,并生成一个元素。

<div *ngFor="let hero of heroes">{{hero.name}}</div>

*ngFor指令上下文中有index属性,能够返回一个从零开始的索引,表示迭代中的顺序。

能够经过模板输入变量捕获这个index值,并把它用在模板中。

<div *ngFor="let hero of heroes; let i=index">{{i + 1}} - {{hero.name}}</div>

trackBy的*ngFor

trackBy能够防止Angular更新所有DOM。往组件中添加一个方法,会返回NgFor应该追踪的值。

trackByHeroes(index: number, hero: Hero): number { return hero.id; }

<div *ngFor="let hero of heroes; trackBy: trackByHeroes">
  ({{hero.id}}) {{hero.name}}
</div>

若是建立的是相同的hero.id,那么页面不会刷新DOM树。只有id变换了,才会刷新DOM树

NgSwitch

[ngSwitch]相似于JS中的Switch,能够从多个可能的元素中根据switch条件来显示某一个。Angular只会把选中的元素放进DOM中。

NgSwitch包含三个相互协做指令,NgSwitch、NgSwitchCase、NgSwitchDefault

<div [ngSwitch]="currentHero.emotion">
  <app-happy-hero    *ngSwitchCase="'happy'"    [hero]="currentHero"></app-happy-hero>
  <app-sad-hero      *ngSwitchCase="'sad'"      [hero]="currentHero"></app-sad-hero>
  <app-confused-hero *ngSwitchCase="'confused'" [hero]="currentHero"></app-confused-hero>
  <app-unknown-hero  *ngSwitchDefault           [hero]="currentHero"></app-unknown-hero>
</div>

模板引用变量(#var)

用来引用模板中的某个DOM元素,还能够引用Angular组件或指令

使用#号来声明引用变量,#phone的意思是声明一个叫phone的变量来引用<input>元素

能够在模板的任何地方引用模板引用变量,好比声明在input上的变量在另外一边也能够直接使用

<input #phone placeholder="phone number">

<button (click)="callPhone(phone.value)">Call</button>

大多数状况下,Angular会把模板引用变量的值设置为声明它的内个元素。

<form (ngSubmit)="onSubmit(heroForm)" #heroForm="ngForm">
  <div class="form-group">
    <label for="name">Name
      <input class="form-control" name="name" required [(ngModel)]="hero.name">
    </label>
  </div>
  <button type="submit" [disabled]="!heroForm.form.valid">Submit</button>
</form>
<div [hidden]="!heroForm.form.valid">
  {{submitMessage}}
</div>

上图代码中模板引用变量heroForm出现了三次。这里的heroForm就是Angular NgForm指令的引用,具有跟踪表单每一个控件值和有效性的能力。

模板引用变量的做用范围是整个模板,不要在同一个模板中定义屡次,不然会没法肯定。

也能够使用ref-前缀代替#,例如 

<input ref-phone placeholder="phone number">

输出和输入属性

绑定目标是在=号左侧部分,绑定源是=号右侧部分

绑定的目标是绑定符:[]、()、[()]中的属性或事件名

绑定源则是引号中的部分或插值符号中的部分。

<img [src]="iconUrl"/>
<button (click)="onSave()">Save</button>

上面代码中src是绑定目标的属性绑定,iconUrl是绑定源的属性

目标属性必须被显式的标记为输入或输出。@Input() 和 @Output

@Input()  hero: Hero;
@Output() deleteRequest = new EventEmitter<Hero>();

另外也能够在@Component里面写出输出数组,使用inputs和outputs

@Component({
  inputs: ['hero'],
  outputs: ['deleteRequest'],
})

两种方法不要同时使用。

输入仍是输出? 

输入属性一般接受数据值。输出属性一般暴露事件产生者,例如:EventEmitter对象

输入和输出是从目标指令的角度来看的。

 

hero是输入由于数据流从模板绑定表达式流入那个属性

deleteRequest是输出由于事件从那个属性流出,流向模板绑定语句中的处理器

别名

能够把别名传进@Input或者@Output装饰器,就能够为属性指定别名。或者在数组中指定别名。

@Output('myClick') clicks = new EventEmitter<string>();

  inputs:['size'],
  outputs:['sizeChange:Change']

模板表达式操做符

管道操做符(|)

在绑定以前,表达式的结果可能须要一些转换。例如,数字显示成金额、强制文本大写、过滤列表

Angular管道对于这样的小型转换是很是好的选择,管道就是一个简单的函数,接受一个输入值,并返回结果。

<div>Title through uppercase pipe: {{title | uppercase | lowercase}}</div>

管道操做符会把它左侧表达式结果传递给他右侧的管道函数,能够串联起来。Angular一共16个管道函数。使用管道函数不要写Pipe

管道函数API:https://angular.cn/api?type=pipe

全部管道均在 '@angular/common';,使用以前记得引入:import { AsyncPipe } from '@angular/common';

AsyncPipe:更新Promise\Observable的状态,让他当即返回结果

DatePipe:根据当前环境格式化日期

I18nPluralPipe:将值映射到根据区域设置规则将值复数化的字符串。

I18nSelectPipe:显示与当前值匹配的字符串的通用选择器。

JsonPipe:将值转换为JSON字符串。

LowerCasePipe:将文本转换为小写。

CurrencyPipe:使用区域设置规则将数字格式化为货币。

DecimalPipe:根据区域设置规则格式化数字。

PercentPipe:根据语言环境规则将数字格式化为百分比。

SlicePipe:建立包含元素子集(切片)的新List或String。

UpperCasePipe:将文本转换为大写。

TitleCasePipe:将文本转换为标题。

DeprecatedDatePipe:根据语言环境规则格式化日期。

DeprecatedCurrencyPipe:使用区域设置规则将数字格式化为货币。

DeprecatedDecimalPipe:根据区域设置规则格式化数字。

DeprecatedPercentPipe:根据语言环境规则将数字格式化为百分比。

安全导航操做符(?.)

安全导航操做符?.是一种流畅而便利的方式,用来保护出如今属性路径中 null 和 undefined 值。 下例中,当 currentHero 为空时,保护视图渲染器,让它免于失败

The current hero's name is {{currentHero?.name}}

Angular 安全导航操做符 (?.) 是在属性路径中保护空值的更加流畅、便利的方式。 表达式会在它遇到第一个空值的时候跳出。 显示是空的,但应用正常工做,而没有发生错误。

非空断言操做符(!.)

有类型的变量默认是不容许 null 或 undefined 值的,若是有未赋值的变量,或者试图把 null 或 undefined 赋值给不容许为空的变量,类型检查器就会抛出一个错误。

若是类型检查器在运行期间没法肯定一个变量是 null 或 undefined,那么它也会抛出一个错误。 你本身可能知道它不会为空,但类型检查器不知道。 因此你要告诉类型检查器,它不会为空,这时就要用到非空断言操做符

The hero's name is {{hero!.name}}

在 Angular 编译器把你的模板转换成 TypeScript 代码时,这个操做符会防止 TypeScript 报告 "hero.name 可能为 null 或 undefined"的错误。

类型转换函数$any

有时候,绑定表达式可能会报类型错误,而且它不能或很难指定类型。要消除这种报错,你能够使用 $any 转换函数来把表达式转换成 any 类型

The hero's marker is {{$any(hero).marker}}

当 Angular 编译器把模板转换成 TypeScript 代码时,$any 表达式能够防止 TypeScript 编译器报错说 marker 不是 Hero 接口的成员。

$any 转换函数能够和 this 联合使用,以便访问组件中未声明过的成员。

Undeclared members is {{$any(this).member}}

生命周期钩子

每一个组件都有一个被Angular管理的生命周期。建立、渲染、并渲染子组件,属性变化时检查,移出前销毁

Angular提供了生命周期的钩子,把这些生命周期暴露出来,赋予咱们在它发生时采起行动的能力。

每一个接口都有惟一的一个钩子方法,名字都是由接口名+ng前缀构成的。好比,下方OnInit接口

export class PeekABoo implements OnInit {
  constructor(private logger: LoggerService) { }

  // implement OnInit's `ngOnInit` method
  ngOnInit() { this.logIt(`OnInit`); }

  logIt(msg: string) {
    this.logger.log(`#${nextId++} ${msg}`);
  }
}

生命周期的顺序 

当使用构造函数新建一个组件或指令后,按下面顺序在特定时刻调用这些生命钩子

钩子

目的和时机

ngOnChanges()

当Angular(从新)设置数据绑定输入属性时响应。 该方法接受当前和上一属性值的SimpleChanges对象

当被绑定的输入属性的值发生变化时调用,首次调用必定会发生在ngOnInit()以前。

ngOnInit()

在Angular第一次显示数据绑定和设置指令/组件的输入属性以后,初始化指令/组件。

在第一轮ngOnChanges()完成以后调用,只调用一次。

ngDoCheck()

检测,并在发生Angular没法或不肯意本身检测的变化时做出反应。

在每一个Angular变动检测周期中调用,ngOnChanges()ngOnInit()以后。

ngAfterContentInit()

当把内容投影进组件以后调用。

第一次ngDoCheck()以后调用,只调用一次。

只适用于组件。

ngAfterContentChecked()

每次完成被投影组件内容的变动检测以后调用。

ngAfterContentInit()和每次ngDoCheck()以后调用

只适合组件。

ngAfterViewInit()

初始化完组件视图及其子视图以后调用。

第一次ngAfterContentChecked()以后调用,只调用一次。

只适合组件。

ngAfterViewChecked()

每次作完组件视图和子视图的变动检测以后调用。

ngAfterViewInit()和每次ngAfterContentChecked()以后调用。

只适合组件。

ngOnDestroy

当Angular每次销毁指令/组件以前调用并清扫。 在这儿反订阅可观察对象和分离事件处理器,以防内存泄漏。

在Angular销毁指令/组件以前调用。

组件之间的交互

经过输入型绑定能够把数据从父组件传到子组件

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; }
}
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 {
  // Displays 'Mr. IQ', '<no name set>', 'Bombasto'
  names = ['Mr. IQ', '   ', '  Bombasto  '];
}

经过ngOnChanges()来截听输入属性值的变化

使用 OnChanges 生命周期钩子接口的 ngOnChanges() 方法来监测输入属性值的变化并作出回应。

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]="voted">Agree</button>
    <button (click)="vote(false)" [disabled]="voted">Disagree</button>
  `
})
export class VoterComponent {
  @Input()  name: string;
  @Output() onVoted = new EventEmitter<boolean>();
  voted = false;

  vote(agreed: boolean) {
    this.onVoted.emit(agreed);
    this.voted = 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"
      (onVoted)="onVoted($event)">
    </app-voter>
  `
})
export class VoteTakerComponent {
  agreed = 0;
  disagreed = 0;
  voters = ['Mr. IQ', 'Ms. Universe', 'Bombasto'];

  onVoted(agreed: boolean) {
    agreed ? this.agreed++ : this.disagreed++;
  }
}

父组件与子组件经过本地变量互动

父组件不能使用数据绑定来读取子组件的属性或调用子组件的方法。但能够在父组件模板里,新建一个本地变量来表明子组件,而后利用这个变量来读取子组件的属性和调用子组件的方法,以下例所示。

子组件 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; } // reset
        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)放到(<countdown-timer>)标签中,用来表明子组件。这样父组件的模板就获得了子组件的引用,因而能够在父组件的模板中访问子组件的全部属性和方法。

这个例子把父组件的按钮绑定到子组件的 start 和 stop 方法,并用插值表达式来显示子组件的 seconds 属性。

父组件调用@ViewChild()

这个本地变量方法是个简单便利的方法。可是它也有局限性,由于父组件-子组件的链接必须所有在父组件的模板中进行。父组件自己的代码对子组件没有访问权。

若是父组件的类须要读取子组件的属性值或调用子组件的方法,就不能使用本地变量方法。

当父组件类须要这种访问时,能够把子组件做为 ViewChild,注入到父组件里面

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)
  private timerComponent: CountdownTimerComponent;

  seconds() { return 0; }

  ngAfterViewInit() {
    // Redefine `seconds()` to get from the `CountdownTimerComponent.seconds` ...
    // but wait a tick first to avoid one-time devMode
    // unidirectional-data-flow-violation error
    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 {

  // Observable string sources
  private missionAnnouncedSource = new Subject<string>();
  private missionConfirmedSource = new Subject<string>();

  // Observable string streams
  missionAnnounced$ = this.missionAnnouncedSource.asObservable();
  missionConfirmed$ = this.missionConfirmedSource.asObservable();

  // Service message commands
  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() {
    // prevent memory leak when component destroyed
    this.subscription.unsubscribe();
  }
}

在父组件 MissionControlComponent 和子组件 AstronautComponent 之间,信息经过该服务实现了双向传递。

组件样式

Angular 应用使用标准的 CSS 来设置样式。这意味着你能够把关于 CSS 的那些知识和技能直接用于 Angular 程序中,例如:样式表、选择器、规则以及媒体查询等。

另外,Angular 还能把组件样式捆绑在组件上,以实现比标准样式表更加模块化的设计。

:host选择器

使用 :host 伪类选择器,用来选择组件宿主元素中的元素(相对于组件模板内部的元素)。

:host {
  display: block;
  border: 1px solid black;
}

 

:host 选择是是把宿主元素做为目标的惟一方式。除此以外,你将没办法指定它, 由于宿主不是组件自身模板的一部分,而是父组件模板的一部分。 

:host-context选择器

使用 :host-context() 伪类选择器。它也以相似 :host() 形式使用。它在当前组件宿主元素的祖先节点中查找 CSS 类, 直到文档的根节点为止。在与其它选择器组合使用时,它很是有用。

下面的例子中,只有当某个祖先元素有 CSS 类 theme-light 时,才会把 background-color 样式应用到组件内部的全部 <h2> 元素中。

:host-context(.theme-light) h2 {
  background-color: #eef;
}

 

Angular元素(Elements)概览

https://angular.cn/guide/elements

动态组件加载器

组件的模板不会永远是固定的。应用可能会须要在运行期间加载一些新的组件。

新的组件加载方式,不须要在模板中引用固定的组件

指令

定义锚点告诉Angular要把组件插入的地方。 

//ad.directive
import {Directive, ViewContainerRef} from '@angular/core';

@Directive({
  selector: '[ad-host]',
})
export class AdDirective {
  constructor(public viewContainerRef: ViewContainerRef) {
  }
}

注入ViewContainerRef来获取对容器的访问权,这就是动态加入的组件宿主。

@Directive定义的名字ad-host,就是要应用到元素上的指令。

而后咱们须要一个组件容器,来装这些组件。建立ad-item.ts

//ad-item
import { Type } from '@angular/core';

export class AdItem {
  constructor(public component: Type<any>, public data: any) {}
}

接着咱们须要一个广告接口,用来保证动态的组件所必要的属性。建立ad.component.ts

//ad.component.ts
export interface AdComponent {
  data: any;
}

而后咱们建立广告切换显示组件,实现代码都在这个组件中。建立ad-banner.component.ts

ng-template,就是刚才制做的指令将应用到的地方。ng-template元素时动态加载最佳选择,不会渲染任何额外的输出。

//ad-banner.component.ts

import {Component, Input, AfterViewInit, ViewChild, ComponentFactoryResolver, OnDestroy} from '@angular/core';

import {AdDirective} from './ad.directive';
import {AdItem} from './ad-item';
import {AdComponent} from './ad.component';

@Component({
  selector: 'app-add-banner',
  template: `
    <div class="ad-banner">
      <h3>Advertisements</h3>
      <ng-template ad-host></ng-template>
    </div>
  `
})
export class AdBannerComponent implements AfterViewInit, OnDestroy {
  @Input() ads: AdItem[];
  currentAddIndex: number = -1;
  @ViewChild(AdDirective) adHost: AdDirective;
  subscription: any;
  interval: any;

  constructor(private componentFactoryResolver: ComponentFactoryResolver) {
  }

  ngAfterViewInit() {
    this.loadComponent();
    this.getAds();
  }

  ngOnDestroy() {
    clearInterval(this.interval);
  }

  loadComponent() {
    this.currentAddIndex = (this.currentAddIndex + 1) % this.ads.length;
    let adItem = this.ads[this.currentAddIndex];
    let componentFactory = this.componentFactoryResolver.resolveComponentFactory(adItem.component);
    let viewContainerRef = this.adHost.viewContainerRef;
    viewContainerRef.clear();
    let componentRef = viewContainerRef.createComponent(componentFactory);
    (<AdComponent>componentRef.instance).data = adItem.data;
  }

  getAds() {
    this.interval = setInterval(() => {
      this.loadComponent();
    }, 3000);
  }
}

AdBannerComponent接受一个AdItem输入对象数组做为输入。AdItem指定要加载的组件类,已经绑定上的任意数据。

经过getAds()方法,循环遍历AdItem数组,并每三秒调用一次。

loadComponent方法使用循环选取算法,来选择广告。以后使用ComponentFactoryResolver来为每一个组件解析一个ComponentFactor。

而后把viewContainerRef指向这个组件的现有实例,这个实例就是咱们以前设定的指令adHost。

AdDirective的构造函数注入了ViewContainerRef,这个指令能够访问用做动态组件宿主的元素。

使用ViewContainerRef的createComponent方法返回一个引用,指向这个刚刚加载的组件。

对选择器的引用

angular编译器会为模板中所引用的每一个组件都生成一个ComponentFactory类。

可是对于动态加载的组件,模板中不会出现对它们的选择器的引用。

须要动态加载组件到app.module.ts的entryComponents数组中

entryComponents: [ HeroJobAdComponent, HeroProfileComponent ],

接下来,建立两个范例组件

//hero-job-ad.component.ts
import { Component, Input } from '@angular/core';

import { AdComponent }      from './ad.component';

@Component({
  template: `
    <div class="job-ad">
      <h4>{{data.headline}}</h4> 
      
      {{data.body}}
    </div>
  `
})
export class HeroJobAdComponent implements AdComponent {
  @Input() data: any;
}
//hero-profile.component
import { Component, Input }  from '@angular/core';

import { AdComponent }       from './ad.component';

@Component({
  template: `
    <div class="hero-profile">
      <h3>Featured Hero Profile</h3>
      <h4>{{data.name}}</h4>
      
      <p>{{data.bio}}</p>

      <strong>Hire this hero today!</strong>
    </div>
  `
})
export class HeroProfileComponent implements AdComponent {
  @Input() data: any;
}

最后咱们在AppComponent组件上应用它,而且给他初始化数据便可。页面作属性绑定,Init钩子作加载。

 public ads: AdItem[];

  ngOnInit(): void {
    this.ads = [
      new AdItem(HeroProfileComponent, {name: 'Bombasto', bio: 'Brave as they come'}),
      new AdItem(HeroJobAdComponent, {headline: 'Hiring for several positions', body: 'Submit your resume today!'}),
    ];
  }


<app-add-banner [ads]="ads"></app-add-banner>

指令概览

在 Angular 中有三种类型的指令:

  1. 组件 — 拥有模板的指令

  2. 结构型指令 — 经过添加和移除 DOM 元素改变 DOM 布局的指令

  3. 属性型指令 — 改变元素、组件或其它指令的外观和行为的指令。

组件是这三种指令中最经常使用的

结构型指令修改视图的结构。例如,NgFor 和 NgIf。 要了解更多,参见结构型指令 guide。

属性型指令改变一个元素的外观或行为。例如,内置的 NgStyle 指令能够同时修改元素的多个样式。

属性型指令

属性型指令用于改变一个DOM元素的外观或行为

建立属性型指令须要一个带有@Directive装饰器的控制类,该装饰器指定了一个用于标识属性的选择器。

import {Directive, ElementRef, Input} from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})

export class HighlightDirective {
  constructor(el: ElementRef) {
    el.nativeElement.style.backgroundColor = 'yellow';
  }
}

ng generate directive highlight  建立指令类

import 引入Angular的core库公开类

Directive:提供装饰器功能

ElementRef:提供DOM访问功能,注入到指令构造函数中。

Input:数据从绑定表达式传达到指令中

装饰器@Directive函数以配置对象参数的形式,包含了指令的元数据,须要一个CSS选择器。

指令的选择器是[myHighlightDirective],Angular会在模板中找到全部带myHighlight属性的元素

装饰器完了就是指令控制类,叫HighlightDirective,包含该指令的逻辑。须要导出此类

Angular会为每一个匹配的元素建立一个指令控制类的实例,并把ElementRef和Renderer注入到构造函数中。

ElementRef服务赋予咱们经过它访问DOM的能力,Renderer服务容许经过代码设置元素样式。

使用属性型指令

要使用这个HighlightDirective,建立一个模板,把这个指令做为属性应用到一个段落上。这个元素也就是指令的宿主

在html能够直接使用这个指令

<h1>My First Attribute Directive</h1>
<p appHightlight>Highlight me!</p>

不要忘了,还要在app.module里面把指令类暴露出来。给declarations数组添加元素。

总结:Angular在p元素上发现这个属性,建立一个Hightlight实例,并把元素的引用注入到指令的构造函数中。构造里面设置了背景色

响应用户事件

设置用户鼠标悬浮在一个元素时,设置它的颜色

首先给highlightDirective导入HostListener,用来添加事件响应

!!!注意:在类构造函数中,若是但愿某个参数暴露出来,须要给这个参数添加private访问修饰符!!!

import {Directive, ElementRef, HostListener, Input} from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})

export class HighlightDirective {
  constructor(private el: ElementRef) {
  }

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight('yellow');
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }

  private highlight(color: string): void {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

使用@HostListener装饰器,能够添加事件处理器,上面代码注册监听了两个事件。监听方法名,任意起便可。

数据传递值

使用Input输入属性,可让颜色从页面传递过来。注意,这边推荐用别名,来区分具体含义。

在指令内部,该属性叫hightlightColor,外部绑定的时候叫appHightlight。若是忘记添加,也能够给默认值。||非空运算符能够使用

import {Directive, ElementRef, HostListener, Input} from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})

export class HighlightDirective {
  constructor(private el: ElementRef) {
  }

  @Input("appHighlight") public highlightColor: string;

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.highlightColor || 'red');
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }

  private highlight(color: string): void {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

能够经过一样的方法,绑定更多的属性,直接定义@Input,而后绑定指令上便可。

<p [appHighlight]="color" defaultColor="violet">
  Highlight me too!
</p>

@Input() defaultColor: string;

结构型指令

结构性指令的职责是HTML布局,重塑DOM结构,好比添加、删除等。大部分包含*号开头,例如*ngIf

NgIf

星号前缀是一个用来简化语法的语法糖,*ngIf会翻译成<ng-template>元素并包裹宿主元素。

<div *ngIf="hero" >{{hero.name}}</div>
-----------------------------------------------------
<ng-template [ngIf]="hero">
  <div>{{hero.name}}</div>
</ng-template>

NgFor

<div *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById" [class.odd]="odd">
  ({{i}}) {{hero.name}}
</div>

<ng-template ngFor let-hero [ngForOf]="heroes" let-i="index" let-odd="odd" [ngForTrackBy]="trackById">
  <div [class.odd]="odd">({{i}}) {{hero.name}}</div>
</ng-template>

微语法

Angular微语法可以让咱们经过简短的、友好的字符串来配置一个指令。

微语法解析器会把这个字符串翻译成<ng-template>上的属性。

  • let关键字声明一个模板输入变量,咱们会在模板中引用它。本例子中,这个输入变量就是heroiodd。 解析器会把let herolet ilet odd翻译成命名变量let-herolet-ilet-odd

  • 微语法解析器接收oftrackby,把它们首字母大写(of -> OftrackBy -> TrackBy), 而且给它们加上指令的属性名(ngFor)前缀,最终生成的名字是ngForOfngForTrackBy。 还有两个NgFor输入属性,指令据此了解到列表是heroes,而track-by函数是trackById

  • NgFor指令在列表上循环,每一个循环中都会设置和重置它本身的上下文对象上的属性。 这些属性包括indexodd以及一个特殊的属性名$implicit(隐式变量)。

  • let-ilet-odd变量是经过let i=indexlet odd=odd来定义的。 Angular把它们设置为上下文对象中的indexodd属性的当前值。

  • 上下文中的属性let-hero没有指定过,实际上它来自一个隐式变量。 Angular会把let-hero设置为上下文对象中的$implicit属性,NgFor会用当前迭代中的英雄初始化它。

  • API参考手册中描述了NgFor指令的其它属性和上下文属性。

  • NgFor是由NgForOf指令来实现的。请参阅NgForOf API reference来了解NgForOf指令的更多属性及其上下文属性。

模板输入变量

能够在单个实例的模板中引用它的值,例如:hero、i、odd。都是用let做为前导关键字

模板输入变量和模板引用变量是不一样的。

使用let关键字在模板中声明一个模板输入变量,变量范围被限制在所重复模板的单一实例上。

而声明模板引用变量是给变量添加#号前缀的方式,一个引用变量引用的是它所附着的元素、组件或指令。能够在模板任意地方访问。

模板输入变量和引用变量都有各自的命名空间,不会发生影响。

每一个宿主元素上只能有一个结构型指令。ngFor和ngIf不能再同一个元素上

NgSwitch

<div [ngSwitch]="hero?.emotion">
  <app-happy-hero    *ngSwitchCase="'happy'"    [hero]="hero"></app-happy-hero>
  <app-sad-hero      *ngSwitchCase="'sad'"      [hero]="hero"></app-sad-hero>
  <app-confused-hero *ngSwitchCase="'app-confused'" [hero]="hero"></app-confused-hero>
  <app-unknown-hero  *ngSwitchDefault           [hero]="hero"></app-unknown-hero>
</div>
---------------------------------------------------------------
<div [ngSwitch]="hero?.emotion">
  <ng-template [ngSwitchCase]="'happy'">
    <app-happy-hero [hero]="hero"></app-happy-hero>
  </ng-template>
  <ng-template [ngSwitchCase]="'sad'">
    <app-sad-hero [hero]="hero"></app-sad-hero>
  </ng-template>
  <ng-template [ngSwitchCase]="'confused'">
    <app-confused-hero [hero]="hero"></app-confused-hero>
  </ng-template >
  <ng-template ngSwitchDefault>
    <app-unknown-hero [hero]="hero"></app-unknown-hero>
  </ng-template>
</div>

优先使用星号语法的语法糖

<ng-template>

Angular元素用来渲染HTML,永远不会直接显示出来。若是没有使用结构型指令,仅仅是把元素包进里面,这些元素就是不可见的。

渲染视图以前<ng-template>会将其内容替换为一个注释。

一般都要一个根元素做为结构型指令的数组。列表元素<li>就是典型的NgFor使用的宿主元素。

若是没有宿主元素时,能够包括在原生的HTML元素中,好比<div>。可是用这种分组元素可能会破坏外观表现

<ng-container>

Angular的ng-container是分组元素,不会污染样式或元素布局,由于Angular不会把他放进DOM

<p>
  I turned the corner
  <ng-container *ngIf="hero">
    and saw {{hero.name}}. I waved
  </ng-container>
  and continued on my way.
</p>
<div>
  Pick your favorite hero
  (<label><input type="checkbox" checked (change)="showSad = !showSad">show sad</label>)
</div>
<select [(ngModel)]="hero">
  <ng-container *ngFor="let h of heroes">
    <ng-container *ngIf="showSad || h.emotion !== 'sad'">
      <option [ngValue]="h">{{h.name}} ({{h.emotion}})</option>
    </ng-container>
  </ng-container>
</select>

ng-container是由Angular解析器负责识别处理的语法元素。不是指令、组件、类或接口,更像是if中的花括号。包括接下来的元素。

建立结构型指令

导入Directive装饰器

导入Input、TemplateRef和ViewContainerRef

给指令类添加装饰器

设置CSS属性选择器

import {Directive, Input, TemplateRef, ViewContainerRef} from '@angular/core';

@Directive({ selector: '[appUnless]'})

export class UnlessDirective {}

指令的选择器一般把指令的属性名放在方括号中,如[myUnless]。这个方括号定义了CSS属性选择器

TemplateRef和ViewContainerRef

使用TemplateRef能够取得<ng-template>的内容,经过ViewContainerRef能够来访问这个视图容器。

因此须要在构造中引用,而且建立为私有

myUnless属性

指令的使用者会把一个true/false条件绑定在[myUnless]属性。指令须要一个带有@Input的myUnless属性。

@Input() set appUnless(condition: boolean) {
  if (!condition && !this.hasView) {
    this.viewContainer.createEmbeddedView(this.templateRef);
    this.hasView = true;
  } else if (condition && this.hasView) {
    this.viewContainer.clear();
    this.hasView = false;
  }
}

一旦该值的条件发生了变化,Angular就会去设置myUnless属性,这个时候就须要一个设置器

若是条件是假的,而且以前没有建立过视图,就告诉视图容器根据模板建立

若是条件是真的,而且视图已经显示出来了,就会清除该容器,并销毁视图

import {Directive, Input, TemplateRef, ViewContainerRef} from '@angular/core';

@Directive({selector: '[appUnless]'})

export class UnlessDirective {
  private hasView: boolean = false;

  constructor(private templateRef: TemplateRef<any>, private viewContainerRef: ViewContainerRef) {
  }

  @Input() set appUnless(condition: boolean) {
    if (!condition && !this.hasView) {
      this.viewContainerRef.createEmbeddedView(this.templateRef);
      this.hasView = true;
    }
    else if (condition && this.hasView) {
      this.viewContainerRef.clear();
      this.hasView = false;
    }
  }
}


<p *appUnless="false">
  condition = false
</p>
<p *appUnless="true">
  condition = true
</p>

管道 

管道把数据做为输入,而后转换它,给出指望的输出。咱们将把组件的birthday属性转换成对人类更友好的日期格式。

原始日期格式:{{birthday}} <br>
管道日期格式:{{birthday | date}}

能够对管道进行参数化来进行微调,在管道后面添加冒号再跟一个参数值,若是有多个参数就是用冒号分隔

管道日期格式:{{birthday | date:'yyyy-MM-dd'}}

 参数能够是模板表达式、字符串、组件的属性。

自定义管道

须要Pipe装饰器,是一个带有元数据的类

实现PipeTransform接口,方法接受输入和一些可选参数,并返回转换后的值

当每一个输入值被传递给transform方法时,还会带上另外一个参数。

import {Pipe, PipeTransform} from '@angular/core';

@Pipe({name: 'exponentialStrength'})
export class ExponentialStrengthPipe implements PipeTransform{
  transform(value: string, exponent: string): string {
    return `${value}-${exponent}`;
  }
}


自定义管道:{{name | exponentialStrength:'管道'}}

管道还能够应用到循环之中

import { Pipe, PipeTransform } from '@angular/core';

import { Flyer } from './heroes';

@Pipe({ name: 'flyingHeroes' })
export class FlyingHeroesPipe implements PipeTransform {
  transform(allHeroes: Flyer[]) {
    return allHeroes.filter(hero => hero.canFly);
  }
}

<div *ngFor="let hero of (heroes | flyingHeroes)">
  {{hero.name}}
</div>

纯管道(pure)和非纯管道(impure)

@Pipe({ name: 'flyingHeroesImpure', pure: false })

pure能够指定管道是否纯管道。默认均为纯管道

纯管道

Angular只有在它检测到输入值发生纯变动时才会执行纯管道。

指对原始类型值得更改或者对对象引用的更改。

Angular会忽略对象内部的更改,好比更改日期月份、添加数组、更新对象属性。都不会调用纯管道

非纯管道

Angular会在每一个组件的变动检测周期中执行非纯管道,可能会被调用屡次,会增长开销。

动画

Angular的动画系统赋予了制做各类动画效果的能力,构建出于原生CSS动画性能相同的动画。

Angular动画是基于标准的Web动画API构建的,它们在支持此 API 的浏览器中会用原生方式工做。

在往应用中添加动画以前,你要首先在应用的根模块中引入一些与动画有关的模块和函数。

import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
  imports: [ BrowserModule, BrowserAnimationsModule ],
  // ... more stuff ...
})
export class AppModule { }

Hero 类有一个 name 属性、一个 state 属性(用于代表该英雄是否为激活状态)和一个 toggleState() 函数,用来在这两种状态之间切换。

export class Hero {
  constructor(public name: string, public state = 'inactive') {
  }

  toggleState() {
    this.state = 'active' === this.state ? 'inactive' : 'active';
  }
} 

能够在组件元数据中定义一个名叫 heroState 的动画触发器。它在两个状态 active 和 inactive 之间进行转场。 当英雄处于激活状态时,它会把该元素显示得稍微大一点、亮一点。

import {Component, Input, OnInit} from '@angular/core';
import {trigger, state, style, animate, transition} from '@angular/animations';

import {Hero} from '../hero-animation/hero';

@Component({
  selector: 'app-hero-list-basic',
  templateUrl: './hero-list-basic.component.html',
  styleUrls: ['./hero-list-basic.component.css'],
  animations: [
    trigger('heroState', [
      state('inactive', style({
        backgroundColor: '#eee',
        transform: 'scale(1)'
      })),
      state('active', style({
        backgroundColor: '#cfd8dc',
        transform: 'scale(1.1)'
      })),
      transition('inactive => active', animate('100ms ease-in')),
      transition('active => inactive', animate('100ms ease-out'))
    ])
  ]
})

 

<ul>
  <li *ngFor="let hero of heros"
      [@heroState]="hero.state"
      (click)="hero.toggleState()">
    {{hero.name}}
  </li>
</ul>

状态与转场

这些 state 具体定义了每一个状态的最终样式。一旦元素转场到那个状态,该样式就会被应用到此元素上,当它留在此状态时,这些样式也会一直保持着。

定义完状态,就能定义在状态之间的各类转场了。每一个转场都会控制一条在一组样式和下一组样式之间切换的时间线:

若是多个转场都有一样的时间线配置,就能够把它们合并进同一个 transition 定义中:

transition('inactive => active, active => inactive', animate('100ms ease-out'))

若是要对同一个转场的两个方向都使用相同的时间线(就像前面的例子中那样),就能够使用 <=> 这种简写语法

transition('inactive <=> active', animate('100ms ease-out'))

*(通配符)状态

*(通配符)状态匹配任何动画状态。当定义那些不须要管当前处于什么状态的样式及转场时,这颇有用。好比:

  • 当该元素的状态从 active 变成任何其它状态时,active => * 转场都会生效。

  • 当在任意两个状态之间切换时,* => * 转场都会生效。

void状态

有一种叫作 void 的特殊状态,它能够应用在任何动画中。它表示元素没有被附加到视图。这种状况多是因为它还没有被添加进来或者已经被移除了。 void 状态在定义“进场”和“离场”的动画时会很是有用。

好比当一个元素离开视图时,* => void 转场就会生效,而无论它在离场之前是什么状态。

transition('* => void', [ animate(100, style({transform: 'translateX(100%)'})) ])

https://angular.cn/guide/animations

用户输入

经过$event对象能够取得用户输入

template: `
  <input (keyup)="onKey($event)">
  <p>{{values}}</p>
`

export class KeyUpComponent_v1 {
  onKey(event: any) { // without type info
    this.values += event.target.value + ' | ';
  }
}

从引用变量得到用户输入

<input #box (keyup)="0">
<p>{{box.value}}</p>

除非绑定一个事件,不然这将彻底没法工做。keyup事件绑定数字0,这是最短的模板语句。

按键事件过滤

(keyup)事件处理器监听每一次按键,有时候只有回车键才触发事件,能够绑定keyup.enter模拟事件。

<input #box (keyup.enter)="0">
<p>{{box.value}}</p>

也能够绑定多个事件,好比失去焦点

<input #box (keyup.enter)="0" (blur)="0">
<p>{{box.value}}</p>

模板驱动表单

<form (ngSubmit)="onSubmit()" #heroForm="ngForm">
  <label>{{title}}</label>
  <button type="submit">Save</button>
</form>

能够给表单定义ngSubmit事件来模拟表单提交。

也能够结合验证来模拟按钮不能点击事件。前端同样,方法集合Promise写成以下形式。

onSubmit(){
    this.isShow = true;
    let valid = new Promise((resolve, reject)=>{
      return setTimeout(()=>{
        this.isShow = false;
        resolve('OK');
      },3000);
    }).then((value)=>{
      this.title = '已提交';
    });
  }

表单验证

https://angular.cn/guide/form-validation

响应式表单

https://angular.cn/guide/reactive-forms

动态表单

https://angular.cn/guide/dynamic-form

可观察对象(Observable)与RxJS

可观察对象支持在应用中的发布者和订阅者之间传递消息。在须要进行事件处理、异步编程和处理多个值的时候,可观察对象相对其它技术有着显著的优势。

可观察对象是声明式的 —— 也就是说,虽然你定义了一个用于发布值的函数,可是在有消费者订阅它以前,这个函数并不会实际执行。 订阅以后,当这个函数执行完或取消订阅时,订阅者就会收到通知。

可观察对象能够发送多个任意类型的值。不管这些值是同步发送仍是异步发送,接受这些值的API都是同样的。

基本用法

建立一个 Observable 的实例,其中定义了一个订阅者(subscriber)函数。 当有消费者调用 subscribe() 方法时,这个函数就会执行。 订阅者函数用于定义“如何获取或生成那些要发布的值或消息”。

要执行所建立的可观察对象,并开始从中接收通知,你就要调用它的 subscribe() 方法,并传入一个观察者(observer)。 这是一个 JavaScript 对象,它定义了你收到的这些消息的处理器(handler)。 subscribe() 调用会返回一个 Subscription 对象,该对象具备一个 unsubscribe() 方法。 当调用该方法时,你就会中止接收通知。

定义观察者

用于接收可观察对象通知的处理器须要Observer接口。定义了一些回调函数来处理可观察对象可能会发来的三种通知

next:必要。用来处理每一个送达值。在开始执行后可能执行零次或屡次。

error:可选。用来处理错误通知。错误会中断这个可观察对象实例的执行过程。

complete:可选。用来处理执行完毕(complete)通知。当执行完毕后,这些值就会继续传给下一个处理器。

订阅

只有当有人订阅 Observable 的实例时,它才会开始发布值。 订阅时要先调用该实例的 subscribe() 方法,并把一个观察者对象传给它,用来接收通知。

为了展现订阅的原理,咱们须要建立新的可观察对象。它有一个构造函数能够用来建立新实例,可是为了更简明,也能够使用 Observable 上定义的一些静态方法来建立一些经常使用的简单可观察对象:

  • Observable.of(...items) —— 返回一个 Observable 实例,它用同步的方式把参数中提供的这些值发送出来。

  • Observable.from(iterable) —— 把它的参数转换成一个 Observable 实例。 该方法一般用于把一个数组转换成一个(发送多个值的)可观察对象。

// Create simple observable that emits three values
const myObservable = Observable.of(1, 2, 3);

// Create observer object
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'),
};

// Execute with the observer object
myObservable.subscribe(myObserver);
// Logs:
// Observer got a next value: 1
// Observer got a next value: 2
// Observer got a next value: 3
// Observer got a complete notification

另外,subscribe() 方法还能够接收定义在同一行中的回调函数,不管 nexterror 仍是 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() 方法。

好比,要建立一个与前面的 Observable.of(1, 2, 3) 等价的可观察对象,你能够这样作:

// This function runs when subscribe() is called
function sequenceSubscriber(observer) {
  // synchronously deliver 1, 2, and 3, then complete
  observer.next(1);
  observer.next(2);
  observer.next(3);
  observer.complete();

  // unsubscribe function doesn't need to do anything in this
  // because values are delivered synchronously
  return {unsubscribe() {}};
}

// Create a new Observable that will deliver the above sequence
const sequence = new Observable(sequenceSubscriber);

// execute the Observable and print the result of each notification
sequence.subscribe({
  next(num) { console.log(num); },
  complete() { console.log('Finished sequence'); }
});

// Logs:
// 1
// 2
// 3
// Finished sequence

若是要略微增强这个例子,咱们能够建立一个用来发布事件的可观察对象

function fromEvent(target, eventName) {
  return new Observable((observer) => {
    const handler = (e) => observer.next(e);

    // Add the event handler to the target
    target.addEventListener(eventName, handler);

    return () => {
      // Detach the event handler from the target
      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;

  // Will run through an array of numbers, emitting one value
  // per second until it gets to the end of the array.
  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);

  // Unsubscribe should clear the timeout to stop execution
  return {unsubscribe() {
    clearTimeout(timeoutId);
  }};
}

// Create a new Observable that will deliver the above sequence
const sequence = new Observable(sequenceSubscriber);

sequence.subscribe({
  next(num) { console.log(num); },
  complete() { console.log('Finished sequence'); }
});

// Logs:
// (at 1 second): 1
// (at 2 seconds): 2
// (at 3 seconds): 3
// (at 3 seconds): Finished sequence

注意,若是你订阅了两次,就会有两个独立的流,每一个流都会每秒发出一个数字

// Subscribe starts the clock, and will emit after 1 second
sequence.subscribe({
  next(num) { console.log('1st subscribe: ' + num); },
  complete() { console.log('1st sequence finished.'); }
});

// After 1/2 second, subscribe again.
setTimeout(() => {
  sequence.subscribe({
    next(num) { console.log('2nd subscribe: ' + num); },
    complete() { console.log('2nd sequence finished.'); }
  });
}, 500);

// Logs:
// (at 1 second): 1st subscribe: 1
// (at 1.5 seconds): 2nd subscribe: 1
// (at 2 seconds): 1st subscribe: 2
// (at 2.5 seconds): 2nd subscribe: 2
// (at 3 seconds): 1st subscribe: 3
// (at 3 seconds): 1st sequence finished
// (at 3.5 seconds): 2nd subscribe: 3
// (at 3.5 seconds): 2nd sequence finished

修改这个可观察对象以支持多播

function multicastSequenceSubscriber() {
  const seq = [1, 2, 3];
  // Keep track of each observer (one for every active subscription)
  const observers = [];
  // Still a single timeoutId because there will only ever be one
  // set of values being generated, multicasted to each subscriber
  let timeoutId;

  // Return the subscriber function (runs when subscribe()
  // function is invoked)
  return (observer) => {
    observers.push(observer);
    // When this is the first subscription, start the sequence
    if (observers.length === 1) {
      timeoutId = doSequence({
        next(val) {
          // Iterate through observers and notify all subscriptions
          observers.forEach(obs => obs.next(val));
        },
        complete() {
          // Notify all complete callbacks
          observers.forEach(obs => obs.complete());
        }
      }, seq, 0);
    }

    return {
      unsubscribe() {
        // Remove from the observers array so it's no longer notified
        observers.splice(observers.indexOf(observer), 1);
        // If there's no more listeners, do cleanup
        if (observers.length === 0) {
          clearTimeout(timeoutId);
        }
      }
    };
  };
}

// Run through an array of numbers, emitting one value
// per second until it gets to the end of the array.
function doSequence(observer, arr, idx) {
  return setTimeout(() => {
    observer.next(arr[idx]);
    if (idx === arr.length - 1) {
      observer.complete();
    } else {
      doSequence(observer, arr, idx++);
    }
  }, 1000);
}

// Create a new Observable that will deliver the above sequence
const multicastSequence = new Observable(multicastSequenceSubscriber);

// Subscribe starts the clock, and begins to emit after 1 second
multicastSequence.subscribe({
  next(num) { console.log('1st subscribe: ' + num); },
  complete() { console.log('1st sequence finished.'); }
});

// After 1 1/2 seconds, subscribe again (should "miss" the first value).
setTimeout(() => {
  multicastSequence.subscribe({
    next(num) { console.log('2nd subscribe: ' + num); },
    complete() { console.log('2nd sequence finished.'); }
  });
}, 1500);

// Logs:
// (at 1 second): 1st subscribe: 1
// (at 2 seconds): 1st subscribe: 2
// (at 2 seconds): 2nd subscribe: 2
// (at 3 seconds): 1st subscribe: 3
// (at 3 seconds): 1st sequence finished
// (at 3 seconds): 2nd subscribe: 3
// (at 3 seconds): 2nd sequence finished

错误处理

因为可观察对象会异步生成值,因此用 try/catch 是没法捕获错误的。你应该在观察者中指定一个 error 回调来处理错误。发生错误时还会致使可观察对象清理现有的订阅,而且中止生成值。可观察对象能够生成值(调用 next 回调),也能够调用 complete 或 error 回调来主动结束。

myObservable.subscribe({
  next(num) { console.log('Next num: ' + num)},
  error(err) { console.log('Received an errror: ' + err)}
});

RxJS库

响应式编程是一种面向数据流和变动传播的异步编程范式(Wikipedia)。RxJS(响应式扩展的 JavaScript 版)是一个使用可观察对象进行响应式编程的库,它让组合异步代码和基于回调的代码变得更简单 (RxJS Docs)。

RxJS 提供了一种对 Observable 类型的实现,直到 Observable 成为了 JavaScript 语言的一部分而且浏览器支持它以前,它都是必要的。这个库还提供了一些工具函数,用于建立和使用可观察对象。这些工具函数可用于:

  • 把现有的异步代码转换成可观察对象

  • 迭代流中的各个值

  • 把这些值映射成其它类型

  • 对流进行过滤

  • 组合多个流

建立可观察对象的函数

RxJS 提供了一些用来建立可观察对象的函数。这些函数能够简化根据某些东西建立可观察对象的过程,好比事件、定时器、承诺等等。好比:

import { fromPromise } from 'rxjs';

// Create an Observable out of a promise
const data = fromPromise(fetch('/api/endpoint'));
// Subscribe to begin listening for async result
data.subscribe({
 next(response) { console.log(response); },
 error(err) { console.error('Error: ' + err); },
 complete() { console.log('Completed'); }
});
import { interval } from 'rxjs';

// Create an Observable that will publish a value on an interval
const secondsCounter = interval(1000);
// Subscribe to begin publishing values
secondsCounter.subscribe(n =>
  console.log(`It's been ${n} seconds since subscribing!`));
import { fromEvent } from 'rxjs';

const el = document.getElementById('my-element');

// Create an Observable that will publish mouse movements
const mouseMoves = fromEvent(el, 'mousemove');

// Subscribe to start listening for mouse-move events
const subscription = mouseMoves.subscribe((evt: MouseEvent) => {
  // Log coords of mouse movements
  console.log(`Coords: ${evt.clientX} X ${evt.clientY}`);

  // When the mouse is over the upper-left of the screen,
  // unsubscribe to stop listening for mouse movements
  if (evt.clientX < 40 && evt.clientY < 40) {
    subscription.unsubscribe();
  }
});
import { ajax } from 'rxjs/ajax';

// Create an Observable that will create an AJAX request
const apiData = ajax('/api/data');
// Subscribe to create the request
apiData.subscribe(res => console.log(res.status, res.response));

操做符

操做符是基于可观察对象构建的一些对集合进行复杂操做的函数。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));

// Logs
// 1
// 4
// 9

你能够使用管道来把这些操做符连接起来。管道让你能够把多个由操做符返回的函数组合成一个。pipe() 函数以你要组合的这些函数做为参数,而且返回一个新的函数,当执行这个新函数时,就会顺序执行那些被组合进去的函数。

应用于某个可观察对象上的一组操做符就像一个菜谱 —— 也就是说,对你感兴趣的这些值进行处理的一组操做步骤。这个菜谱自己不会作任何事。你须要调用 subscribe() 来经过这个菜谱生成一个结果。

例子以下:

import { filter, map } from 'rxjs/operators';

const nums = of(1, 2, 3, 4, 5);

// Create a function that accepts an Observable.
const squareOddVals = pipe(
  filter(n => n % 2),
  map(n => n * n)
);

// Create an Observable that will run the filter and map functions
const squareOdd = squareOddVals(nums);

// Suscribe to run the combined functions
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)
  );

// Subscribe to get values
squareOdd.subscribe(x => console.log(x));

经常使用操做符

RxJS 提供了不少操做符(超过 150 个),不过只有少数是经常使用的。 下面是一个经常使用操做符的列表,要查看用法范例,参见 RxJS 文档中的 RxJS 5 操做符范例

类别 操做
建立 from , fromPromise , fromEvent , of
组合 combineLatest , concat , merge , startWith , withLatestFrom , zip
过滤 debounceTime , distinctUntilChanged , filter , take , takeUntil
转换 bufferTime , concatMap , map , mergeMap , scan , switchMap
工具 tap
多播 share

错误处理

除了能够在订阅时提供 error() 处理器外,RxJS 还提供了 catchError 操做符,它容许你在管道中处理已知错误。

假设你有一个可观察对象,它发起 API 请求,而后对服务器返回的响应进行映射。若是服务器返回了错误或值不存在,就会生成一个错误。若是你捕获这个错误并提供了一个默认值,流就会继续处理这些值,而不会报错。

下面是使用 catchError 操做符实现这种效果的例子:

import { ajax } from 'rxjs/ajax';
import { map, catchError } from 'rxjs/operators';
// Return "response" from the API. If an error happens,
// return an empty array.
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'); }
});

重试失败的可观察对象

catchError 提供了一种简单的方式进行恢复,而 retry 操做符让你能够尝试失败的请求。

能够在 catchError 以前使用 retry 操做符。它会订阅到原始的来源可观察对象,它能够从新运行致使结果出错的动做序列。若是其中包含 HTTP 请求,它就会从新发起那个 HTTP 请求。

下列代码为前面的例子加上了捕获错误前重发请求的逻辑:

import { ajax } from 'rxjs/ajax';
import { map, retry, catchError } from 'rxjs/operators';

const apiData = ajax('/api/data').pipe(
  retry(3), // Retry up to 3 times before failing
  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'); }
});

可观察对象的命名约定

因为 Angular 的应用几乎都是用 TypeScript 写的,你一般会但愿知道某个变量是否可观察对象。虽然 Angular 框架并无针对可观察对象的强制性命名约定,不过你常常会看到可观察对象的名字以“$”符号结尾。

这在快速浏览代码并查找可观察对象值时会很是有用。一样的,若是你但愿用某个属性来存储来自可观察对象的最近一个值,它的命名惯例是与可观察对象同名,但不带“$”后缀。

import { Component } from '@angular/core';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-stopwatch',
  templateUrl: './stopwatch.component.html'
})
export class StopwatchComponent {

  stopwatchValue: number;
  stopwatchValue$: Observable<number>;

  start() {
    this.stopwatchValue$.subscribe(num =>
      this.stopwatchValue = num
    );
  }
}

Angular中的可观察对象

Angular 使用可观察对象做为处理各类经常使用异步操做的接口。好比:

  • EventEmitter 类派生自 Observable

  • HTTP 模块使用可观察对象来处理 AJAX 请求和响应。

  • 路由器和表单模块使用可观察对象来监听对用户输入事件的响应。

事件发送器EventEmitter

Angular 提供了一个 EventEmitter 类,它用来从组件的 @Output() 属性中发布一些值。EventEmitter 扩展了 Observable,并添加了一个 emit() 方法,这样它就能够发送任意值了。当你调用 emit() 时,就会把所发送的值传给订阅上来的观察者的 next() 方法。

这种用法的例子参见 EventEmitter 文档。下面这个范例组件监听了 open 和 close 事件:

<zippy (open)="onOpen($event)" (close)="onClose($event)"></zippy>

@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) {
    // Create a new Observable the publishes only the NavigationStart event
    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));
  }
}

响应式表单

响应式表单具备一些属性,它们使用可观察对象来监听表单控件的值。 FormControl 的 valueChanges 属性和 statusChanges 属性包含了会发出变动事件的可观察对象。订阅可观察的表单控件属性是在组件类中触发应用逻辑的途径之一。好比:

import { FormGroup } from '@angular/forms';

@Component({
  selector: 'my-component',
  template: 'MyComponent Template'
})
export class MyComponent implements OnInit {
  nameChangeLog: string[] = [];
  heroForm: FormGroup;

  ngOnInit() {
    this.logNameChange();
  }
  logNameChange() {
    const nameControl = this.heroForm.get('name');
    nameControl.valueChanges.forEach(
      (value: string) => this.nameChangeLog.push(value)
    );
  }
}

输入提示(type-ahead)建议

可观察对象能够简化输入提示建议的实现方式。典型的输入提示要完成一系列独立的任务:

  • 从输入中监听数据。

  • 移除输入值先后的空白字符,并确认它达到了最小长度。

  • 防抖(这样才能防止连续按键时每次按键都发起 API 请求,而应该等到按键出现停顿时才发起)

  • 若是输入值没有变化,则不要发起请求(好比按某个字符,而后快速按退格)。

  • 若是已发出的 AJAX 请求的结果会由于后续的修改而变得无效,那就取消它。

彻底用 JavaScript 的传统写法实现这个功能可能须要大量的工做。使用可观察对象,你能够使用这样一个 RxJS 操做符的简单序列:

import { fromEvent } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { map, filter, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';

const searchBox = document.getElementById('search-box');

const typeahead = fromEvent(searchBox, 'input').pipe(
  map((e: KeyboardEvent) => e.target.value),
  filter(text => text.length > 2),
  debounceTime(10),
  distinctUntilChanged(),
  switchMap(() => ajax('/api/endpoint'))
);

typeahead.subscribe(data => {
 // Handle the data from the API
});

指数化退避

指数化退避是一种失败后重试 API 的技巧,它会在每次连续的失败以后让重试时间逐渐变长,超过最大重试次数以后就会完全放弃。 若是使用承诺和其它跟踪 AJAX 调用的方法会很是复杂,而使用可观察对象,这很是简单:

import { pipe, range, timer, zip } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { retryWhen, map, mergeMap } from 'rxjs/operators';

function backoff(maxTries, ms) {
 return pipe(
   retryWhen(attempts => range(1, maxTries)
     .pipe(
       zip(attempts, (i) => i),
       map(i => i * i),
       mergeMap(i =>  timer(i * ms))
     )
   )
 );
}

ajax('/api/endpoint')
  .pipe(backoff(3, 250))
  .subscribe(data => handleData(data));

function handleData(data) {
  // ...
}

可观察对象与其余技术的比较

你能够常用可观察对象(Observable)而不是承诺(Promise)来异步传递值。 相似的,可观察对象也能够取代事件处理器的位置。最后,因为可观察对象传递多个值,因此你能够在任何可能构建和操做数组的地方使用可观察对象。

在这些状况下,可观察对象的行为与其替代技术有一些差别,不过也提供了一些显著的优点。下面是对这些差别的详细比较。

可观察对象VS承诺

可观察对象常常拿来和承诺进行对比。有一些关键的不一样点:

  • 可观察对象是声明式的,在被订阅以前,它不会开始执行。承诺是在建立时就当即执行的。这让可观察对象可用于定义那些应该按需执行的菜谱。

  • 可观察对象能提供多个值。承诺只提供一个。这让可观察对象可用于随着时间的推移获取多个值。

  • 可观察对象会区分串联处理和订阅语句。承诺只有 .then() 语句。这让可观察对象可用于建立供系统的其它部分使用而不但愿当即执行的复杂菜谱。

  • 可观察对象的 subscribe() 会负责处理错误。承诺会把错误推送给它的子承诺。这让可观察对象可用于进行集中式、可预测的错误处理。

建立与订阅

在有消费者订阅以前,可观察对象不会执行。subscribe() 会执行一次定义好的行为,而且能够再次调用它。每次订阅都是单独计算的。从新订阅会致使从新计算这些值。

// declare a publishing operation
new Observable((observer) => { subscriber_fn });
// initiate execution
observable.subscribe(() => {
      // observer handles notifications
    });

承诺会当即执行,而且只执行一次。当承诺建立时,会当即计算出结果。没有办法从新作一次。全部的 then 语句(订阅)都会共享同一次计算。

// initiate execution
new Promise((resolve, reject) => { executer_fn });
// handle return value
promise.then((value) => {
      // handle result here
    });

串联

可观察对象会区分各类转换函数,好比映射和订阅。只有订阅才会激活订阅者函数,以开始计算那些值。

observable.map((v) => 2*v);

承诺并不区分最后的 .then() 语句(等价于订阅)和中间的 .then() 语句(等价于映射)。

promise.then((v) => 2*v);

可取消

可观察对象的订阅是可取消的。取消订阅会移除监听器,使其再也不接受未来的值,并通知订阅者函数取消正在进行的工做。

const sub = obs.subscribe(...);
sub.unsubscribe();

错误处理

可观察对象的错误处理是交给订阅者的错误处理器的,而且该订阅者会自动取消对这个可观察对象的订阅。

obs.subscribe(() => {
throw Error('my error');
});

承诺会把错误推给其子承诺。

promise.then(() => {
throw Error('my error');
});

速查表

操做

可观察对象

承诺

建立

new Observable((observer) => {
  observer.next(123);
});
new Promise((resolve, reject) => {
  resolve(123);
});

转换

obs.map((value) => value * 2 );
promise.then((value) => value * 2);

订阅

sub = obs.subscribe((value) => {
  console.log(value)
});
promise.then((value) => {
  console.log(value);
});

取消订阅

sub.unsubscribe();

承诺被解析时隐式完成。

 

可观察对象VS事件API

可观察对象和事件 API 中的事件处理器很像。这两种技术都会定义通知处理器,并使用它们来处理一段时间内传递的多个值。订阅可观察对象与添加事件处理器是等价的。一个显著的不一样是你能够配置可观察对象,使其在把事件传给事件处理器之间先进行转换。

使用可观察对象来处理错误和异步操做在 HTTP 请求这样的场景下更加具备一致性。

下列代码片断揭示了一样的操做要如何分别使用可观察对象和事件 API 进行实现。

 

可观察对象

事件 API

建立与取消

// Setup
let clicks$ = fromEvent(buttonEl, ‘click’);
// Begin listening
let subscription = clicks$
  .subscribe(e => console.log(‘Clicked’, e))
// Stop listening
subscription.unsubscribe();
function handler(e) {
  console.log(‘Clicked’, e);
}

// Setup & begin listening
button.addEventListener(‘click’, handler);
// Stop listening
button.removeEventListener(‘click’, handler);

订阅

observable.subscribe(() => {
  // notification handlers here
});
element.addEventListener(eventName, (event) => {
  // notification handler here
});

配置

监听按键,提供一个流来表示这些输入的值。

fromEvent(inputEl, 'keydown').pipe(
  map(e => e.target.value)
);

不支持配置。

element.addEventListener(eventName, (event) => {
  // Cannot change the passed Event into another
  // value before it gets to the handler
});

 

启动过程

Angular模块类描述应用的部件是如何组合在一块儿的,每一个应用都至少有一个Angular模块,也就是根模块。

用来引导并运行应用,常规名字是AppModule。下面是最小的AppModule

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent }  from './app.component';

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

@NgModule装饰器将AppModule标记为模块类,接受一个元数据对象,告诉Angular如何编译和启用

imports数组

Angular模块把特性合并成离散单元的一种方式,Angular自身的许多特性也是经过Angular模块组织的。

HTTP服务在HttpModule里,路由器在RouterModule里。当应用须要模块的特性时,将其添加到此数组中。

NgMobile属于FormsModule,RouterLink属于RouterModule

declarations数组

告诉Angular那个组件属于AppModule,只要建立组件都应该放在此数组里。不然会报错。

bootstrap数组

经过引导根AppModule来启动应用,建立列在此数组的组件,并把他们插入浏览器的DOM中。

providers数组

提供依赖注入类,能够从构造函数注入

特性模块

特性模块是带有@NgModule装饰器及其元数据的类,就像根模块同样。特性模块的元数据和根模块的元数据的属性是同样的。

根模块和特性模块共享着相同的执行环境,共享着同一个依赖注入器,意味着某个模块中定义的服务在全部模块中都能使用。

能够解决如下问题:

1.随着一个个类被加入到应用中,根模块AppModule变大

2.指令冲突

3.和其余特性区之间缺少清晰的边界,致使难以在不一样的开发组之间分配职责

他在技术上有两个显著的不一样点

1.咱们引导根模块来启动应用,但导入特性模块来扩展应用

2.特性模块能够对其余模块暴露或隐藏本身的实现

特性模块用来提供内聚的功能集合,应用于某个领域、工做流、基础设施

建立特性模块

import { NgModule }           from '@angular/core';
import { CommonModule }       from '@angular/common';
import { FormsModule }        from '@angular/forms';
 
import { AwesomePipe }        from './awesome.pipe';
 
import { ContactComponent }   from './contact.component';
import { ContactService }     from './contact.service';
import { HighlightDirective } from './highlight.directive';
 
@NgModule({
  imports:      [ CommonModule, FormsModule ],
  declarations: [ ContactComponent, HighlightDirective, AwesomePipe ],
  exports:      [ ContactComponent ],
  providers:    [ ContactService ]
})
export class ContactModule { }

而后在app.module中,填写这条特性模块

imports:      [ BrowserModule, ContactModule ],

Angular模块常见问题

应该添加那些类到declarations中

把可声明的类(组件、指令和管道)添加到declarations列表中。

这些类只能在应用程序的一个而且只有一个模块中声明,只有当他们从属于某个模块时,才能把此模块中声明它们。

可声明指的是:组件、指令和管道等能够被加到模块的declarations列表中的类。

哪些类不须要declarations中

只有可声明的类才能加到模块的declarations列表中

如下状况不须要添加:

1.已经在其余模块中声明过的类,不管它来自应用本身的模块(@NgModule)仍是第三方模块

2.其余模块中导入的指令。

3.模块类

4.服务类

5.非Angular类和对象

https://angular.cn/guide/ngmodule-faq

依赖注入

依赖注入(DI)是用来建立对象及其依赖的其它对象的一种方式。 当依赖注入系统建立某个对象实例时,会负责提供该对象所依赖的对象(称为该对象的依赖)

ng generate service heroes/hero   建立服务

配置注入器

不须要建立Angular注入器,Angular启动过程当中自动为咱们建立一个应用级注入器。

//main.ts

platformBrowserDynamic().bootstrapModule(AppModule);

必须经过注册提供商(provider)来配置注入器,提供商为应用建立所需服务。

@Injectable的providers数组

@Injectable 装饰器会指出这些服务或其它类是用来注入的。它还能用于为这些服务提供配置项。

这里咱们使用类上的 @Injectable 装饰器来为 HeroService 配置了一个提供商。

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class HeroService {
  constructor() { }
}

 

providedIn 告诉 Angular,它的根注入器要负责调用 HeroService 类的构造函数来建立一个实例,并让它在整个应用中都是可用的。在使用 CLI 生成新服务时,会默认帮你设置为这种提供商。

这种状况下,服务提供商应该关联到一个特定的 @NgModule 类,并且应该用于该模块包含的任何一个注入器中。

@Injectable 装饰器用来配置一个服务提供商,它能够用在任何包含了 HeroModule 的注入器中。

import { Injectable } from '@angular/core';
import { HeroModule } from './hero.module';
import { HEROES }     from './mock-heroes';

@Injectable({
  // we declare that this service should be created
  // by any injector that includes HeroModule.

  providedIn: HeroModule,
})
export class HeroService {
  getHeroes() { return HEROES; }
}

在NgModule中注册提供商

@NgModule({
  imports: [
    BrowserModule
  ],
  declarations: [
    AppComponent,
    CarComponent,
    HeroesComponent
  ],
  providers: [
    UserService,
    { provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

第一条使用UserService这个注入令牌(injection token)注册了UserService类。

第二条使用APP_CONFIG这个注入令牌注册了一个值(HERO_DI_CONFIG)

在组件中注册提供商

import { Component }          from '@angular/core';

import { HeroService }        from './hero.service';

@Component({
  selector: 'app-heroes',
  providers: [HeroService],
  template: `
  <h2>Heroes</h2>
  <app-hero-list></app-hero-list>
  `
})
export class HeroesComponent { }

@Injectable,@NgModule仍是@Component

该使用 @Injectable 装饰器、@NgModule仍是 @Component 来提供服务。差异在于最终的打包体积、服务的范围和服务的生命周期。

当你在服务自己的 @Injectable 装饰器中注册提供商时,优化工具(好比 CLI 产品模式构建时所用的)能够执行摇树优化,这会移除全部没在应用中使用过的服务。摇树优化会致使更小的打包体积。

Angular 模块中的 providers@NgModule.providers)是注册在应用的根注入器下的。 所以,Angular 能够往它所建立的任何类中注入相应的服务。 一旦建立,服务的实例就会存在于该应用的所有生存期中,Angular 会把这一个服务实例注入到需求它的每一个类中。

组件的提供商(@Component.providers)会注册到每一个组件实例本身的注入器上。

所以 Angular 只能在该组件及其各级子组件的实例上注入这个服务实例,而不能在其它地方注入这个服务实例。

注意,由组件提供的服务,也一样具备有限的生命周期。组件的每一个实例都会有它本身的服务实例,而且,当组件实例被销毁的时候,服务的实例也一样会被销毁。

在这个范例应用中,HeroComponent 会在应用启动时建立,而且它从未销毁,所以,由 HeroComponent 建立的 HeroService 也一样会活在应用的整个生命周期中。

若是你要把 HeroService 的访问权限定在 HeroesComponent 及其嵌套的 HeroListComponent中,那么在 HeroesComponent 中提供这个 HeroService 就是一个好选择。

@Injectable()

标识一个类能够被注入器实例化,一般在视图实例化没有被标识为@Injectable类时,注入会报错。

建议为每一个服务类,都添加@Injectable()装饰器

import {Injectable} from '@angular/core';

@Injectable()
export class HeroService{

}

Provide类和提供商字面量

providers:[Logger]

这实际上是用于注册提供商的简写表达式,使用的是一个带有两个属性的提供商字面量

[{ provide: Logger, useClass: Logger }]

第一个是令牌(token),做为键值(key)使用,用于定位依赖值和注册提供商

第二个是提供商定义对象,能够当作是如何建立依赖值的配方。

有些时候,若是咱们须要请求一个不一样的类来提供服务,能够写成以下形式

[{ provide: Logger, useClass: BetterLogger }]

当有人请求Logger时,返回BetterLogger。注意:BetterLogger必须继承Logger

值提供商

能够提供一个预先作好的对象,会比请求注入器从类中建立它更容易

值得注意的是,对象必须与类一致。

import {Injectable} from '@angular/core';

@Injectable()
export class Logger {
  logs: string[] = []; // capture logs for testing

  log(message: string) {
    this.logs.push(message);
    console.log(message);
  }
}
providers: [
    {
      provide: Logger, useValue: {
        logs: ['Log'],
        log: () => {
          console.log(`123`);
        }
      }
    }
  ]

工厂提供商

有时候须要动态建立依赖值,由于所须要的信息直到最后一刻才能肯定。这种状况下,须要调用工厂提供商

建立工厂方法

//hero.service.ts
constructor(
  private logger: Logger,
  private isAuthorized: boolean) { }

getHeroes() {
  let auth = this.isAuthorized ? 'authorized ' : 'unauthorized';
  this.logger.log(`Getting heroes for ${auth} user.`);
  return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
}

//hero.service.provider.ts
let heroServiceFactory = (logger: Logger, userService: UserService) => {
  return new HeroService(logger, userService.user.isAuthorized);
};

同时把Logger和UserService注入到工厂提供商中,而且让注入器把他们传给工厂

export let heroServiceProvider =
  { provide: HeroService,
    useFactory: heroServiceFactory,
    deps: [Logger, UserService]
  };

useFactory字段告诉Angular:这个提供商是一个工厂方法,实现是heroServiceFactory

deps属性是提供商令牌数组,Logger和UserService类做为他们自身的提供商的令牌

import { Component }          from '@angular/core';
 
import { heroServiceProvider } from './hero.service.provider';
 
@Component({
  selector: 'app-heroes',
  template: `
  <h2>Heroes</h2>
  <app-hero-list></app-hero-list>
  `,
  providers: [heroServiceProvider]
})
export class HeroesComponent { }

依赖注入令牌

向注入器注册提供商时,实际上就是把提供商和DI令牌关联起来。

注入器维护一个内部的令牌-提供商映射表,会在请求依赖时被引用 。令牌就是这个映射表的键值。

前面的全部例子中,依赖值都是一个类实例,而且类的类型做为它本身的查找键值。

编写须要基于类的依赖注入的构造函数很简单,只须要添加参数便可。

非类依赖

若是依赖值不是一个类,想注入的东西是一个字符串、函数或者对象。例如全局配置

export interface AppConfig {
  apiEndpoint: string;
  title: string;
}

export const HERO_DI_CONFIG: AppConfig = {
  apiEndpoint: 'api.heroes.com',
  title: 'Dependency Injection'
};

使用值提供商来注册一个对象。

可是,没办法找到一个类当作令牌,由于没有Config类。TypeScript不能把接口用做令牌。

// FAIL! Can't inject using the interface as the parameter type
constructor(private config: AppConfig){ }

InjectionToKend值

解决方案是为非类依赖定义和使用InjectionToken值做为提供商令牌。总体代码以下:

//app-config
export interface AppConfig {
  message: string;
}

export const HERO_CONFIG: AppConfig = {
  message: 'HeroConfig'
}

//app.config
import {InjectionToken} from '@angular/core';

import {AppConfig, HERO_CONFIG} from './app-config';

export let APP_CONFIG = new InjectionToken<AppConfig>('app.config');

//providers.component
import {Component, Inject} from '@angular/core';

import {AppConfig, HERO_CONFIG} from './app-config';
import {APP_CONFIG} from './app.config';

@Component({
  selector: 'app-provider',
  template: `
    <div>{{config.message}}</div>
  `,
  providers: [{provide: APP_CONFIG, useValue: HERO_CONFIG}]
})

export class ProvidersComponent {
  constructor(@Inject(APP_CONFIG) private config: AppConfig) {

  }
}

new InjectionToKend的参数是可选的,填写之后能够给开发者和开发工具传递信息,提供描述做用。

注册的时候,直接注册APP_CONFIG这个变量

构造函数的时候,须要使用@Inject装饰器来获取实际的值

可选依赖 

若是不想提供构造函数,也想调用类,能够把构造函数标记为@Optional(),告诉Angular该依赖可选。

import { Optional } from '@angular/core';

constructor(@Optional() private logger: Logger) {
  if (this.logger) {
    this.logger.log(some_message);
  }
}

当使用Optional的时候,代码必须准备好如何处理空值,这是参数为null

多级依赖注入 

Angular有一个多级依赖注入系统,应用程序中有一个与组件树平行的注入器树。能够在组件树中的任何级别上从新配置注入器。

能够在组件树中的任何级别上从新配置注入器。

注入器冒泡

当一个组件申请得到一个依赖时,Angular先尝试用该组件本身的注入器来知足它。

若是该组件的注入器没有找到对应的提供商,就把这个申请转交给他父组件的注入器来处理。

若是那个注入器也没法知足这个申请,就继续传递给它的父组件的注入器。按照这个顺序继续往上冒泡,知道找到注入器为止。

DI使用技巧

https://angular.cn/guide/dependency-injection-in-action

HttpClient库

大多数前端应用都须要经过HTTP协议与后端服务器通信,浏览器支持两种不一样API发起HTTP请求:XMLHttpRequest接口和fetch()API

@angular/common/http中的HttpClient类,Angular为应用程序提供了一个简化的API来实现HTTP功能。

HttpClient带来其余的优势包括:可测试性、强类型的请求和响应对象、发起请求与接收响应时的拦截器支持,更好的、基于可观察(Observable)的对象错误处理机制。

安装

首先须要在app.module中的imports暴露出来

import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
 
// Import HttpClientModule from @angular/common/http
import {HttpClientModule} from '@angular/common/http';
 
@NgModule({
  imports: [
    BrowserModule,
    // Include it under 'imports' in your application module
    // after BrowserModule.
    HttpClientModule,
  ],
})
export class MyAppModule {}

获取请求 

HttpClient的get方法能够直接从api获取数据

@Component(...)
export class MyComponent implements OnInit {

  results: string[];

  // Inject HttpClient into your component or service.
  constructor(private http: HttpClient) {}

  ngOnInit(): void {
    // Make the HTTP request:
    this.http.get('/api/items').subscribe(data => {
      // Read the result field from the JSON response.
      this.results = data['results'];
    });
  }
}

响应类型检查 

能够告诉HttpClient这个响应体应该是什么类型的,这是推荐作法。

首先要定义接口来描述这个类型的正确形态

interface ItemsResponse {
  results: string[];
}

而后,发起HttpClient.get调用时,传入一个类型参数:

http.get<ItemsResponse>('/api/items').subscribe(data => {
  // data is now an instance of type ItemsResponse, so you can do this:
  this.results = data.results;
});

完整响应体

响应体可能并不包含咱们须要的所有信息,有时候服务器会返回一个特殊的响应头或状态码。

须要经过observe选项来告诉HttpClient,你想要完整的响应信息,而不是只有响应体。

http
  .get<MyJsonData>('/data.json', {observe: 'response'})
  .subscribe(resp => {
    // Here, resp is of type HttpResponse<MyJsonData>.
    // You can inspect its headers:
    console.log(resp.headers.get('X-Custom-Header'));
    // And access the body directly, which is typed as MyJsonData as requested.
    console.log(resp.body.someField);
  });

错误处理 

若是请求致使了服务器错误,HttpClient会返回error。要处理他能够在subscribe中添加错误处理器

http
  .get<ItemsResponse>('/api/items')
  .subscribe(
    // Successful responses call the first callback.
    data => {...},
    // Errors will call this callback instead:
    err => {
      console.log('Something went wrong!');
    }
  );

错误分为两种

1.后端返回一个失败的返回码(404,500) ,会返回一个错误

2.客户端这边出现错误,会抛出一个Error类型的异常

能够经过instanceof来判断是那种类型

http
  .get<ItemsResponse>('/api/items')
  .subscribe(
    data => {...},
    (err: HttpErrorResponse) => {
      if (err.error instanceof Error) {
        // A client-side or network error occurred. Handle it accordingly.
        console.log('An error occurred:', err.error.message);
      } else {
        // The backend returned an unsuccessful response code.
        // The response body may contain clues as to what went wrong,
        console.log(`Backend returned code ${err.status}, body was: ${err.error}`);
      }
    }
  );

.retry()操做符 

能够重试请求,对于临时性的并且不太可能重复发生的错误颇有用。

RxJS有一个叫.retry()的颇有用的操做符,会在遇到错误时自动从新订阅这个可观察对象,会再次发送请求

import 'rxjs/add/operator/retry';

http
  .get<ItemsResponse>('/api/items')
  // Retry this request up to 3 times.
  .retry(3)
  // Any errors after the 3rd retry will fall through to the app.
  .subscribe(...);

请求非JSON数据

若是须要请求的数据不是JSON,须要告诉HttpClient指望的响应类型。

http
  .get('/textfile.txt', {responseType: 'text'})
  // The Observable returned by get() is of type Observable<string>
  // because a text response was specified. There's no need to pass
  // a <string> type parameter to get().
  .subscribe(data => console.log(data));

发起POST请求

const body = {name: 'Brad'};

http
  .post('/api/developers/add', body)
  // See below - subscribe() is still necessary when using post().
  .subscribe(...);

注意,subscribe方法,全部从HttpClient返回的可观察对象都是冷的,也就是说,还未发起请求。

在咱们调用subscribe方法以前,什么都不会发生,而当咱们每次调用subscribe时,就会独立发起一次请求。

好比,下面代码会发送两次请求

const req = http.post('/api/items/add', body);
// 0 requests made - .subscribe() not called.
req.subscribe();
// 1 request made.
req.subscribe();
// 2 requests made.

配置请求中的其余部分

除了URL和可能的请求体以外,能够经过传递options对象来解决,HTTP头

添加Authorization头

http
  .post('/api/items/add', body, {
    headers: new HttpHeaders().set('Authorization', 'my-auth-token'),
  })
  .subscribe();

添加URL参数

http
  .post('/api/items/add', body, {
    params: new HttpParams().set('id', '3'),
  })
  .subscribe();

实际发送的请求会变成,/api/items/add?id=3 这个地址 

拦截器

@angular/common/http 的主要特性之一是拦截器,能声明一些拦截器,拦在应用和后端之间。

当应用程序发起一个请求时,拦截器能够在请求被发往服务器之间先转换这个请求。

而且在应用看到服务器发回来的响应以前,转换这个响应。对于处理包括认证和记录日志在内的一系列工做都很是有用。

要实现一个拦截器,就要声明一个实现HttpInterceptor接口的类,只有一个intercept方法。

import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {HttpEvent, HttpInterceptor, HttpHandler, HttpRequest} from '@angular/common/http';
@Injectable() export
class NoopInterceptor implements HttpInterceptor { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(req); } }

intercept是一个方法,它把一个请求对象转换成一个返回这个响应的可观察对象(Observables)。 每一个拦截器都要本身处理请求。

大多时候,拦截器会对请求作一些小修改,而后才传给拦截器链中的其余部分,这就是传进来的next参数。

next是一个HttpHandler,是一个相似于intercept的接口,会把一个请求对象转换成一个可观察的响应对象。

拦截器中next老是表明位于拦截器链中的下一个拦截器,若是没有更多的拦截器,就会是最终的后端。

大多数拦截器的最后一句都会以它们转换后请求对象为参数调用next.handle函数。上面代码就是简单的请求,不作修改。

应用模块设置拦截器

import {NgModule} from '@angular/core';
import {HTTP_INTERCEPTORS} from '@angular/common/http';

@NgModule({
  providers: [{
    provide: HTTP_INTERCEPTORS,
    useClass: NoopInterceptor,
    multi: true,
  }],
})
export class AppModule {}

multi:true是必须的,会告诉Angular这个HTTP_INTERCEPTORS表示一个数组,而不单是一个值。

注意,intercept和Handler.handle返回的可观察对象并非Observable<HttpResponse<any>>而是Observable<HttpEvent<any>>

拦截器所工做的层级要低于HttpClient接口,单个请求会生成多个事件。好比上传、下载过程的事件。

HttpReponse类实际上自己也是一个事件,只是它的type是HttpEventType.HttpResponseEvent

拦截器必须透传全部它不理解或不打算修改的事件,不能过滤本身不许备处理的事件。

顺序

当咱们设置多个拦截器时,会按照你提供的顺序应用他们,即Providers数组中列出的顺序

不可变性 

拦截器要检查和修改准备发出的请求和接受进来的响应。可是HttpRequest和HttpResponse类倒是不可变的

由于应用可能会重发请求,而拦截器链可能会屡次处理同一个请求。若是请求是可变的,每次重试时的请求均可能和原始的请求不同。

而不可变对象能够确保拦截器每次重试时处理的都是同一个请求。

请求体(body)没法在写拦截器时提供保护,在拦截器修改请求体应该是无效的,可是类型检查系统没法发现它

若是确实须要修改请求体,就得本身复制它,修改这个复本,而后clone()复制这个请求,并使用新的请求体

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  // This is a duplicate. It is exactly the same as the original.
  const dupReq = req.clone();

  // Change the URL and replace 'http://' with 'https://'
  const secureReq = req.clone({url: req.url.replace('http://', 'https://')});
}

设置HTTP头 

拦截器最多见用途之一是设置默认的请求头,好比有一个可注入的AuthService,能够提供一个认证令牌。

而咱们但愿写一个拦截器,把这个令牌添加到全部要发出的请求中。

import {Injectable} from '@angular/core';
import {HttpEvent, HttpInterceptor, HttpHandler, HttpRequest} from '@angular/common/http';
 
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private auth: AuthService) {}
 
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Get the auth header from the service.
    const authHeader = this.auth.getAuthorizationHeader();
    // Clone the request to add the new header.
    const authReq = req.clone({headers: req.headers.set('Authorization', authHeader)});
    // Pass on the cloned request instead of the original request.
    return next.handle(authReq);
  }
}

这种克隆一个请求并设置一组新的请求头的操做很是常见,所以有了一种快捷写法

const authReq = req.clone({setHeaders: {Authorization: authHeader}});

这种能够修改头的拦截器,能够用于不少不一样的操做。好比:

认证/受权、控制缓存行为,好比If-Modified-Since、XSRF防御

记日志 

因为拦截器能够同时处理请求和响应,能够用来记录日志或请求计时等。 下面是记录请求时长的例子

import {HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse} from '@angular/common/http';
import {Observable} from 'rxjs';

export class TimingInterceptor implements HttpInterceptor {
  constructor(private auth: AuthService) {
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const started = Date.now();
    return next.handle(req).do(event => {
      if (event instanceof HttpResponse) {
        const elapsed = Date.now() - started;
        console.log(`Request for ${req.urlWithParams} took ${elapsed} ms.`);
      }
    });
  }
}

 注意,RxJS的do操做符,能够为可观察对象添加一个反作用,而不会影响到流中的值。

这里,会检测HttpResponse的事件,并记录这个请求所花费的时间。

缓存

能够使用拦截器来实现缓存,好比,已经写好的一个HTTP缓存,具备以下简单的接口

abstract class HttpCache {
  /**
   * Returns a cached response, if any, or null if not present.
   */
  abstract get(req: HttpRequest<any>): HttpResponse<any>|null;

  /**
   * Adds or updates the response in the cache.
   */
  abstract put(req: HttpRequest<any>, resp: HttpResponse<any>): void;
}

拦截器能够把这个请求缓存应用到所发出的请求上

@Injectable()
export class CachingInterceptor implements HttpInterceptor {
  constructor(private cache: HttpCache) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
      // Before doing anything, it's important to only cache GET requests.
    // Skip this interceptor if the request method isn't GET.
    if (req.method !== 'GET') {
      return next.handle(req);
    }

    // First, check the cache to see if this request exists.
    const cachedResponse = this.cache.get(req);
    if (cachedResponse) {
      // A cached response exists. Serve it instead of forwarding
      // the request to the next handler.
      return Observable.of(cachedResponse);
    }

    // No cached response exists. Go to the network, and cache
    // the response when it arrives.
    return next.handle(req).do(event => {
      // Remember, there may be other events besides just the response.
      if (event instanceof HttpResponse) {
          // Update the cache.
          this.cache.put(req, event);
      }
    });
  }
}

拦截器除了转换请求外,还有不少强力的功能。能够彻底接管请求流程。

修改上面例子,改成若是有请求,返回两个响应 

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  // Still skip non-GET requests.
  if (req.method !== 'GET') {
    return next.handle(req);
  }

  // This will be an Observable of the cached value if there is one,
  // or an empty Observable otherwise. It starts out empty.
  let maybeCachedResponse: Observable<HttpEvent<any>> = Observable.empty();

  // Check the cache.
  const cachedResponse = this.cache.get(req);
  if (cachedResponse) {
    maybeCachedResponse = Observable.of(cachedResponse);
  }

  // Create an Observable (but don't subscribe) that represents making
  // the network request and caching the value.
  const networkResponse = next.handle(req).do(event => {
    // Just like before, check for the HttpResponse event and cache it.
    if (event instanceof HttpResponse) {
      this.cache.put(req, event);
    }
  });

  // Now, combine the two and send the cached response first (if there is
  // one), and the network response second.
  return Observable.concat(maybeCachedResponse, networkResponse);
}

若是URL被缓存过,那么任何人调用http.get(URL)都会收到两个响应。

监听进度事件

有时候应用须要传输大量数据,过程当中给用户反馈能带来更好的体验,而@angular/common/http就支持它

要发起要给支持进度事件的请求,首先要建立一个reportProgress选项的一个HttpRequest实例

const req = new HttpRequest('POST', '/upload/file', file, {
  reportProgress: true,
});

该选项让咱们能够跟踪事件进度,每一个进度事件都会出发变动检测,因此只有真的打算在每一个事件中更新UI才打开它

接下来经过HttpClient的request方法发起请求,其结果应该是一个关于事件的可观察对象,就像拦截器中看到的那样

http.request(req).subscribe(event => {
  // Via this API, you get access to the raw event stream.
  // Look for upload progress events.
  if (event.type === HttpEventType.UploadProgress) {
    // This is an upload progress event. Compute and show the % done:
    const percentDone = Math.round(100 * event.loaded / event.total);
    console.log(`File is ${percentDone}% uploaded.`);
  } else if (event instanceof HttpResponse) {
    console.log('File is completely uploaded!');
  }
});

安全XSRF防御 

跨请求伪造(XSRF)是一个攻击技术,能让攻击者假冒一个已认证的用户在你的网站上进行未知的操做。

HttpClient支持一种通用的机制来防范XSRF攻击,当执行HTTP请求时,一个拦截器会从cookie中读取XSRF令牌(默认名字为XSRF-TOKEN)

而且把它设置成一个HTTP头X-XSRF-TOKEN,因为只有运行在咱们本身的域名下的代码才能读取到这个cookie

所以后端能够确认这个HTTP请求真的来自咱们的客户端应用,而不是攻击者。

默认状况下,拦截器会在全部的修改型请求中(好比POST等)把这个cookie发送给使用相对URL的请求,但不会在GET/HEAD请求中发送,也不会发送给使用绝对URL的请求。

要得到这种优势,咱们的服务器须要在页面加载或首个GET请求中把一个名叫XSRF-TOKEN的令牌写入可被JavaScript读取到的会话cookie。

而在后续的请求中,服务器能够验证这个cookie是否与HTTP头X-XSRF-TOKEN的值一致。以确保只有运行时在咱们本身的域名下的代码才能发起这个请求

这个令牌必须对每一个用户都是惟一的,而且必须能被服务器验证,所以不能由客户端生成本身的令牌,把这个令牌设置为你的站点认证信息并加了摘要,提高安全性。

为了防止多个Angular应用共享同一个域名或子域的时候出现冲突,要给每一个应用分配一个惟一的cookie名称。

配置自定义cookie/hander名称

若是咱们的后端服务中对XSRF令牌的cookie或头使用了不同的名称,就要使用HttpClientXsfModule.withConfig来覆盖掉默认值

imports: [
  HttpClientModule,
  HttpClientXsrfModule.withConfig({
    cookieName: 'My-Xsrf-Cookie',
    headerName: 'My-Xsrf-Header',
  }),
]

路由与导航

Angular的路由器能让用户从一个视图导航到另外一个视图

浏览器具备咱们熟悉的导航模式:

1.在地址栏输入URL,浏览器就会导航到相应的页面

2.在页面中点击连接,浏览器就会导航到一个新页面。

3.点击浏览器的前进和后退按钮,浏览器就会在你的浏览记录中向前或向后导航。

Angular的Router借鉴了这个模型,它把浏览器中的URL看做一个操做指南,据此导航到一个由客户端生成的视图,并能够把参数传给支撑视图的相应组件

帮他决定具体该展示哪些内容,咱们能够为页面中的连接绑定一个路由,这样当用户点击连接时,就会导航到相应的视图中。

当用户点击按钮、从下拉框拉取,或响应来自任何地方的事件时,也能够在代码控制下进行导航。

路由器在浏览器的历史记录下这些活动,这样浏览器的前进和后退按钮也能照常工做。

<base href>元素

大多数带路由的应用都要在index.html的<head>标签下添加<base href="/">,来告诉路由器该如何合成导航用的URL。

路由导入

Angular的路由器是一个可选的服务,用来呈现指定的URL所对应的视图,并非Angular核心库的一部分,而是在他本身的@angular/router包中

像其余的Angular包同样,能够从它导入所需的一切。

import { RouterModule, Routes } from '@angular/router';

配置 

每一个带路由的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 } // <-- debugging purposes only
    )
    // other imports here
  ],
  ...
})
export class AppModule { }

这里的路由数组appRoutes描述如何进行导航,把它传递给RouterModule.forRoot方法并传给本模块的imports数组便可

每一个Router都会把一个URL的path映射到一个组件,注意path不能以斜杠(/)开头。路由器会解析和构建最终的URL。

这样当咱们在多个视图之间导航时,就能够任意使用相对、绝对路径了。

第一个路由器:是写的静态路径,对应的组件,当URL是该地址,则会跳转到对应组件。

第二个路由器:中的:id是一个路由参数的令牌(Token),好比/hero/42中42就是id参数的值。此URL对应的HeroDetailComponent组件将据此查找和展现id为42的数据

第三个路由器:中的data属性用来存放每一个具体路由相关的任意信息,该数据能够被任何一个激活路由访问,并能用来绑定静态只读数据

第四个路由器: 空路径表示应用的默认路径,URL为空会访问到哪里,所以一般会做为起点,默认重定向到/heroes路由。

第五个路由器:路径**是一个通配符,当所请求的URL不匹配前面定义的路由表中的任何路径时,会选择此路由。可用于404页面等

路由的定义顺序是刻意如此设计的,使用先匹配者优先的策略来匹配路由,因此具体路由应该放在通用路由的前面。

在上面配置中,带静态路径的路由被放在前面,后面是空路径路由,所以它会做为默认路由。

而通配符路由被放在最后面,这是由于他能匹配上每个URL,所以应该只有在前面找不到其余匹配的路由时才匹配它

若是咱们想看到在导航的生命周期中发生过那些事情,能够使用配置路由器的enableTracing选项,会把每一个导航生命周期中的事件输出到浏览器的控制台。

这应该只是用于调试,只须要把enableTracing:true选项做为第二个参数传递给RouterModule.forRoot方法就能够了。

路由出口

有了这份配置,当应用的URL变为对应路由时,就会匹配到对应组件。并在宿主视图中,RouterOutlet以后显示。

<router-outlet></router-outlet>
<!-- Routed views go here -->

路由器连接

template: `
  <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>
`

a标签上的RouterLink指令让路由器得以控制这个a元素,这里的导航路径是固定的,所以能够把一个字符串赋值给routeLink

若是须要动态添加导航路径,就须要把它绑定在一个返回连接参数数组的模板表达式,路由器会把这个数组解析成完整的URL

每一个a标签上的RouterLinkActive指令,能够帮用户在外观区分出当前选中的活动路由。

当与它关联的RouterLink被激活时,路由器会把CSS类active添加到这个元素上,能够把指令添加到a元素或它的父元素上。

路由器状态 

在导航时的每一个生命周期成功完成时,路由器会构建出一个ActivatedRoute组成的树。表示路由器的当前状态。

能够在应用中的任何地方用Router服务及其routerState属性来访问当前的RouterState值。

激活的路由

该路由的路径和参数能够经过注入一个名叫ActivatedRouter的路由服务来获取,他有一大堆信息,包括:

Property 属性 Description 描述
url

路由路径的Observable对象,是一个由路由路径中的各个部分组成的字符串数组。

data

一个Observable,其中包含提供给路由的data对象。也包含由解析守卫(resolve guard)解析而来的值。

paramMap

一个Observable,其中包含一个由当前路由的必要参数和可选参数组成的map对象。用这个map能够获取来自同名参数的单一值或多重值。

queryParamMap

一个Observable,其中包含一个对全部路由都有效的查询参数组成的map对象。 用这个map能够获取来自查询参数的单一值或多重值。

fragment

An Observable of the URL fragment available to all routes.

outlet

要把该路由渲染到的RouterOutlet的名字。对于无名路由,它的路由名是primary,而不是空串。

routeConfig

用于该路由的路由配置信息,其中包含原始路径。

parent

当该路由是一个子路由时,表示该路由的父级ActivatedRoute

firstChild

包含该路由的子路由列表中的第一个ActivatedRoute

children

包含当前路由下全部已激活的子路由

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

路由事件

每次导航中,Router都经过Router.event属性发布一些导航事件,这些事件的范围覆盖了从开始导航到结束导航之间的不少时间点。

Router Event 路由器事件 Description 描述
NavigationStart

事件会在导航开始时触发。

RoutesRecognized

事件会在路由器解析完URL,并识别出了相应的路由时触发

RouteConfigLoadStart

事件会在Router对一个路由配置进行惰性加载以前触发。

RouteConfigLoadEnd

事件会在路由被惰性加载以后触发。

NavigationEnd

事件会在导航成功结束以后触发。

NavigationCancel

事件会在导航被取消以后触发。 这多是由于在导航期间某个路由守卫返回了false

NavigationError

这个事件会在导航因为意料以外的错误而失败时触发。

 

 

 

 

 

 

 

 

 

 

 

当打开enableTracing选项时,这些事件也同时会记录到控制台中,因为这些事件是以Observable的形式提供的。

因此咱们能够对本身感兴趣的事件进行filter(),并subscribe()他们,以便根据导航过程当中的时间顺序作出决策。

总结

应用有一个配置过的路由器,外壳组件中有一个RouterOutlet,能显示路由器所生成的视图,还有一些RouterLink,用户能够点击他们来进行导航。

路由器部件

含义

Router(路由器)

为激活的URL显示应用组件。管理从一个组件到另外一个组件的导航

RouterModule(路由器模块)

一个独立的Angular模块,用于提供所需的服务提供商,以及用来在应用视图之间进行导航的指令。

Routes(路由数组)

定义了一个路由数组,每个都会把一个URL路径映射到一个组件。

Route(路由)

定义路由器该如何根据URL模式(pattern)来导航到组件。大多数路由都由路径和组件类构成。

RouterOutlet(路由出口)

该指令(<router-outlet>)用来标记出路由器该在哪里显示视图。

RouterLink(路由连接)

该指令用来把一个可点击的HTML元素绑定到路由。 点击带有绑定到字符串连接参数数组routerLink指令的元素就会触发一次导航。

RouterLinkActive(活动路由连接)

当HTML元素上或元素内的routerLink变为激活或非激活状态时,该指令为这个HTML元素添加或移除CSS类。

ActivatedRoute(激活的路由)

为每一个路由组件提供提供的一个服务,它包含特定于路由的信息,好比路由参数、静态数据、解析数据、全局查询参数和全局碎片(fragment)。

RouterState(路由器状态)

路由器的当前状态包含了一棵由程序中激活的路由构成的树。它包含一些用于遍历路由树的快捷方法。

连接参数数组

这个数组会被路由器解释成一个路由操做指南。咱们能够把一个RouterLink绑定到该数组,或者把它做为参数传给Router.navigate方法。

路由组件

一个带有RouterOutlet的Angular组件,它根据路由器的导航来显示相应的视图。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

范例应用

实例源码在router项目中,在此处记录知识点

RouterOutlet指令,是一个来自路由库的组件,路由器会在<router-outlet>标签中显示视图。

一个模板只能一个未命名的<router-outlet>,可是路由器能够支持多个命名的出口outlet。

重定向路由须要一个pathMatch属性,告诉路由器如何用URL去匹配路由的路径,路由器只在完整的URL等于空时才选择,所以须要把pathMatch设置为full

使用区域特性配置

能够在子目录中配置NgModel和路由,这样能够用于将模块区分开。注意:NgModel配置和路由配置,必定要在一块儿

//heroes-routing.module.ts

import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';

const heroesRoutes: Routes = [
  { path: 'heroes',  component: HeroListComponent },
  { path: 'hero/:id', component: HeroDetailComponent }
];

@NgModule({
  imports: [
    RouterModule.forChild(heroesRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class HeroRoutingModule { }

此处必定要exports导出RouterModule,否则路由访问不到

特性模块中注册服务提供商,须要使用forChild,只有在根目录时须要使用forRoot。

还须要在区域配置中,把路由配置暴露出来。

//heroes.module.ts

import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';

import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';

import { HeroService } from './hero.service';

import { HeroRoutingModule } from './heroes-routing.module';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    HeroRoutingModule
  ],
  declarations: [
    HeroListComponent,
    HeroDetailComponent
  ],
  providers: [ HeroService ]
})
export class HeroesModule {}

最后还须要把区域特性,配置到全局特性中,才能完成总体配置

import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';

import { AppComponent }     from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HeroesModule }     from './heroes/heroes.module';

import { CrisisListComponent }   from './crisis-list.component';
import { PageNotFoundComponent } from './not-found.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HeroesModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    CrisisListComponent,
    PageNotFoundComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

导入模块的顺序很重要 

注意,AppRoutingModule是最后一个,位于区域特性HeroesModule以后。

路由配置很重要,路由器会接受第一个匹配上导航所要求的路径的那个路由。

当全部路由都在同一个AppRoutingModule时,要把默认路由和通配符路由放在最后,这样才会匹配到具体的路由。

当路由不是单一文件,每一个模块都会根据导入的顺序把本身的路由配置追加进去。

若是先列出AppRoutingModule,那么通配符路由就会被注册在Hero路由前面,会拦截每个请求。

Activated Route

Actvated Router能够从路由器的URL中解析出路由参数

首先须要导入Router、ActivatedRoute、Params类

import { Router, ActivatedRoute, ParamMap } from '@angular/router';

导入switchMap操做符,将会处理路由参数的可观察对象(Observable)

import 'rxjs/add/operator/switchMap';

一般会直接写构造函数,让Angular把组件所需的服务注入进来 

//hero-detail.component.ts
constructor(
private route: ActivatedRoute, private router: Router, private service: HeroService ) {}

而后在ngOnInit方法中,用ActivatedRoute服务来接收路由的参数,从参数中取得该英雄的id,并显示此英雄

hero$: Observable<Hero>;

  ngOnInit() {
    this.hero$ = this.route.paramMap
      .switchMap((params: ParamMap) =>
        this.service.getHero(params.get('id')));
  } 

因为参数是做为Observable提供的,因此咱们得用switchMap操做符来根据名字取得id参数。

switchMap操做符也会取消之前未完成的在途请求,若是使用心得id再次导航,那么会抛弃老的请求。

这个可观察对象的Subscription将会由AsyncPipe处理,而且组件的hero属性将会设置为刚刚接收到的这个英雄。

ParamMap参数API

提供了一些方法来处理对路由参数(paramMap)和查询参数(queryParamMap)中的访问。

Member 成员 Description 描述
has(name)

若是参数名位于参数列表中,就返回 true 。

get(name)

若是这个map中有参数名对应的参数值(字符串),就返回它,不然返回null。若是参数值其实是一个数组,就返回它的第一个元素。

getAll(name)

若是这个map中有参数名对应的值,就返回一个字符串数组,不然返回空数组。当一个参数名可能对应多个值的时候,请使用getAll

keys

返回这个map中的全部参数名组成的字符串数组。

 

 

 

 

 

 

 

 

参数的可观察对象(Observable)与组件复用

上面订阅了路由参数的Observable对象,这种写法暗示着路由参数在该组件的生存期内可能会变化。

默认状况下,若是没有访问过其余组件就导航到同一个组件实例,路由器倾向于复用组件实例。若是复用参数可能变化

假设父组件的导航栏有前进、后退按钮,用来轮流显示英雄列表中英雄的详情。每次点击都会强制导航到前一个或后一个id的HeroDetailComponent组件。

不但愿路由器仅仅从DOM中移除当前的实例,而且用下一个id从新建立它,那可能致使页面抖动,更好的方式是复用同一个组件实例,并更新这些参数。

可是ngOnInit对每一个实例只调用一次,须要一种方式来检测在同一个实例中路由参数何时发生变化,而params属性这个Observable处理了这种状况。

当组件中订阅一个可观察对象时,一般老是要在组件销毁时取消这个订阅。

可是也有少数状况不须要取消订阅,ActivateRoute中的各类可观察对象就是属于这种状况。

ActivateRoute及其可观察对象都是由Router自己负责管理的。Router会在不在须要时销毁这个路由组件,而注入的ActivateRoute也随之销毁。

Snapchat(快照):当不须要Observable时的替代品

若是肯定某个组件的实例,永远不会被复用,就能够使用快照来简化这段代码。

route.snapchat提供了路由参数的初始值,能够经过它来直接访问参数,而不用订阅或者添加Observable的操做符

ngOnInit() {
  let id = this.route.snapshot.paramMap.get('id');

  this.hero$ = this.service.getHero(id);
}

用这种技巧,只获得这些参数的初始值,若是有可能连续屡次导航到此组件,那么就该用params可观察对象的方式。

咱们这里使用params可观察对象策略,以防万一。

navigate

使用router的navigate,能够导航到对应的路由中,例如咱们作一个后退按钮

gotoHeroes() {
  this.router.navigate(['/heroes']);
}

可选对象

传送一个包含可选id参数的对象,在对象中定义一个没用的额外参数(foo)

gotoHeroes(hero: Hero) {
  let heroId = hero ? hero.id : null;
  // Pass along the hero id if available
  // so that the HeroList component can select that hero.
  // Include a junk 'foo' property for fun.
  this.router.navigate(['/heroes', { id: heroId, foo: 'foo' }]);
}

此时浏览器地址栏的地址为:localhost:3000/heroes;id=15;foo=foo 

id的值出如今URL中,但不在URL的路径部分,可选的路由参数没用使用?和&分隔,是用;分隔的。

ActivatedRoute服务中的路由参数

export class HeroListComponent implements OnInit {
  heroes$: Observable<Hero[]>;

  private selectedId: number;

  constructor(
    private service: HeroService,
    private route: ActivatedRoute
  ) {}

  ngOnInit() {
    this.heroes$ = this.route.paramMap
      .switchMap((params: ParamMap) => {
        // (+) before `params.get()` turns the string into a number
        this.selectedId = +params.get('id');
        return this.service.getHeroes();
      });
  }
}

ActivatedRoute.paramMap属性是一个路由参数的可观察对象,当用户导航到这个组件时,会发出一个新的值其中包含id

在ngOnInit中,订阅了这些值,设置到selectedId上,并获取英雄数据。

用CSS类绑定更新模板,绑定到isSelected方法上,若是该方法返回true,此绑定就会添加CSS类selected,不然就移除它

template: `
  <h2>HEROES</h2>
  <ul class="items">
    <li *ngFor="let hero of heroes$ | async"
      [class.selected]="hero.id === selectedId">
      <a [routerLink]="['/hero', hero.id]">
        <span class="badge">{{ hero.id }}</span>{{ hero.name }}
      </a>
    </li>
  </ul>

  <button routerLink="/sidekicks">Go to sidekicks</button>
`

 子路由

Angular应用推荐的模式为:每一个特性放在本身的目录中

1.每一个特性都有本身的Angular特性模块。

2.每一个特性区都有本身的根组件

3.每一个特性区的根组件中都有本身的路由出口及其子路由

4.特性区的路由不多(或彻底不)与其余特性区的路由交叉

若是有更多的特性区,组件树是这样的:

特性区也能够设置本身的特区壳和特区路由。

特区Component

//crisis-center.component.ts

import { Component } from '@angular/core';

@Component({
  template:  `
    <h2>CRISIS CENTER</h2>
    <router-outlet></router-outlet>
  `
})
export class CrisisCenterComponent { }

子路由配置

//crisis-center-routing.module.ts

import {NgModule} from '@angular/core';
import {Routes, RouterModule} from '@angular/router';

import {CrisisCenterComponent} from './crisis-center.component';
import {CrisisListComponent} from './crisis-list.component';
import {CrisisDetailComponent} from './crisis-detail.component';
import {CrisisCenterHomeComponent} from './crisis-center-home.component';

const crisisCenterRoutes: Routes = [
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent
          }, {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(crisisCenterRoutes)
  ],
  exports: [
    RouterModule
  ]
})

export class CristsCenterRoutingModule {
}

父路由crisis-center有一个children属性,包含一个List的路由。List路由还有一个children数组带两个路由。

路由器会把路由对应的组件放在CrisisCenter中的RouterOutlet中,而不是AppCommponent壳中。

//crisis-list.component

import {Component} from '@angular/core';

@Component({
  template: `
    <h3>CrisisList</h3>
    <router-outlet></router-outlet>
  `
})

export class CrisisListComponent {
}

List包含RouterOutlet,用来显示CenterHome和Detail。

Detail是List的子路由,路由器默认会复用组件,当选择另外一个时会被复用。

在顶级,以/开头的路径指向的老是应用的根,但这里是子路由。是在父路由路径的基础上作出的扩展。

在路由树中每深一步,就会在路由的路径上添加一个斜线"/",除非路径是空的。

若是把该逻辑应用到导航中,父路径就是/cris-center

相对路径

能够把输出路径改成相对于当前URL的路径,这样就能够把连接从依赖中解放出来。

当修改该特性区的父路由时,该特性区内部的导航仍然无缺如初。

在连接参数数组中,路由器支持目录式语法来指导咱们如何查询路由名。

./ 或 无前导线 形式是相对于当前级别的

../ 会回到当前路由路径的上一级

this.router.navigate(['../']);

用Router.navigate方法导航到相对路径时,必须提供当前的ActivatedRoute,让路由器知道咱们如今的位置。

在连接参数数组中,添加一个带有relativeTo的属性对象,并设置为当前的ActivatedRoute。

这样路由器就会基于当前激活路由的位置来计算目标URL 。(当调用路由器的navigateByUrl时,老是要指定完整的绝对路径。)

this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route });

命名路由 

模板能够有多个命名路由,每一个命名出口都有本身的一组带组件的路由。多重出口能够在同一时间根据不一样的路由来显示不一样内容。

例如,在app.component,添加一个命名路由

<router-outlet></router-outlet>
<router-outlet name="popup"></router-outlet>

第二路由很像主路由,配置方式也同样,只有一些关键点不一样

1.他们彼此不依赖,与其余路由组合使用

2.显示在命名出口中

在路由配置时,就能够直接定义命名路由出口

{
  path: 'compose',
  component: ComposeMessageComponent,
  outlet: 'popup'
},

只配置路由还不行,routerLink也要直接配置

<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>

连接参数数组包含一个outlets属性,这个对象对应出口名。

清除第二路由

路由之间互不影响,若是想在主路由中清楚第二路由,能够写成以下形式

closePopup() {
  // Providing a `null` value to the named outlet
  // clears the contents of the named outlet
  this.router.navigate([{ outlets: { popup: null }}]);
}

路由守卫 

任何用户都能在任什么时候候导航到任何地方,这样是不对的

1.该用户可能无权导航到目标组件

2.可能用户得先登陆(认证)

3.在显示目标组件前,咱们可能得先获取某些数据

4.在离开组件前,可能要先保存修改

5.可能须要询问用户,是否放弃本次修改

能够往配置中添加守卫来处理这些场景。

守卫返回一个值,以控制路由器的行为

1.若是它返回true,导航过程会继续

2.若是它返回false,导航过程会终止,用户会留在原地

守卫还能够控制路由器导航到别的地方

路由守卫能够返回一个Observable<boolean>或Promise<boolean>,而且路由器会等待这个可观察对象被解析为true或false

路由支持多个保护接口

用CanActivate来处理导航到某个路由的状况

用CanActivateChild来处理导航到某个子路由的状况

用CanDeactivate来处理从当前路由离开的状况

用Resolve在路由激活以前获取路由数据

用CanLoad来处理异步导航到某特性模块的状况

在分层路由的每一个层级上,均可以设置多个守卫,路由器会先按照从最深到子路由由下往上检查的顺序来检查CanDeactivate和CanActivateChild守卫

而后会按照从上到下的顺序检查CanActivate守卫,若是特性模块是异步加载的,加载以前还会检查CanLoad守卫。

若是任何一个守卫返回false,其它还没有完成的守卫会被取消,这样整个导航就取消了。

CanActivate要求认证

应用程序一般会根据访问者来决定是否授予某个特性区的访问权。能够只对已认证的用户或具备特定角色的用户授予访问权。

还能够阻止或限制用户访问权,直到用户帐户激活为止。

import {Component} from '@angular/core';

@Component({
  template: `
    <h3>ADMIN</h3>
    <nav>
      <a routerLink="./" routerLinkActive="active"
         [routerLinkActiveOptions]="{ exact: true }">Dashboard</a>
      <a routerLink="./crises" routerLinkActive="active">Manage Crises</a>
      <a routerLink="./heroes" routerLinkActive="active">Manage Heroes</a>
    </nav>
    <router-outlet></router-outlet>
  `,
  styles: [
      `
      nav a.active {
        color: #039be5;
      }
    `
  ]
})
export class AdminComponent {
}

routerLink为./的路由属于空路由,会匹配到管理特性区的任何路由,可是咱们想只有Dashboard时,才激活routerLinkActive。

此时就须要添加绑定[routerLinkActiveOptions]="{ exact: true }",这样只有导航到/admin时,才会激活。

无组件路由

const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    children: [
      {
        path: '',
        children: [
          {path: 'crises', component: ManageCrisesComponent},
          {path: 'heroes', component: ManageHeroesComponent},
          {path: '', component: AdminDashboardComponent}
        ]
      }
    ]
  }
];

咱们的目标是对admin路径下的类进行路由分组,并不须要另外一个仅用来分组路由的组件

一个无组件路由就能让咱们轻松地守卫子路由。

CanActivte:要求认证

咱们写一个CanActivte守卫,将正在尝试访问管理组件的匿名用户重定向登陆页。

这是一种通用性的守护目标,咱们在应用的根目录下建立一个auth-guard.ts文件。

第一个版本让守卫先写一个日志

//auth-guard.service.ts
import { Injectable }     from '@angular/core';
import { CanActivate }    from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate() {
    console.log('AuthGuard#canActivate called');
    return true;
  }
}

接下来,咱们把守卫注册到路由里面,让该路由相关的请求,都通过守卫 

import { AuthGuard }                from '../auth-guard.service';

const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ],
      }
    ]
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(adminRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AdminRoutingModule {}

这个时候无组件路由就派上用场了,能够给一组路由设置一个守卫便可。

而后建立认证程序,来模拟用户登陆相关操做。

//auth.service.ts
import { Injectable } from '@angular/core';

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/delay';

@Injectable()
export class AuthService {
  isLoggedIn = false;

  // store the URL so we can redirect after logging in
  redirectUrl: string;

  login(): Observable<boolean> {
    return Observable.of(true).delay(1000).do(val => this.isLoggedIn = true);
  }

  logout(): void {
    this.isLoggedIn = false;
  }
}

isLoggedIn用来标识用户是否已经登陆过, login方法会仿真发起一个http请求,返回一个可观察对象(observable)

而后咱们修改路由守卫来让他调用auth.service

import {Injectable} from '@angular/core';
import {CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router';

import {AuthService} from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {
  }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    const url: string = state.url;
    return this.checkLogin(url);
  }

  checkLogin(url: string): boolean {
    if (this.authService.isLoggedIn) {
      return true;
    }
    this.authService.redirectUrl = url;
    this.router.navigate(['/login']);
    return false;
  }
}

把AuthService和Router服务注入到构造函数中, 能够往路由守卫中注入有用的服务。

ActivatedRouteSnashot包含了即将被激活的路由,RouterStateSnapshot包含了应用即将到达的状态,要经过咱们的守卫进行检查。

若是用户没有登陆,用RouterStateSnapshot.url保存来自用户的URL并让路由器导航到登陆页。

间接致使了路由器自动停止了此次导航,checkLogin返回false并非必须的,可是可让代码结构更清楚。

CanActivateChild:保护子路由

CanActivateChild和CanActive守卫很像,区别在于,CanActivatedChild会在任何子路由被激活以前运行。

咱们要保护管理特性模块,防止他被非受权访问,还要保护这个特性模块内部的那些子路由。

扩展AuthGuard,以便在admin路由之间导航时提供保护。打开auth-gurad.service并从路由库导入CanActivateChild接口

import {Injectable} from '@angular/core';
import {CanActivate, CanActivateChild, Router, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router';

import {AuthService} from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router: Router) {
  }

  canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    return this.canActivate(childRoute, state);
  }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    const url: string = state.url;
    return this.checkLogin(url);
  }

  checkLogin(url: string): boolean {
    if (this.authService.isLoggedIn) {
      return true;
    }
    this.authService.redirectUrl = url;
    this.router.navigate(['/login']);
    return false;
  }
}

一样把这个AuthGuard添加到无组件路由中,同时保护它的子路由 

const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        canActivateChild: [AuthGuard],
        children: [
          {path: 'crises', component: ManageCrisesComponent},
          {path: 'heroes', component: ManageHeroesComponent},
          {path: '', component: AdminDashboardComponent}
        ]
      }
    ]
  }
];

CanDeactivate:处理未保存的更改

把用户的改动积累起来,这些改动保存成一种特定状态,直到用户把这些改动做为一组进行确认或撤销全部改动。

当用户要导航到外侧,应该暂停并让用户决定该怎么作,若是选择取消就留下来,不然进行保存。

咱们建立一个Guard,检查这个组件中canDeactivate函数的工做现场。并不须要知道组件确认退出的激活状态详情,让咱们的守卫能够复用。

import { Injectable }    from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { Observable }    from 'rxjs/Observable';

export interface CanComponentDeactivate {
 canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}

@Injectable()
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(component: CanComponentDeactivate) {
    return component.canDeactivate ? component.canDeactivate() : true;
  }
}

能够为Detail组件建立一个特定的CanDeactivate守卫,须要访问外部信息时,canDeactivate()方法提供了组件、ActivatedRoute和RouterStateSnapshot的当前实例。

若是想要为这个组件使用该守卫,而且须要使用该组件属性、或者须要路由器确认是否容许导航出去时,这个守卫就很重要。

import {Observable} from 'rxjs/observable';
import {Injectable} from '@angular/core';
import {CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router';

import {CrisisDetailComponent} from './crisis-center/crisis-detail.component';

@Injectable()
export class CanDeactivateGuard implements CanDeactivate<CrisisDetailComponent> {
  canDeactivate(component: CrisisDetailComponent, route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
    // Get the Crisis Center ID
    console.log(route.paramMap.get('id'));

    // Get the current URL
    console.log(state.url);

    // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged
    if (!component.crisis || component.crisis.name === component.editName) {
      return true;
    }
    // Otherwise ask the user with the dialog service and return its
    // observable which resolves to true or false when the user decides
    return component.dialogService.confirm('Discard changes?');
  }
}

注意,canDeactivate方法能够同步返回,若是没有未定的修改,就当即返回true,也能够返回一个Promise或Observable。

路由器将等待它们被解析为真值(继续导航)或假值(留下)

如今往路由器中须要此守卫的地方,添加canDeactivate数组添加一个Guard守卫

const crisisCenterRoutes: Routes = [
  {
    path: '',
    redirectTo: '/crisis-center',
    pathMatch: 'full'
  },
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent,
            canDeactivate: [CanDeactivateGuard]
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

还须要把守卫添加到依赖注入里面

@NgModule({
  imports: [
    RouterModule.forRoot(
      appRoutes,
      { enableTracing: true } // <-- debugging purposes only
    )
  ],
  exports: [
    RouterModule
  ],
  providers: [
    CanDeactivateGuard
  ]
})

Resolve:预先获取组件数据 

https://angular.cn/guide/router#resolve-pre-fetching-component-data

异步路由

使用异步路由到应用程序中,并得到在请求时才惰性加载特性模块的能力。这样给咱们带来如下好处:

1.能够只在用户请求时才加载某些特性区

2.对于那些只访问应用程序某些区域的用户,这样能加快加载速度

3.能够持续扩充惰性加载特性区的功能,而不用增长初始加载的包体积

有些模块(AppModule)必须在启动时加载,但其余的均可以而且应该惰性加载,好比AdminModule就只有少数的人使用。

惰性加载路由配置

首先须要把特性模块的默认路由改成空。Router支持空路径路由,来进行分组路由,而且不用添加额外的路径片断。

//admin-routing.module

import {NgModule} from '@angular/core';
import {Routes, RouterModule} from '@angular/router';

import {AdminComponent} from './admin.component';
import {ManageCrisesComponent} from './manage-crises.component';
import {ManageHeroesComponent} from './manage-heroes.component';
import {AdminDashboardComponent} from './admin-dashboard.component';
import {AuthGuard} from '../auth-guard.service';

const adminRoutes: Routes = [
  {
    path: '',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        canActivateChild: [AuthGuard],
        children: [
          {path: 'crises', component: ManageCrisesComponent},
          {path: 'heroes', component: ManageHeroesComponent},
          {path: '', component: AdminDashboardComponent}
        ]
      }
    ]
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(adminRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AdminRoutingModule {
}

而后须要在主路由上,把一个新的admin路由,添加到appRouter数组中。在主路由上,控制路径选项。记得在空路径前添加 

给他一个loadChildren属性(注意不是children属性),把它设置为AdminModule的地址。

该地址是AdminModule的文件路径(相对于app目录)加上一个#号分隔符,再加上导出模块的类名AdminModule

//app-routing.module

import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';

import {NotFoundComponent} from './not-found.component';

const appRoutes: Routes = [
  {
    path: 'admin',
    loadChildren: 'app/admin/admin.module#AdminModule',
  }, {
    path: '',
    redirectTo: '/demo',
    pathMatch: 'full'
  }, {
    path: '**',
    component: NotFoundComponent
  }
];

@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes, {enableTracing: true})
  ],
  exports: [RouterModule]
})

export class AppRoutingModule {
}

当路由器导航到这个路由时,会用loadChildren字符串来动态加载AdminModule,而后把AdminModule添加到当前的路由配置中。

惰性加载和从新配置工做只会发生一次,也就是该路由首次被请求时。在后续的请求中,都是当即可用的。

最后一步,根模块AppModule既不能加载也不能引用AdminModule及其文件

在app.module中,从顶部移出AdminModule,并从imports数组中删除。

CanLoad守卫:保护对特性模块的未受权加载

咱们已经使用CanActivate保护AdminModule了,它会阻止未受权用户访问管理特性区。

若是用户未登陆,就会跳转到登陆页。可是路由依然会加载AdminModule,即便用户没法访问。

理想状况下,应该是用户已登陆的状况下,咱们才加载AdminModule。

添加一个CanLoad守卫,只在用户已登陆而且尝试访问管理特性区时,才加载AdminModule一次。

如今AuthGuard的checkLogin方法中已经有了支持CanLoad守卫的基础逻辑。

打开auth-guard.service从@angular/router中导入CanLoad接口,把它添加到AuthGuard类的接口列表中

canLoad(route: Route): Observable<boolean> | Promise<boolean> | boolean {
    let url = `/${route.path}`;
    return this.checkLogin(url);
  }

路由器会把canLoad方法的route参数设置为准备访问的目标URL,用户已登陆,checkLogin()方法就会重定向URL。

而后须要导入AppRoutingModule中,添加到admin路由的canLoad数组中

{
    path: 'admin',
    loadChildren: 'app/admin/admin.module#AdminModule',
    canLoad: [AuthGuard]
  },

预加载:特性区的后台加载 

AppModule在应用启动时就被加载了,是当即加载。而AdminModule只有当用户点击某个连接时才会加载,是惰性加载。

预加载是介于二者之间的一种方式,咱们应该对AppModule和首先加载的模块加速。而后开始后台加载。

这就是预加载

预加载的工做原理

每次成功导航后,路由器会在本身的配置中查找还没有加载而且能够预加载的模块,是否加载以及要加载的模块,取决于预加载策略。

Router内置了两种加载策略:

1.彻底不预加载,这是默认值,惰性加载的特性区仍然会按需加载

2.预加载全部惰性加载的特性区

默认状况下,路由器或彻底不预加载或者预加载每一个惰性加载模块,路由器还支持自定义预加载策略,以便彻底控制。

预加载全部惰性加载特性区 

从Angular路由中导入PreloadAllModules

app-routing.module中RouterModule.forRoot方法的第二个参数接受一个附加配置选项对象。preloadingStrategy就是其中之一

把PreloadAllModules添加到forRoot调用中。 

@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes, {enableTracing: true, preloadingStrategy: PreloadAllModules})
  ],
  exports: [RouterModule]
})

这会让Router预加载器当即加载全部惰性加载路由(带loadChildren属性的路由) 

CanLoad会阻塞预加载

PreloadAllModule策略不会被加载,由于他被CanLoad守卫所保护的特性区。CanLoad守卫优先级高于预加载策略

自定义预加载策略

大多数场景下,预加载每一个惰性模块就很好了。咱们能够只选择对某些特性模块进行加载。

添加项目selective-preloading-strategy

import 'rxjs/add/observable/of';
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class SelectivePreloadingStrategy implements PreloadingStrategy {
  preloadedModules: string[] = [];

  preload(route: Route, load: () => Observable<any>): Observable<any> {
    if (route.data && route.data['preload']) {
      // add the route path to the preloaded module array
      this.preloadedModules.push(route.path);

      // log the route path to the console
      console.log('Preloaded: ' + route.path);

      return load();
    } else {
      return Observable.of(null);
    }
  }
}

SelectivePreloadingStrategy实现了PreloaddingStrategy,只有一个preload方法。

路由器会用两个参数调用preload方法

1.要加载的路由

2.一个加载器函数,能异步加载带路由的模块

preload的实现必须返回一个Observable,若是该路由应该预加载,就会返回调用加载器函数所返回的Observable。

若是该路由不该该预加载,就返回一个null值的Observable对象。

preload方法只有在路由的data.preload标识为真时才会加载该路由。

import { Component, OnInit }    from '@angular/core';
import { ActivatedRoute }       from '@angular/router';
import { Observable }           from 'rxjs/Observable';

import { SelectivePreloadingStrategy } from '../selective-preloading-strategy';

import 'rxjs/add/operator/map';

@Component({
  template:  `
    <p>Dashboard</p>

    <p>Session ID: {{ sessionId | async }}</p>
    <a id="anchor"></a>
    <p>Token: {{ token | async }}</p>

    Preloaded Modules
    <ul>
      <li *ngFor="let module of modules">{{ module }}</li>
    </ul>
  `
})
export class AdminDashboardComponent implements OnInit {
  sessionId: Observable<string>;
  token: Observable<string>;
  modules: string[];

  constructor(
    private route: ActivatedRoute,
    private preloadStrategy: SelectivePreloadingStrategy
  ) {
    this.modules = preloadStrategy.preloadedModules;
  }

  ngOnInit() {
    // Capture the session ID if available
    this.sessionId = this.route
      .queryParamMap
      .map(params => params.get('session_id') || 'None');

    // Capture the fragment if available
    this.token = this.route
      .fragment
      .map(fragment => fragment || 'None');
  }
}

审查路由配置 

经过注入他(Router)并检查它的config属性,能够随时审查路由器的当前配置。

把AppModule修改为这样,就会在控制台窗口中查看最终的路由配置。

import { Router } from '@angular/router';

export class AppModule {
  // Diagnostic only: inspect router configuration
  constructor(router: Router) {
    console.log('Routes: ', JSON.stringify(router.config, undefined, 2));
  }
}
相关文章
相关标签/搜索