这里写目录标题

  • 第 1 章 Angular 初识
    • 1.1 Angular 简介
      • 1.1.1 AngularJS 和 Angular2.x、Angular4.x、Angular5.x的区别?
      • 1.1.2 为什么没有 Angular3.x?
    • 1.2 环境搭建
      • 1.2.1 Typescript 和 npm
      • 1.2.2 Angular CLI
        • 1.2.2.1 全局安装 CLI
        • 1.2.2.2 创建新项目
        • 1.2.2.3 启动开发服务器
    • 1.3 编辑第一个 Angular 组件
    • 1.4 项目结构
  • 第 2 章 架构概览
    • 2.1 模块
    • 2.2 组件
    • 2.3 模板
    • 2.4 元数据
    • 2.5 数据绑定
    • 2.6 指令
      • 2.6.1 结构型指令
      • 2.6.2 属性型指令
    • 2.7 服务
    • 2.8 依赖注入
  • 第 3 章 内置指令
    • 3.1 创建第一个自己的组件
      • 3.1.1 定义组件
      • 3.1.2 声明组件
      • 3.1.3 使用组件
      • 3.1.4 使用 CLI 创建组件
    • 3.2 ngIf
    • 3.3 ngFor
    • 3.4 NgSwitch
    • 3.5 NgStyle
      • 3.5.1 [style.style-property]
      • 3.5.2 带单位
      • 3.5.3 ngStyle
    • 3.6 NgClass
      • 3.6.1 class 属性绑定
      • 3.6.2 class 和[class]
      • 3.6.3 [class.class-name]
      • 3.6.4 ngClass
    • 3.7 ngNonBindable
    • 3.8 自定义指令
      • 3.8.1 编写指令代码
      • 3.8.2 添加到根模块
      • 3.8.3 通过事件触发
  • 第 4 章 数据绑定
    • 4.1 属性绑定
    • 4.2 事件绑定
      • 4.2.1 $event
      • 4.2.2 自定义事件
        • 4.2.2.1 创建新组件
        • 4.2.2.2 父组件引用子组件
        • 4.2.2.3 子组件添加到根模块
    • 4.3 双向绑定
      • 4.3.1 [(x)] 语法
      • 4.3.2 NgModel
    • 4.4 @Input 和@Output 别名
      • 4.4.1 @Output
      • 4.4.2 @Input
    • 4.5 模板引用变量 ( #var )
    • 4.6 安全导航符?.
  • 第 5 章 TypeScript
    • 5.1 Angular 是用 TypeScript 构建的
    • 5.2 TypeScript 提供了哪些特性
    • 5.3 类型
    • 5.4 内置类型
      • 5.4.1 字符串
      • 5.4.2 数字
      • 5.4.3 布尔类型
      • 5.4.4 数组
      • 5.4.5 枚举
      • 5.4.6 任意类型
      • 5.4.7 “ 无” 类型
  • 第 6 章 表单
    • 6.1 表单—— 既重要,又复杂
    • 6.2 获取用户输入
      • 6.2.1 使用$event
      • 6.2.2 量 使用模板变量 #var
      • 6.2.3 事件修饰符
      • 6.2.4 使用 NgModel 双向绑定
      • 6.2.5 过 通过 ngModel 跟踪修改状态与有效性验证
    • 6.3 模板驱动表单校验
      • 6.3.1 模板变量
      • 6.3.2 表单提交
    • 6.4 响应式表单的校验
      • 6.4.1 验证器函数
      • 6.4.2 内置验证器
    • 6.5 自定义校验
      • 6.5.1 定义
      • 6.5.2 添加到响应式表单
      • 6.5.3 添加到模板驱动表单
        • 6.5.3.1 新建自定义校验器:forbidden-name.directive.ts
        • 6.5.3.2 添加到根模块
        • 6.5.3.3 模板引用
    • 6.6 响应式表单
      • 6.6.1 响应式表单
      • 6.6.2 模板驱动表单
      • 6.6.3 步 异步 vs. 同步
      • 6.6.4 基础的表单类
    • 6.7 FormBuilder 简介
      • 6.7.1 使用 FormBuilder 创建 FormGroup
      • 6.7.2 模板引用
      • 6.7.2 多级 FromGroup
      • 6.7.3 模板引用
      • 6.7.4 查看 FormControl 的属性
    • 6.8 重置表单
    • 6.9 数据模型与表单模型
      • 6.9.1 定义数据模型
      • 6.9.2 setValue
      • 6.9.3 patchValue
  • 第 7 章 组件
    • 7.1 组件交互
      • 7.1.1 通过输入型绑定把数据从父组件传到子组件。
      • 7.1.2 子组件接收
      • 7.1.3 过 通过 setter 截听输入属性值的变化
      • 7.1.4 父组件监听子组件的事件
      • 7.1.5 父子组件通过本地变量互动
      • 7.1.6 父组件调用@ViewChild()
    • 7.2 组件样式
      • 7.2.1 使用组件样式
      • 7.2.2 styles 属性
      • 7.2.3 styleUrls 属性
      • 7.2.5 使用 link 标签
      • 7.2.6 @import 语法
      • 7.2.7 控制视图的封装模式
    • 7.3 特殊选择器
      • 7.3.1 :host 选择器
      • 7.3.2 :host-context 选择器
      • 7.3.3 弃 已废弃 /deep/ 、>>> 和::ng-deep
    • 7.4 组件生命周期
      • 7.4.1 生命周期的顺序
      • 7.4.2 OnInit 和 和 OnDestroy
        • 7.4.2.1 OnInit() 钩子
        • 7.4.2.2 OnDestroy() 钩子
        • 7.4.2.3 OnChanges() 钩子
        • 7.4.2.4 DoCheck() 钩子
        • 7.4.2.5 AfterView 钩子
        • 7.4.2.6 AfterContent 钩子
    • 7.5 动态组件
      • 7.5.1 指令
      • 7.5.2 加载组件
      • 7.5.3 解析组件
  • 第8章 管道
    • 8.1 使用管道
    • 8.2 管道参数化
    • 8.3 链式管道
    • 8.4 自定义管道
    • 8.5 纯(pure) 管道与非纯(impure) 管道
      • 8.5.1 纯管道
      • 8.5.2 非纯管道
  • 第 9 章 依赖注入
    • 9.1 创建服务
      • 9.1.1 编写服务
      • 9.1.3 注册服务
        • 9.1.3.1 全局注册:
        • 9.1.3.2 局部注册:
    • 9.2 带依赖类服务
    • 9.3 别类名提供商
      • 9.3.1 新服务提供商
      • 9.3.2 替换旧服务
      • 9.3.3 类名冲突
    • 9.4 值提供商
    • 9.5 工厂提供商
      • 9.5.1 改写服务
      • 9.5.2 创建提供商
      • 9.5.3 修改 user-list.component.ts
    • 9.6 可选依赖
      • 9.6.1 引入
      • 9.6.2 构造注入
      • 9.6.3 使用
    • 9.7 多级依赖注入器
      • 9.7.1 注入器树
      • 9.7.2 注入器冒泡
      • 9.7.3 在不同层级再次提供同一个服务
      • 9.7.4 组件注入器
  • 第 10 章 HttpClient 库
    • 10.1 安装 http 模块
    • 10.2 请求 JSON 数据
      • 10.2.1 安装 express 框架
      • 10.2.2 后台服务
    • 10.3 读取完整响应体
    • 10.4 错误处理
    • 10.5 获取错误详情
    • 10.6 .retry() 操作符
    • 10.7 非 请求非 JSON 数据
    • 10.8 把数据发送到服务器
      • 10.8.1 GET 传参
  • 第 11 章 路由和导航
    • 11.1 搭建路由
      • 11.1.1 添加\
      • 11.1.2 导入路由库
      • 11.1.3 配置路由
    • 11.2 通配符路由
      • 11.2.1 定义组件 not-found.component.ts
      • 11.2.2 添加路由
      • 11.2.3 添加路由导航
    • 11.3 重定向路由
    • 11.4 参数路由
      • 11.4.1 定义组件
      • 11.4.2 配置路由
      • 11.4.3 配置路由链接
    • 11.5 路由嵌套
      • 11.5.1 配置子路由
      • 11.5.2 配置子路由链接
    • 11.6 路由模块
      • 11.6.1 将路由配置重构为路由模块
      • 11.6.2 导入路由模块

第 1 章 Angular 初识

1.1 Angular 简介

Angular 是一个开发平台。它能帮你更轻松的构建 Web 应用。Angular 集声明式模板、依赖注入、端到端工具和一些最佳实践于一身,为你解决开发方面的各种挑战。Angular 为开发者提升构建 Web、手机或桌面应用的能力。

1.1.1 AngularJS 和 Angular2.x、Angular4.x、Angular5.x的区别?

AngularJS 可以看成是 Angular1.x 版本,与 Angular2.x 以后的版本相比差别非常大,从 2.x 版本开始支TypeScript/JavaScript/Dart, 而不再是 JavaScript。 但是之后的版本都是向前兼容,差别不会太大。也就是说,理想情况下, 4 的程序是可以直接迁移到 5 的, 只是会收到一些API 废弃示, 到 6 中才会彻底移除。 同时, 官方会在文档中给出详细的升级指南, 帮助开发者升级。

1.1.2 为什么没有 Angular3.x?

简单点说就是因为路由模块比其他模块多发布过一次, 因此当你使用 core 模块的 2.0 时, 和它配套的 router 模块却是 3.0 的, 这容易让开发人员困惑,跳过 3, 可以让所有模块的编号重新对齐。 语义化命名版本规则(参考:http://semver.org/lang/zh-CN/)

1.2 环境搭建

1.2.1 Typescript 和 npm

Angular 本 身 就 是 Typescript 写 的 , 所 以 开 发 ng 项 目 一 般 都 采 用Typescript 语法编写,当然也可以使用 ES5 API,但是不建议。npm 是 NodeJS 自带的一个包管理工具,所以要提前安装好 NodeJS。

1.2.2 Angular CLI

好的工具能让开发更加简单快捷。Angular CLI 是一个命令行界面工具,它可以创建项目、添加文件以及执行一大堆开发任务,比如测试、打包和发布。

1.2.2.1 全局安装 CLI
npm install -g @angular/cli
1.2.2.2 创建新项目
ng new my-app
1.2.2.3 启动开发服务器

进入项目目录,安装依赖,并启动应用

cd my-app
npm install
ng serve --open

ng serve 命令会启动开发服务器,监听文件变化,并在修改这些文件时重新构建 此 应 用 。 使 用 --open ( 或 -o ) 参 数 可 以 自 动 打 开 浏 览 器 并 访 问http://localhost:4200/。

ng serve 命令会启动开发服务器,监听文件变化,并在修改这些文件时重新构建 此 应 用 。 使 用 --open ( 或 -o ) 参 数 可 以 自 动 打 开 浏 览 器 并 访 问http://localhost:4200/。

1.3 编辑第一个 Angular 组件

这个 CLI 为我们创建了第一个 Angular 组件。 它就是名叫 app-root 的根组件。 在./src/app/app.component.ts 目录下找到它。打开这个组件文件,并且把 title 属性从 Welcome to app!! 改为 WelcometoMyFirstAngularApp!!

export class AppComponent {
title = 'Welcome to My First Angular App!! ';
}

浏览器会自动刷新,而我们会看到修改之后的标题。不错,不过它还可以更好看一点。打开src/app/app.component.css 并给这个组件设置一些样式:

h1 {
color: #369;
font-family: Arial, Helvetica, sans-serif;
font-size: 250%;
}

1.4 项目结构

src 文件夹

  • app 目录存放组件以及后续新增组件,
  • app/app.module.ts 定义 AppModule,这个根模块会告诉 Angular 如何组装该应用。
  • assets 目录存放静态资源文件,比如图片
  • index.html 网站主页面
  • main.ts 应用主入口

第 2 章 架构概览

Angular 是 一 个 用 HTML 和 JavaScript 或 者 一 个 可 以 编 译 成JavaScript 的语言(例如 Dart 或者 TypeScript ),来构建客户端应用的框架。该框架包括一系列库,有些是核心库,有些是可选库。

我们是这样写 Angular 应用的:

  • 用 Angular 扩展语法编写 HTML 模板,
  • 用组件类管理这些模板,
  • 用服务添加应用逻辑, 用模块打包发布组件与服务。
  • 然后,我们通过引导根模块来启动该应用。
  • Angular 在浏览器中接管、展现应用的内容,并根据我们提供的操作指令响应用户的交互。
  • Angular 架构中核心概念有:模块、组件、模板、元数据、数据绑定、指令、服务、依赖注入。

2.1 模块

Angular 应用是模块化的,并且 Angular 有自己的模块系统,它被称为 Angular 模块或 NgModules,Angular 模块很重要。
每个 Angular 应用至少有一个模块(根模块),习惯上命名为 AppModule。
Angular 模块(无论是根模块还是特性模块)都是一个带有@NgModule 装饰器的类。
装饰器是用来修饰 JavaScript 类的函数。 Angular 有很多装饰器,它们负责把元数据附加到类上,以了解那些类的设计意图以及它们应如何工作。
下面查看一下项目的根模块:app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [ AppComponent ],
imports: [ BrowserModule ],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

NgModule 是一个装饰器函数,它接收一个用来描述模块属性的元数据对象。其
中最重要的属性是:

  1. declarations - 声明本模块中拥有的视图类。Angular 有三种视图类:
    组件、指令和管道。
  2. exports - declarations 的子集,可用于其它模块的组件模板。
  3. imports - 本模块声明的组件模板需要的类所在的其它模块。
  4. providers - 服务的创建者,并加入到全局服务列表中,可用于应用任何部分。
  5. bootstrap - 指定应用的主视图(称为根组件),它是所有其它视图的宿主。只有根模块才能设置 bootstrap 属性。

2.2 组件

组件负责控制屏幕上的一小块区域,我们称之为视图。我们在类中定义组件的应用逻辑,为视图提供支持。
查看一个最简单的组件:app.component.ts

import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'Welcome to My First Angular App!! ';
}

Component 组件中的@Component 装饰器有很多属性,这是一部分:

  • selector - 组件名称,在使用是通过引用组件。
  • templateUrl - 模板 HTML,引入外部 html 作为模板。也可以直接使用template 属性定义模板字符串。
  • styleUrls - 模板样式,引入外部 css 文件。也可以使用 style 属性来定义样式字符串

2.3 模板

通过组件的自带的模板来定义组件视图。模板以 HTML 形式存在,告诉Angular 如何渲染组件。
模板文件:app.component.html

<div style="text-align:center"><h1>Welcome to {{title}}!</h1>
</div>

通过 templateUrl: './app.component.html'引用,或者直接使用:

template:`
<div style="text-align:center"><h1>Welcome to {{title}}!</h1>
</div>
`

2.4 元数据

metadata 元 数 据 告 诉 Angular 如 何 处 理 一 个 类 , 用 装 饰 器(decorator) 来附加元数据给一个类 class, Angular 就会把它作为一个组件。

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
//组件
}

这里看到@Component 装饰器,它把紧随其后的类标记成了组件类。

2.5 数据绑定

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

数据绑定的语法有四种形式。每种形式都有一个方向 —— 绑定到DOM 、绑定自 DOM 以及双向绑定。简单理解就是:取值、单向绑定、事件绑定、双向绑定。

2.6 指令

Angular模板是动态的。当 Angular渲染它们时,它会根据指令(directive)提供的操作对 DOM 进行转换。
组件是一个带模板的指令,还有两种其它类型的指令:结构型指令和属性(attribute) 型指令。
它们往往像属性 (attribute) 一样出现在元素标签中,偶尔会以名字的形式出现,但多数时候还是作为赋值目标或绑定目标出现。

2.6.1 结构型指令

结构型指令通过在 DOM 中添加、移除和替换元素来修改布局。

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

2.6.2 属性型指令

属性型指令修改一个现有元素的外观或行为。 在模板中,它们看起来就像是标准的 HTML 属性,故名。

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

Angular 还有少量指令,它们或者修改结构布局(例如 ngSwitch), 或者修改 DOM 元素和组件的各个方面(例如 ngStyle 和 ngClass)。当然,我们也能编写自己的指令。

2.7 服务

服务是一个广义范畴,包括:值、函数,或应用所需的特性。
几乎任何东西都可以是一个服务。 典型的服务是一个类,具有专注的、明确的用途。它应该做一件特定的事情,并把它做好。
例如:

  • 日志服务
  • 数据服务
  • 消息总线
  • 税款计算器
  • 应用程序配置

服务无处不在,组件类应保持精简。

组件本身不从服务器获得数据、不进行验证输入,也不直接往控制台写日志。 它们把这些任务委托给服务。
组件的任务就是提供用户体验,仅此而已。它介于视图(由模板渲染)和应用逻辑(通常包括模型的某些概念)之间。 设计良好的组件为数据绑定提供属性和方法,把其它琐事都委托给服务。

2.8 依赖注入

“依赖注入”Dependency injection 是提供类的新实例的一种方式,还负责处理好类所需的全部依赖。大多数依赖都是服务。 Angular 使用依赖注入来提供新组件以及组件所需的服务。

constructor(private service: HeroService) { }

当 Angular 创建组件时,会首先为组件所需的服务请求一个注入器(injector)。
注入器维护了一个服务实例的容器,存放着以前创建的实例。
如果所请求的服务实例不在容器中,注入器就会创建一个服务实例,并且添加到容器中,然后把这个服务返回给 Angular。
当所有请求的服务都被解析完并返回时,Angular 会以这些服务为参数去调用组件的构造函数。 这就是依赖注入 。
我们可以在模块中或组件中注册提供商。通常会把提供商添加到根模块上,以便在任何地方都使用服务的同一个实例。

@NgModule({
providers: [HeroService]
})

或者,也可以在@Component 元数据中的 providers 属性中把它注册在组件层:

@Component({
selector: 'app-root',
providers: [ HeroService ]
})

把它注册在组件级表示该组件的每一个新实例都会有一个服务的新实例。
需要记住的关于依赖注入的要点是:

  • 依赖注入渗透在整个 Angular 框架中,被到处使用。
  • 注入器 (injector) 是本机制的核心。
    • 注入器负责维护一个容器,用于存放它创建过的服务实例。
    • 注入器能使用提供商创建一个新的服务实例。
  • 提供商是一个用于创建服务的配方。
  • 把提供商注册到注入器。

第 3 章 内置指令

3.1 创建第一个自己的组件

3.1.1 定义组件

新建 hello-world.component.ts 模块文件,模块命名一般要 component来表示是一个模块。

import { Component} from '@angular/core';
@Component({
selector: 'app-hello-world',
template: `
<h3>{{greetText}}</h3>
`
})
export class HelloWorldComponent {
greetText = 'hello world!!';
}

通过@Component 装饰器定义组件,selector 定义组件的引用名称为app-hello-world,template 定义模板的 html 片段,注意使用的是 ES6的模板字符串``,当然也可以使用普通字符串,但是不建议。{{}}双花括号进行取值,class 来声明一个组件类,greetText 是定一个的组件变量。

3.1.2 声明组件

要在 Angular 模块中使用新定义的组件,需要先声明组件。把定义好的组件import 到 app.mudule.ts 根模块中,并在@NgModule 中的 declarations中声明才能使用。

import { HelloWorldComponent } from './hello-world.component'
@NgModule({
declarations: [
AppComponent,
HelloWorldComponent
],
...
})

3.1.3 使用组件

在 app.component.html 文件中引用组件

<app-hello-world></app-hello-world>

页面中就会看到:hello world!! 现在我们已经定义好了一个属于自己的组
件了。

3.1.4 使用 CLI 创建组件

手动创建组件时,每次都需要自己添加新的组件到根模块中,比较繁琐。我们可以通过CLI命令来创建我们的Angular组件,他会自动帮我们添加到根模块中,创建一个新的 hello-everyone 模块:

ng generate component hello-everyone

同时 app.module.ts 根模块也同步更新。

3.2 ngIf

它接受一个布尔值,并据此让一整块 DOM 树出现或消失。

<p *ngIf="true">
你能看到我!- true
</p>
<p *ngIf="false">
你能看不到我!- false
</p>

ngIf 指令并不是使用 CSS 来隐藏元素的。它会把这些元素从 DOM 中物理删除。

3.3 ngFor

循环遍历一个数组或者对象。

citys = ['shanghai', 'beijing', 'hangzhnou'];//在组件中定义
<ul>
<li *ngFor="let city of citys">
{{city}}
</li>
</ul>

NgFor 的全特性应用:

//数据
fruits = [
{id:1, name:'apple'},
{id:2, name:'bananer'},
{id:3, name:'orange'}
];
<ul>
<li *ngFor="let fruit of fruits; let i=index; let odd=odd;
trackBy: trackById">
名称:{{fruit.name}} - 索引:{{i}} - 偶数行:{{odd}}
</li>
</ul>

3.4 NgSwitch

NgSwitch 指令由:NgSwitch、NgSwitchCase 和 NgSwitchDefault 组
成。

<div [ngSwitch]="myChar">
<p *ngSwitchCase="'A'">
myChar is A
</p>
<p *ngSwitchCase="'B'">
myChar is B
</p>
<p *ngSwitchDefault>
myChar is not A and B
</p>
</div>

3.5 NgStyle

样式绑定,可以设置内联样式。
方括号中的部分不是元素的属性名,而由 style 前缀,一个点 (.)和 CSS 样式的属性名组成。 形如:[style.style-property]。

3.5.1 [style.style-property]

<button [style.color]="'red'">Red</button>
<button [style.color]="false ? 'red' : 'blue'">Blue</button>

3.5.2 带单位

有些样式绑定中的样式带有单位。在这里,以根据条件用“px”、 “em” 和 “%”来设置字体大小的单位。

<button [style.font-size.px]="20">Red</button>
<button [style.font-size.em]="2">Red</button>
<button [style.font-size.%]="100">Red</button>

3.5.3 ngStyle

同时操作多个属性,可以传入一个对象

<button [ngStyle]="{'font-size':'20px'}">Red</button>

3.6 NgClass

借助 CSS 类绑定,可以从元素的 class attribute 上添加和移除 CSS 类名。
方括号中的部分不是元素的属性名,而是由 class 前缀,一个点 (.)和 CSS 类的名字组成, 其中后两部分是可选的。形如:[class.class-name]。

3.6.1 class 属性绑定

<div [class]="'container'">hello</div>

3.6.2 class 和[class]

class 和[class]共存时,如果[class]有值会完全覆盖 class。

<div class="box" [class]="'container'">hello</div>

3.6.3 [class.class-name]

最后,可以绑定到特定的类名。 当模板表达式的求值结果是真值时,Angular会添加这个类,反之则移除它

<div class="box" [class.container]="true">hello</div>
<div class="box" [class.container]="fasle">hello</div>

3.6.4 ngClass

同时操作多个 class 类,可以传入一个对象

<div [ngClass]="{box:true, container:false}">hello</div>

3.7 ngNonBindable

当我们想告诉 Angular 不要编译或者绑定页面中的某个特殊部分时, 要使用 ngNodBindable 指令。

<div ngNonBindable>
使用 {{content}} 获取值
</div>

3.8 自定义指令

属性型指令至少需要一个带有@Directive 装饰器的控制器类。该装饰器指定了一个用于标识属性的选择器。 控制器类实现了指令需要的指令行为。
创建一个简单的属性型指令 myHighlight ,当用户把鼠标悬停在一个元素上时,改变它的背景色。你可以这样用它:

<p appHighlight>heightLight</p>

3.8.1 编写指令代码

import { Directive, ElementRef } from '@angular/core';
@Directive({
selector: '[appHighlight]',
})
export class HightlightDirective {
constructor(el: ElementRef){
el.nativeElement.style.backgroundColor = 'yellow';
}
}

ElementRef 对象可以获取指定绑定的 DOM 元素,通过 nativeElement 来操作 DOM。

3.8.2 添加到根模块

import { HightlightDirective } from
'./directive/highlight.directive'
@NgModule({
declarations: [
AppComponent,
HightlightDirective
],

3.8.3 通过事件触发

当前,appHighlight 只是简单的设置元素的颜色。 这个指令应该在用户鼠标悬浮一个元素时,设置它的颜色。先把 HostListener 加进导入列表中,同时再添加 Input 装饰器。

import { Directive, ElementRef, HostListener } from
'@angular/core';
@Directive({
selector: '[appHighlight]',
})
export class HightlightDirective {
constructor(private el: ElementRef){ }
private highlight(color: string){
this.el.nativeElement.style.backgroundColor = color;
}
@HostListener('mouseenter') onmouseenter() {
this.highlight('yellow');
}
@HostListener('mouseleave') onmouseleave(){
this.highlight(null);
}
}

3.8.4 自定义颜色

现在的高亮颜色是硬编码在指令中的,这不够灵活。 我们应该让指令的使用者可以在模板中通过绑定来设置颜色。

import { Directive, ElementRef, HostListener, Input } from
'@angular/core';
@Directive({
selector: '[appHighlight]',
})
export class HightlightDirective {
@Input() appHighlight;
constructor(private el: ElementRef){ }
private highlight(color: string){
this.el.nativeElement.style.backgroundColor = color;
}
@HostListener('mouseenter') onmouseenter() {
this.highlight(this.appHighlight || 'yellow');
}
@HostListener('mouseleave') onmouseleave(){
this.highlight(null);
}
}

使用:

<p [appHighlight]="'green'">heightLight-green</p>

第 4 章 数据绑定

4.1 属性绑定

当要把视图元素的属性 (property) 设置为模板表达式时,就要写模板的属性(property) 绑定。数据流动方向是从等号右边流入左边。
最常用的属性绑定是把元素属性设置为组件属性的值。下面这个例子中,image元素的 src 属性会被绑定到组件的 imageUrl 属性上:

<img [src]="imageUrl">

另一个例子是当组件说它 isUnchanged(未改变)时禁用按钮:

<button [disabled]="isUnchanged">Cancel is disabled</button>

4.2 事件绑定

常常需要监听某些事件,如按键、鼠标移动、点击和触摸屏幕。事件绑定语法由等号左侧带圆括号的目标事件和右侧引号中的模板语句组成。 数据流动方向从等号左边到右边。下面事件绑定监听按钮的点击事件。每当点击发生时,都会调用组件的 onSave()方法。

onSave(){
console.log('save...');
}
<button (click)="onSave()">Save</button>

4.2.1 $event

当事件发生时,这个处理器会执行模板语句。 典型的模板语句通常涉及到响应事件执行动作的接收器,例如从 HTML 控件中取得值,并存入模型。
事件对象的形态取决于目标事件。如果目标事件是原生 DOM 元素事件, $event 就是 DOM 事件对象,它有像 target 和 target.value 这样的属性。

show(event){
console.log(event, event.target, event.target.value);
}
<input (input)="show($event)" >

4.2.2 自定义事件

通常,指令使用 Angular EventEmitter 来触发自定义事件。 指令创建一个 EventEmitter 实 例 , 并 且 把 它 作 为 属 性 暴 露 出 来 。 指 令 调 用EventEmitter.emit(payload)来触发事件,可以传入任何东西作为消息载荷。 父指令通过绑定到这个属性来监听事件,并通过$event 对象来访问载荷。

4.2.2.1 创建新组件
import { Component, EventEmitter, Output } from '@angular/core';
@Component({
selector: 'app-self-event',
template: `
<button (click)="save()">self-event</button>
`
})
export class SelfEventComponent {
@Output() myClick = new EventEmitter();
save(){
this.myClick.emit('self-event');
}
}

首先引入 EventEmitter 和 Output 组件,@Output 装饰器定义这是一个输出变量,在这里就是从子组件输出到父组件。

4.2.2.2 父组件引用子组件

在 html 中引用组件

<app-self-event (myClick)="onMyClick($event)"></app-self-event>

在父组件中定义事件处理

onMyClick(event){
console.log(event);
}
4.2.2.3 子组件添加到根模块

别忘了把新定义的组件添加到根模块中,否则会报错。

import { SelfEventComponent } from
'./directive/self-event.componet'
@NgModule({
declarations: [
AppComponent,
DirectiveComponent,
SelfEventComponent
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})

现在点击页面的按钮即可触发自定义事件 myClick 的执行。

4.3 双向绑定

我们经常需要显示数据属性,并在用户作出更改时更新该属性。在元素层面上,既要设置元素属性,又要监听元素事件变化。Angular 为此提供一种特殊的双向数据绑定语法:[(x)]。
结合了属性绑定的方括号[x]和事件绑定的圆括号(x)。
当一个元素拥有可以设置的属性 x 和对应的事件 xChange 时,解释[(x)]语法就容易多了。

4.3.1 [(x)] 语法

下面的 Component 符合这个模式。它有 size 属性和伴随的 sizeChange 事件:

import { Component, Input, Output, EventEmitter } from
'@angular/core';
@Component({
selector: 'app-two-way',
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 TwoWayComponent {
@Input() size: number;
@Output() sizeChange = new EventEmitter();
dec() {
this.sizeChange.emit(this.size--);
}
inc() {
this.sizeChange.emit(this.size++);
}
}

父组件引用,记得在在父组件中记得定义 fontSize 属性。

<app-two-way [(size)]="fontSizePx"></app-two-way>
<div [style.font-size.px]="fontSizePx">双向绑定</div>

这里的 size 就是双向数据绑定,可以看成是 size 和 sizeChange 自定义事件的简写。其实,双向绑定语法实际上是属性绑定和事件绑定的语法糖。
Angular 将 Component 的绑定分解成这样:

<app-two-way [size]="fontSizePx"
(sizeChange)="fontSizePx=$event"></app-two-way>

event变量包含了SizerComponent.sizeChange事件的荷载。当用户点击按钮时,Angular将event 变量包含了 SizerComponent.sizeChange 事件的荷载。 当用户点击按钮时,Angular 将event变量包含了SizerComponent.sizeChange事件的荷载。当用户点击按钮时,Angular将event 赋值给 AppComponent.fontSizePx。
显然,比起单独绑定属性和事件,双向数据绑定语法显得非常方便。

4.3.2 NgModel

使用[(ngModel)]双向绑定到表单元素。当开发数据输入表单时,我们通常都要既显示数据属性又根据用户的更改去修改那个属性。
使用 NgModel 指令进行双向数据绑定可以简化这种工作。例子如下:

<div>
<input type="text" [(ngModel)]="fontSizePx">
</div>

但此时还不生效,需要在根模块引入 FormsModule,

import { FormsModule } from '@angular/forms';
@NgModule({
...
imports: [
BrowserModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

4.4 @Input 和@Output 别名

4.4.1 @Output

给输入/输出属性起别名,有时需要让输入/输出属性的公开名字不同于内部名字。比如自定义事件外部叫myClick,内部是使用 innerClick,

格式: ==@Output(外部公开名称) 内部使用名称 = ==

@Output('myClick') innerClick = new EventEmitter();
save(){
this.innerClick.emit('self-event');
}

或者在@Component 中定义,使用
格式:outputs:[内部使用名称:外部公开名称]

@Component({
selector: 'app-self-event',
outputs:['innerClick: myClick'],
template: `
<button (click)="save()">self-event</button>
`
})

4.4.2 @Input

用法同@Output,假如外部传入 size 属性,内部使用 mySize 属性:

@Input('size') mySize: number;

或者在@Component 中定义:

inputs: ['mySize: size'],

4.5 模板引用变量 ( #var )

模板引用变量通常用来引用模板中的某个 DOM 元素,它还可以引用 Angular 组件或指令或 Web Component。使用井号 (#) 来声明引用变量。 #phone 的意思就是声明一个名叫 phone 的变量来引用元素。

<input #phone placeholder="phone number">
<button (click)="callPhone(phone.value)">Call</button>

不要在同一个模板中多次定义同一个变量名,否则它在运行期间的值是无法确定的。
我们也可以用 ref-前缀代替#。 下面的例子中就用把 phone 变量声明成了ref-phone 而不是#phone。

<input ref-phone placeholder="phone number">
<button (click)="callPhone(phone.value)">Phone</button>

4.6 安全导航符?.

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

<p> 你的名字:{{people?.name}} </p>

第 5 章 TypeScript

5.1 Angular 是用 TypeScript 构建的

Angular 是用 TypeScript 构建的,或许你会对用新语言来开发 Angular 心存疑虑, 但事实上, 在开发 Angular 应用时, 我们有充分的理由用TypeScript 代替普通的 JavaScript。

TypeScript 并不是一门全新的语言, 而是 ES6 的超集。 所有的 ES6 代码都是完全有效且可编译的 TypeScript 代码。 下图展示了它们之间的关系。

ES5 是 ECMAScript 5 的缩写, 也被称为“普通的 JavaScript”。 ES5 就是大家熟知的 JavaScript, 它能够运行在大部分浏览器上。 ES6 则是下一个版本的 JavaScript, 已经于 2015 年发布。

从 TypeScript 代码到 ES5 代码的唯一转换器是由 TypeScript 核心团队编写的。 然而, 将 ES6 代码(不是 TypeScript 代码) 转换到 ES5 代码则有两个主要的转换器: Google 开发的 Traceur 与 JavaScript 社区创建的 Babel 。 安装 TypeScript 环境:

npm install -g typescript

5.2 TypeScript 提供了哪些特性

TypeScript 相对于 ES5 有五大改善:

  • 类型
  • 注解
  • 模块导入
  • 语言工具包(比如, 解构)

5.3 类型

顾名思义, 相对于 ES6, TypeScript 最大的改善是增加了类型系统。有些人可能会觉得,缺乏类型检查正是 JavaScript 这些弱类型语言的优点。 也许你对类型检查心存疑虑, 但我仍然鼓励你试一试。 类型检查的好处有:

  1. 有助于代码的编写, 因为它可以在编译期预防 bug;
  2. 有助于代码的阅读, 因为它能清晰地表明你的意图。

另外值得一提的是, TypeScript 中的类型是可选的。 如果希望写一些快速代码或功能原型, 可以首先省略类型, 然后再随着代码日趋成熟逐渐加上类型。
TypeScript的基本类型与我们平时所写JavaScript 代码中用的隐式类型一样, 包括字符串、 数字、 布尔值等。直到 ES5, 我们都在用 var 关键字定义变量, 比如 var name;。TypeScript 的新语法是从 ES5 自然演化而来的, 仍沿用 var 来定义变量,
但现在可以同时为变量名提供可选的变量类型了:

var name: string;

在声明函数时, 也可以为函数参数和返回值指定类型:

function greetText(name: string): string {
return "Hello " + name;
}

这个例子中, 我们定义了一个名为 greetText 的新函数, 它接收一个名为name 的参数。 name: string 语法表示函数想要的 name 参数是 string 类型。 如果给该函数传一个 string 以外的参数, 代码将无法编译通过。 对我们来说, 这是好事, 否则这段代码将会引入 bug。

或许你还注意到了, greetText 函数在括号后面还有一个新语法:string {。冒号之后指定的是该函数的返回值类型, 在本例中为 string。
这很有用, 原因有二:

  • 如果不小心让函数返回了一个非 string 型的返回值, 编译器就会告诉我们这里有错误;
  • 使用该函数的开发人员也能很清晰地知道自己将会拿到什么类型的数据。

我们来看看如果写了不符合类型声明的代码会怎样

function hello(name: string): string {
return 12;
}

当尝试编译代码时, 将会得到下列错误:

$ tsc compile-error.ts
compile-error.ts(2,12): error TS2322: Type 'number' is not
assignable to type

这是怎么回事? 我们尝试返回一个 number 类型的 12, 但 hello 函数期望的返回值类型为 string(它是在参数声明的后面以): string {的形式声明的) 。要纠正它, 可以把函数的返回值类型改为 number:

function hello(name: string): number {
return 12;
}

虽然这只是一个小例子, 但足以证明类型检查能为我们节省大量调试 bug 的时间。

5.4 内置类型

5.4.1 字符串

字符串包含文本, 声明为 string 类型:

var name: string = 'Tim';

5.4.2 数字

无论整数还是浮点, 任何类型的数字都属于 number 类型。 在 TypeScript中, 所有的数字都是用浮点数表示的, 这些数字的类型就是 number:

var age: number = 36;

5.4.3 布尔类型

布尔类型(boolean) 以 true(真) 和 false(假) 为值。

var married: boolean = true;

5.4.4 数组

数组用 Array 类型表示。 然而, 因为数组是一组相同数据类型的集合,所以我们还需要为数组中的条目指定一个类型。
我们可以用 Array或者 type[]语法来为数组条目指定元素类型:

var jobs: Array<string> = ['IBM', 'Microsoft', 'Google'];
var jobs: string[] = ['Apple', 'Dell', 'HP'];

数字型数组的声明与之类似:

var jobs: Array<number> = [1, 2, 3];
var jobs: number[] = [4, 5, 6];

5.4.5 枚举

枚举是一组可命名数值的集合。 比如, 如果我们想拿到某人的一系列角色, 可以这么写:

enum Role {Employee, Manager, Admin};
var role: Role = Role.Employee;

默认情况下, 枚举类型的初始值是 0。 我们也可以调整初始化值的范围:

enum Role {Employee = 3, Manager, Admin};
var role: Role = Role.Employee;

在上面的代码中, Employee 的初始值被设置为 3 而不是 0。 枚举中其他项的值是依次递增的, 意味着 Manager 的值为 4, Admin 的值为 5。 同样, 我们也可以单独为枚举中的每一项指定值:

enum Role {Employee = 3, Manager = 5, Admin = 7};
var role: Role = Role.Employee;

还可以从枚举的值来反查它的名称:

enum Role {Employee, Manager, Admin};
console.log('Roles: ', Role[0], ',', Role[1], 'and',Role[2]);

5.4.6 任意类型

如果我们没有为变量指定类型, 那它的默认类型就是 any。 在 TypeScript中, any 类型的变量能够接收任意类型的数据:

var something: any = 'as string';
something = 1;
something = [1, 2, 3];

5.4.7 “ 无” 类型

void 意味着我们不期望那里有类型。 它通常用作函数的返回值, 表示没有任何返回值:

function setName(name: string): void {
this.name = name;
}

第 6 章 表单

6.1 表单—— 既重要,又复杂

在 Web 应用中, 表单或许是最重要的部分。 虽然我们常从点击链接或移动鼠标中得到事件通知, 但大多数“富数据”都是通过表单从用户那里获得的。
从表面上看, 表单似乎很简单: 创建一个 input 标签, 用户填入数据,然后再点击提交。 这有什么难的?
但事实证明, 表单最终可能是非常复杂的。 原因如下:

  • 表单输入意味着需要在页面和服务器端同时修改这份数据;

  • 修改的内容通常要在页面的其他地方反映出来;

  • 用户的输入可能存在很多问题, 所以需要验证输入的内容;

  • 用户界面需要清晰地显示出可能出现的预期结果和错误信息;

  • 字段之间的依赖可能存在复杂的业务逻辑;

我们希望不依赖 DOM 选择器就能轻松测试表单。
值得庆幸的是, Angular 已经给出了上述所有问题的解决方案。

  • 表单控件(FormControl) 封装了表单中的输入, 并提供了一些可供操
    纵的对象。
  • 验证器(validator) 让我们能以自己喜欢的任何方式验证表单输入。
  • 观察者(observer) 让我们能够监听表单的变化, 并作出相应的回应。

6.2 获取用户输入

当用户点击链接、按下按钮或者输入文字时,这些用户动作都会产生 DOM 事件。 使用 Angular 事件绑定机制来响应任何 DOM 事件。

6.2.1 使用$event

import { Component} from '@angular/core';
@Component({
selector: 'app-form-basic',
template: `
<div>
<h3>请输入:</h3>
<input type="text" (keyup)="getValue($event)">
<p>{{values}}</p>
</div>`
})
export class FormBasicComponent {
values = '';
getValue(event: any){
this.values += event.target.value + ' | ';
}
}

传入 $event 是靠不住的做法

类型化事件对象揭露了重要的一点,即反对把整个 DOM 事件传到方法中,因为
这样组件会知道太多模板的信息。只有当它知道更多它本不应了解的 HTML 实
现细节时,它才能提取信息。 这就违反了模板(用户看到的)和组件(应用如
何处理用户数据)之间的分离关注原则。

6.2.2 量 使用模板变量 #var

使用 Angular 的模板引用变量。 这些变量提供了从模块中直接访问元素的能力。 在标识符前加上井号 (#) 就能声明一个模板引用变量。
下面的例子使用了局部模板变量,在一个超简单的模板中实现按键反馈功能。

<div>
<h3>请输入:</h3>
<input type="text" #box (keyup)="1">
<p>{{box.value}}</p>
</div>

本例代码将 keyup 事件绑定到了数字 0,这是可能是最短的模板语句。 虽然这个语句不做什么,但它满足 Angular 的要求,所以 Angular 将更新屏幕。

6.2.3 事件修饰符

<div>
<h3>请输入:</h3>
<input type="text" #box (keyup.enter)="onEnter(box.value)">
<p>{{values}}</p>
</div>
onEnter(value){
this.values += value + ' | ';
}

(keyup)事件处理器监听每一次按键。 有时只在意回车键,因为它标志着用户结束输入。 解决这个问题的一种方法是检查每个$event.keyCode,只有键值是回车键时才采取行动。
更简单的方法是:绑定到 Angular 的 keyup.enter 模拟事件。 然后,只有当用户敲回车键时,Angular 才会调用事件处理器。

6.2.4 使用 NgModel 双向绑定

[(ngModel)]语法,使表单绑定到模型的工作变得超级简单。

import { Component} from '@angular/core';
@Component({
selector: 'app-form-basic',
template: `
<input type="text" [(ngModel)]="user.name">
<p>名字:{{user.name}}</p>`
})
export class FormBasicComponent {
user = {};
}

6.2.5 过 通过 ngModel 跟踪修改状态与有效性验证

在表单中使用 ngModel 可以获得比仅使用双向数据绑定更多的控制权。它还会告诉我们很多信息:用户碰过此控件吗?它的值变化了吗?数据变得无效了吗?

NgModel 指令不仅仅跟踪状态。它还使用特定的 Angular CSS 类来更新控件,以反映当前状态。 可以利用这些 CSS 类来修改控件的外观,显示或隐藏消息。

状态 为真时的css类 为假时的css类
控件被访问过 ng-touched ng-untouched
控件的值变化了 ng-dirty ng-pristine
控件的值有效 ng-valid ng-invalid

往姓名标签上添加名叫 spy 的临时模板引用变量,然后用这个 spy来显示它上面的所有 CSS 类。

<input type="text"
[(ngModel)]="user.name"
name="name"
required
#spy>
<p>名字:{{user.name}} - {{spy.className}}</p>

6.3 模板驱动表单校验

我们可以通过验证用户输入的准确性和完整性,来增强整体数据质量。
为了往模板驱动表单中添加验证机制,我们要添加一些验证属性,就像原生的HTML 表单验证器。 Angular 会用指令来匹配这些具有验证功能的指令。
每当表单控件中的值发生变化时,Angular 就会进行验证,并生成一个验证错误的列表(对应着 INVALID 状态)或者 null(对应着 VALID 状态)。

6.3.1 模板变量

我们可以通过把 ngModel 导出成局部模板变量来查看该控件的状态。 比如下面这个例子就把 NgModel 导出成了一个名叫 name 的变量:

<input type="text"
[(ngModel)]="user.name"
name="name"
required
minlength="4"
#name="ngModel">
<!-- valid invalid dirty touched errors -->
<p> valid: {{name.valid}} </p>
<p> invalid: {{name.invalid}} </p>
<p> dirty: {{name.dirty}} </p>
<p> touched: {{name.touched}} </p>
<p> errors: {{name.errors | json}} </p>

注意以下几点:

  • 元素带有一些 HTML 验证属性:required 和 minlength。
  • #name="ngModel"把 NgModel 导出成了一个名叫 name 的局部变量。NgModel 把自己控制的 FormControl 实例的属性映射出去,让我们能在模板中检查控件的状态,比如 valid 和 dirty。
  • errors 返回失败验证所产生的任何错误。如果没有错误,它将返回 null。
  • 依赖 FormsModule 组件,需要提前引入根模块

6.3.2 表单提交

<form (ngSubmit)="onSubmit()">
<input type="text" [(ngModel)]="user.name" name="name"
required minlength="4" appForbiddenName="Tim" #name="ngModel">
<button type="submit" [disabled]="name.valid">提交</button>
</form>
  • ngSubmit 用来控制表单提交,当提交表单时会触发 onSubmit 方法的执
    行。
  • 提交按钮通过disabled属性绑定name.valid来控制表单是否可以提交。

6.4 响应式表单的校验

在响应式表单中,真正的源码都在组件类中。我们不应该通过模板上的属性来添
加验证器,而应该在组件类中直接把验证器函数添加到表单控件模型上(FormControl)。然后,一旦控件发生了变化,Angular 就会调用这些函数。依赖 ReactiveFormsModule 需要提前引入根模块。

6.4.1 验证器函数

有两种验证器函数:同步验证器和异步验证器。

  • 同步验证器函数接受一个控件实例,然后返回一组验证错误或 null。我们可以在实例化一个 FormControl 时把它作为构造函数的第二个参数传进去。

  • 异步验证器函数接受一个控件实例,并返回一个承诺(Promise)或可观察对象(Observable),它们稍后会发出一组验证错误或者 null。我们可以在实例化一个 FormControl 时把它作为构造函数的第三个参数传进去。

注意:出于性能方面的考虑,只有在所有同步验证器都通过之后,Angular 才会运行异步验证器。当每一个异步验证器都执行完之后,才会设置这些验证错误。

6.4.2 内置验证器

我们可以写自己的验证器,也可以使用一些 Angular 内置的验证器。
模板驱动表单中可用的那些属性型验证器(如 required、minlength 等)对应于 Validators 类中的同名函数。
要想把这个表单改造成一个响应式表单,我们还是用那些内置验证器,但这次改为用它们的函数形态。

< form [formGroup]="myGroup" (ngSubmit)="onSubmit()">
<input type="text" [(ngModel)]="user.name" name="name"
required minlength="4" formControlName="name">
<!-- valid invalid dirty touched errors -->
<p> valid: {{name.valid}} </p>
<p> invalid: {{name.invalid}} </p>
<p> dirty: {{name.dirty}} </p>
<p> touched: {{name.touched}} </p>
<p> errors: {{name.errors | json}} </p>
<button type="submit">提交</button>
</form>

这里我们去掉了模板变量 #name=“ngModel” ,使用 formControlName="name"来控制,最外层使用 <div [formGroup]=“myGroup”> 包裹起来 myGroup 是我们自定义的名称。

user = {name:''};
myGroup = {};
ngOnInit(): void {
this.myGroup = new FormGroup({
'name': new FormControl(this.user.name,[
Validators.required,
Validators.minLength(4)
])
});
}
get name(){
return this.myGroup.get('name');
}

使用 FormGroup 来校验表单,每个输入框增加一个 FormControl 进行检验配置,第一个参数是参数值,第二是校验数组。验证依赖 FormsModule 和ReactiveFormsModule 需要添加到根模块@NgModule 中:

import { FormsModule, ReactiveFormsModule } from '@angular/forms';
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule
]

6.4.3 表单提交

onSubmit(){
console.log(this.myGroup.valid);
console.log(this.myGroup.value);
}

在提交方法中可以直接获取表单校验的结果,确定是否需要提交。

6.5 自定义校验

由于内置验证器无法适用于所有应用场景,有时候我们还是得创建自定义验证器。

6.5.1 定义

export function forbiddenNameValidator(nameReg: RegExp):
ValidatorFn{
return (control: AbstractControl): {[key: string]: any} =>{
const forbidden = nameReg.test(control.value);
return forbidden ? {'forbiddenName': {value: control.value}} :
null;
}
}

6.5.2 添加到响应式表单

ngOnInit(): void {
this.myGroup = new FormGroup({
'name': new FormControl(this.user.name,[
Validators.required,
Validators.minLength(4),
forbiddenNameValidator(/lala/i)
])
});
}

6.5.3 添加到模板驱动表单

在模板驱动表单中,我们不用直接访问 FormControl 实例。所以我们不能像响应式表单中那样把验证器传进去,而应该在模板中添加一个指令。
ForbiddenValidatorDirective 指令相当于forbiddenNameValidator 的包装器。
Angular 在验证流程中的识别出指令的作用,是因为指令把自己注册到了NG_VALIDATORS 提供商中,该提供商拥有一组可扩展的验证器。

6.5.3.1 新建自定义校验器:forbidden-name.directive.ts
i mport { Directive, Input } from '@angular/core';
import { NG_VALIDATORS, Validator, AbstractControl, ValidatorFn }
from '@angular/forms';
function forbiddenNameValidator(nameReg: RegExp): ValidatorFn{
return (control: AbstractControl): {[key: string]: any} =>{
const forbidden = nameReg.test(control.value);
return forbidden ? {'forbiddenName': {value: control.value}} :
null;
}
}
@Directive({
selector: '[appForbiddenName]',
providers: [{provide: NG_VALIDATORS, useExisting:
ForbiddenValidatorDirective, multi: true}]
})
export class ForbiddenValidatorDirective implements Validator {
@Input() appForbiddenName: string;
validate(control: AbstractControl): {[key: string]: any} {
return this.appForbiddenName ? forbiddenNameValidator(new
RegExp(this.appForbiddenName, 'i'))(control)
: null;
}
}
6.5.3.2 添加到根模块
import { ForbiddenValidatorDirective } from
'./form-basic/forbidden-name.directive'
declarations: [
AppComponent,
ForbiddenValidatorDirective
],
6.5.3.3 模板引用
<input type="text"
[(ngModel)]="user.name"
name="name"
required
minlength="4"
appForbiddenName="Tim"
#name="ngModel">

当输入的文本中有 Tim 时就验证失败。

6.6 响应式表单

Angular 提供了两种构建表单的技术:响应式表单和模板驱动表单。 这两项技术都属于@angular/forms 库,并且共享一组公共的表单控件类。但是它们在设计哲学、编程风格和具体技术上有显著区别。 所以,它们都有自己的模块:ReactiveFormsModule 和 FormsModule。

6.6.1 响应式表单

Angular 的响应式表单能让实现响应式编程风格更容易,这种编程风格更倾向于在非 UI 的数据模型(通常接收自服务器)之间显式的管理数据流, 并且用一个 UI 导向的表单模型来保存屏幕上 HTML 控件的状态和值。响应式表单可以让使用响应式编程模式、测试和校验变得更容易。

6.6.2 模板驱动表单

不用自己创建 Angular 表单控件对象。Angular 指令会使用数据绑定中的信息创建它们。 我们不用自己推送和拉取数据。Angular 使用 ngModel 来替你管理它们。 当用户做出修改时,Angular 会据此更新可变的数据模型。
虽然这意味着组件中的代码更少,但是模板驱动表单是异步工作的,这可能在更高级的场景中让开发复杂化。

6.6.3 步 异步 vs. 同步

响应式表单是同步的。模板驱动表单是异步的。这个不同点很重要。
使用响应式表单,我们会在代码中创建整个表单控件树。 我们可以立即更新一个值或者深入到表单中的任意节点,因为所有的控件都始终是可用的。

模板驱动表单会委托指令来创建它们的表单控件。 为了消除“检查完后又变化了”的错误,这些指令需要消耗一个以上的变更检测周期来构建整个控件树。 这意味着在从组件类中操纵任何控件之前,我们都必须先等待一个节拍。
比如,如果我们用@ViewChild(NgForm)查询来注入表单控件,并在生命周期钩子 ngAfterViewInit 中检查它,就会发现它没有子控件。 我们必须使用setTimeout 等待一个节拍才能从控件中提取值、测试有效性,或把它设置为
新值。
两种架构范式,各有优缺点。 请自行选择更合适的方法,甚至可以在同一个应用中同时使用它们。

6.6.4 基础的表单类

  • AbstractControl 是三个具体表单类的抽象基类。 并为它们提供了一些共同的行为和属性,其中有些是可观察对象(Observable)。
  • FormControl 用于跟踪一个单独的表单控件的值和有效性状态。它对应于一个 HTML 表单控件,比如输入框和下拉框。
  • FormGroup用于 跟踪一组AbstractControl的实例的值和有效性状态。该 组的属性中包含了它的子控件。 组件中的顶级表单就是一 个FormGroup。
  • FormArray用于跟踪AbstractControl实例组成的有序数组的值和有效性状态。

6.7 FormBuilder 简介

FormBuilder 类能通过处理控件创建的细节问题来帮我们减少重复劳动。借助数据模型可以很方便的帮助处理表单数据和数据校验。

6.7.1 使用 FormBuilder 创建 FormGroup

userForm: FormGroup;
constructor(private fb: FormBuilder) {
this.userForm = fb.group({
name: ['Tim', Validators.required],
pwd: '123456')
});
}

在构造函数中使用 FormBuilder 的实例 fb 创建一个 group,name 和 pwd 分别对应表单元素的值。name 添加了校验器为一个必输项。

6.7.2 模板引用

<h3>Basic Form</h3>
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<div>
<label for="">姓名:</label>
<input type="text" formControlName="name">
</div>
<div>
<label for="">密码:</label>
<input type="text" formControlName="pwd">
</div>
</form>

formGroup 属性引用我们创建好的 userForm,通过 formControlName 来引用 formGroup 中表单模型的值。

6.7.2 多级 FromGroup

多个 FromGroup 可以嵌套使用,用来处理复杂的表单数据嵌套。

constructor(private fb: FormBuilder) {
this.userForm = fb.group({
name: ['Tim', Validators.required],
pwd: '123456',
address: fb.group({
street: 'road 207',
city: 'shanghai'
})
});
}

address 属性有两个属性 street 和 city,使用 fb.group 创建多级表单数据。

address 属性有两个属性 street 和 city,使用 fb.group 创建多级表单数据。

6.7.3 模板引用

<div formGroupName="address">
<div>
<label for="">街道:</label>
<input type="text" formControlName="street">
</div>
<div>
<label for="">城市:</label>
<input type="text" formControlName="city">
</div>
</div>
<div>

使用时需要在父级指定 formGroupName=”address” 然后表单中直接使用该对象定义的属性名。

6.7.4 查看 FormControl 的属性

此刻,我们把整个表单模型展示在了页面里。 但有时我们可能只关心一个特定FormControl 的状态。
我们可以使用.get()方法来提取表单中一个单独 FormControl 的状态。 我们可以在组件类中这么做,或者通过往模板中添加{{form.value | json}}插值来把它显示在页面中,FormControl 属性:

属性 说明
myControl.value FormControl 的值。
myControl.status FormControl 的有效性。可能的值有 VALID、INVALID、PENDING 或 DISABLED。
myControl.pristine 如果用户尚未改变过这个控件的值,则为 true。它总是与myControl.dirty 相反。
myControl.untouched 如果用户尚未进入这个 HTML 控件,也没有触发过它的 blur(失去焦点)事件,则为 true。 它是 myControl.touched的反义词。
<div>
<p> {{userForm.value | json}} </p>
<p> {{userForm.get('name').value}} </p>
<p> {{userForm.get('pwd').value}} </p>
<p> {{userForm.get('address.street').value}} </p>
<p> {{userForm.get('address.city').value}} </p>
<p> ----------------------- </p>
<p> {{userForm.status}} </p>
<p> {{userForm.pristine}} </p>
<p> {{userForm.untouched}} </p>
</div>

6.8 重置表单

通过 reset 方法可以重置表单,同时也可以在重置时传入 state 值,来初始化表单。

<p>
<button type="button" (click)="forceReset()">重置为空</button>
<button type="button" (click)="defaultReset()">重置默认数据
</button>
<button type="submit">提交</button>
</p>

定义重置方法:

forceReset() {
this.userForm.reset();
}
defaultReset() {
this.userForm.reset({
name: 'Tim',
pwd: '123456',
address: {
street: 'road 207',
city: 'shanghai'
}
});
}

6.9 数据模型与表单模型

用户修改时的数据流是从 DOM 元素流向表单模型的,而不是数据模型。表单控件永远不会修改数据模型。
表单模型和数据模型的结构并不需要精确匹配。在一个特定的屏幕上,我们通常只会展现数据模型的一个子集。 但是表单模型的形态越接近数据模型,事情就会越简单。

6.9.1 定义数据模型

export class Address {
street = '';
city = '';
}

数据模型中,定义了两个字段:street 和 city。

6.9.2 setValue

借助 setValue,我们可以立即设置每个表单控件的值,只要把与表单模型的属性精确匹配的数据模型传进去就可以了。

setValue() {
var addressArr: Address[] = [
{street:'road 808', city:'shanghai'},
{street:'road 809', city:'beijing'}
];
this.userForm.setValue({
name:'Jack',
pwd:'8888',
address: addressArr[0] || new Address()
});
}

setValue 方法会在赋值给任何表单控件之前先检查数据对象的值。
它不会接受一个与 FormGroup 结构不同或缺少表单组中任何一个控件的数据对象。 这种方式下,如果我们有什么拼写错误或控件嵌套的不正确,它就能返回一些有用的错误信息。

6.9.3 patchValue

patchValue 可以更灵活地解决数据模型和表单模型之间的差异。 但是和setValue 不同,patchValue 不会检查缺失的控件值,并且不会抛出有用的错误信息。

patchValue() {
this.userForm.patchValue({
name:'Mac',
pwd:'9999'
});
}

第 7 章 组件

7.1 组件交互

7.1.1 通过输入型绑定把数据从父组件传到子组件。

import { Component } from '@angular/core';
@Component({
selector: 'app-parent-child',
template: `
<app-child [name]="name" [age]="age"></app-child>`
})
export class ParentChildComponent {
name = 'Tim';
age = 12;
}

父组件定义了两个属性 name 和 age,通过绑定方式出入子组件。

7.1.2 子组件接收

import { Component, Input } from '@angular/core';
@Component({
selector: 'app-child',
template: `<div>
<h3>{{myName}}</h3>
<p>{{age}}</p>
</div>`
})
export class ChildComponent{
@Input('name') myName;
@Input() age;
}

子组件接收需要指定@Input 注解,表示改属性可输入。name 属性起了个别名myName,在子组件内部统一使用 myName。

7.1.3 过 通过 setter 截听输入属性值的变化

使用一个输入属性的 setter,以拦截父组件中值的变化,并采取行动。

private _age;
@Input()
set age(age: number){
this._age = age || 0;
}
get age(){
return this._age;
}

使用:

<app-child [name]="name" age></app-child>

7.1.4 父组件监听子组件的事件

子组件暴露一个 EventEmitter 属性,当事件发生时,子组件利用该属性emits(向上弹射)事件。父组件绑定到这个事件属性,并在事件发生时作出回应。子组件的 EventEmitter 属性是一个输出属性,通常带有@Output 装饰器,

import { Component } from '@angular/core';
@Component({
selector: 'app-parent-child',
template: `
<app-child [name]="name" [age]="age"
(changAge)="onchangeAge($event)"></app-child>
`
})
export class ParentChildComponent {
name = 'Tim';
age = 12;
onchangeAge(age){
this.age = age;
}
}

通过() 自定义事件 changeAge 传入子组件,子组件通过@Output 装饰器定义的对象进行接收,并通过 emit 方法触发事件执行。并且在触发是可以纯如载荷。

i mport { Component, Input, Output, EventEmitter } from
'@angular/core';
@Component({
selector: 'app-child',
template: `
<div (click)="change()">
<h3>{{myName}}</h3>
<p>{{age}}</p>
</div>
`
})
export class ChildComponent{
@Input('name') myName;
private _age;
@Input()
set age(age: number){
this._age = age || 0;
}
get age(){
return this._age;
}
@Output() changAge = new EventEmitter();
change(){
this.changAge.emit(24);
}
}

7.1.5 父子组件通过本地变量互动

@Component({
selector: 'app-parent-child',
template: `<div>
<button (click)="child.change()">触发子组件</button>
<app-child [name]="name" [age]="age"
(changAge)="onchangeAge($event)" #child></app-child>
</div>`
})

子组件 app-child 中添加模板变量#child 在模板中就可以通过 child 变量引用到子组件,直接使用子组件的的方法和变量。

7.1.6 父组件调用@ViewChild()

本地变量方法是个简单便利的方法。但是它也有局限性,因为父组件-子组件的连接必须全部在父组件的模板中进行。父组件本身的代码对子组件没有访问权。如果父组件的类需要读取子组件的属性值或调用子组件的方法,就不能使用本地变量方法。
当父组件类需要这种访问时,可以把子组件作为 ViewChild,注入到父组件里面。

import { Component, ViewChild } from '@angular/core';
import { ChildComponent } from './child.component';
@Component({
selector: 'app-parent-child',
template: `
<div>
<button (click)="controlChild()">触发子组件</button>
<app-child [name]="name" [age]="age"
(changAge)="onchangeAge($event)"></app-child>
</div>
`
})
export class ParentChildComponent{
name = 'Tim';
age = 12;
onchangeAge(age){
this.age = age;
}
@ViewChild(ChildComponent)
private child: ChildComponent;
controlChild(){
this.child.change();
}
}

7.2 组件样式

Angular 应用使用标准的 CSS 来设置样式。这意味着我们可以把关于 CSS的那些知识和技能直接用于我们的 Angular 程序中,例如:样式表、选择器、规则以及媒体查询等。另外,Angular 还能把组件样式捆绑在我们的组件上,以实现比标准样式表更加模块化的设计。

7.2.1 使用组件样式

对于我们写的每个 Angular 组件来说,除了定义 HTML 模板之外,我们还要定义用于模板的 CSS 样式、 指定任意的选择器、规则和媒体查询。
实现方式之一,是在组件的元数据中设置 styles 属性。 styles 属性可以接受一个包含 CSS 代码的字符串数组。 通常我们只给它一个字符串就行了,如同下例:

@Component({
selector: 'app-my-style',
template: `<h3>
Hello world!!
</h3>`,styles: [`h3{color: red;}`]
})

我们放在组件样式中的选择器,只会应用在组件自身的模板中。上面这个例子中
的 h31 选择器只会对当前组件模板中的

标签生效,而对应用中其它地方的<h3>元素毫无影响。

  • 这种模块化相对于 CSS 的传统工作方式是一个巨大的改进。
  • 可以使用对每个组件最有意义的 CSS 类名和选择器。
  • 类名和选择器是仅属于组件内部的,它不会和应用中其它地方的类名和选择器出现冲突。
  • 我们组件的样式不会因为别的地方修改了样式而被意外改变。
  • 我们可以让每个组件的 CSS 代码和它的 TypeScript、HTML 代码放在一起,这将促成清爽整洁的项目结构。

将来我们可以修改或移除组件的 CSS 代码,而不用遍历整个应用来看它有没有被别处用到,只要看看当前组件就可以了。

7.2.2 styles 属性

styles 属性需要传入一个数组,在数组中可以传入多个样式字符串来控制组件样式。

styles: [`h3{color: red;}`, `*{font-size:16px;}`]

7.2.3 styleUrls 属性

styleUrls 属性作用和 styles 一样,区别是需要传入的是 css 样式文件的路径。

styleUrls: ['./my-style.component.css']

7.2.4 模板内联样式
在模板中直接使用 style 标签,跟平时写样式一样。

template: `
<style>
*{font-size:16px;}
h3{color: red;}
</style>
<h3>
Hello world!!
</h3>`,

7.2.5 使用 link 标签

在组件的 HTML 模板中嵌入标签。这个 link 标签的 href 指向的URL 是相对于应用的根目录。

@Component({
selector: 'app-my-style',
template: `
<link rel="stylesheet" href="assets/my-style.component.css">
<h3>
Hello world!!
</h3>`
})

7.2.6 @import 语法

还可以利用标准的 CSS @import 规则来把其它 CSS 文件导入到我们的 CSS文件中。在这种情况下,URL 是相对于我们执行导入操作的 CSS 文件的。

@import url(./assets/my-style.component.css);

7.2.7 控制视图的封装模式

组件的 CSS 样式被封装进了自己的视图中,而不会影响到应用程序的其它部分。通过在组件的元数据上设置视图封装模式,我们可以分别控制每个组件的封装模式。可选的封装模式一共有如下几种:生 原生 (Native)真 、仿真 (Emulated)和无 (None)。

  • Native 模式使用浏览器原生的 Shadow DOM 实现来为组件的宿主元素附加一个 Shadow DOM。组件的样式被包裹在这个 Shadow DOM 中。 不进不出,没有样式能进来,组件样式出不去。
  • Emulated 模式(默认值)通过预处理(并改名)CSS 代码来模拟 ShadowDOM 的行为,以达到把 CSS 样式局限在组件视图中的目的。 只进不出,全局样式能进来,组件样式出不去。
  • None 意味着 Angular 不使用视图封装。 Angular 会把 CSS 添加到全局样式中。而不会应用上前面讨论过的那些作用域规则、隔离和保护等。 从本质上来说,这跟把组件的样式直接放进 HTML 是一样的。 能进能出。

通过组件元数据中的 encapsulation 属性来设置组件封装模式:

encapsulation: ViewEncapsulation.Native

7.3 特殊选择器

组件样式中有一些从影子(Shadow) DOM 样式范围领域(记录在 W3C 的 CSS Scoping Module Level 1 中) 引入的特殊选择器:

7.3.1 :host 选择器

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

styles: [`
h3{color: red;}
:host{
display: block;
background: yellow;
}
`]

这是我们能以宿主元素为目标的唯一方式。除此之外,我们将没办法指定它, 因为宿主不是组件自身模板的一部分,而是父组件模板的一部分。

要把宿主样式作为条件,就要像函数一样把其它选择器放在:host 后面的括号中。下面只有当宿主同时带有 active CSS 类的时候才会生效。

:host(.active) {
border: 3px solid red;
}

7.3.2 :host-context 选择器

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

:host-context(.italic) h3{
font-style: italic;
}

7.3.3 弃 已废弃 /deep/ 、>>> 和::ng-deep

组件样式通常只会作用于组件自身的 HTML 上。
我们可以使用/deep/选择器,来强制一个样式对各级子组件的视图也生效,它不但作用于组件的子视图,也会作用于组件的内容。
在这个例子中,我们以所有的

元素为目标,从宿主元素到当前元素再到DOM 中的所有子元素:

:host /deep/ h3 {
font-style: italic;
}

7.4 组件生命周期

每个组件都有一个被 Angular 管理的生命周期。Angular 创建它,渲染它,创建并渲染它的子组件,在它被绑定的属性发生变化时检查它,并在它从 DOM中被移除前销毁它。
Angular 提供了生命周期钩子,把这些关键生命时刻暴露出来,赋予我们在它们发生时采取行动的能力。

除了那些组件内容和视图相关的钩子外,指令有相同生命周期钩子。

指令和组件的实例有一个生命周期:新建、更新和销毁。 通过实现一个或多个Angular core 库里定义的生命周期钩子接口,开发者可以介入该生命周期中的这些关键时刻。
每个接口都有唯一的一个钩子方法,它们的名字是由接口名再加上 ng 前缀构成的。比如,OnInit 接口的钩子方法叫做 ngOnInit, Angular 在创建组件后立刻调用它:

e xport class LifecycleComponent implements OnInit {
constructor() { }
ngOnInit() {
console.log('组件创建...');
}
}

7.4.1 生命周期的顺序

当 Angular 使用构造函数新建一个组件或指令后,就会按下面的顺序在特定时刻调用这些生命周期钩子方法:

钩子 目的和时机
ngOnChanges() 方法接受当前和上一属性值的 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, OnInit, OnChanges, DoCheck, AfterContentInit,
AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy,
SimpleChanges, Input } from '@angular/core';
@Component({
selector: 'app-lifecycle-child',
template:`
<div>
<input type="text" [(ngModel)]="count">
<p>count: {{count}}</p>
</div>
`
})
export class LifecycleChildComponent implements OnChanges, OnInit,
DoCheck,
AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy {
@Input() count;
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
console.log('changes...', changes);
}
ngOnInit() {
console.log('init...');
}
ngOnDestroy(): void {
console.log("destory...");
}
ngDoCheck(): void {
console.log("check...");
}
ngAfterViewInit(): void {
console.log("afterViewInit...");
}
ngAfterContentChecked(): void {
console.log("afterContentChecked...");
}
ngAfterContentInit(): void {
console.log("afterContentInit...");
}
ngAfterViewChecked(): void {
console.log("afterViewChecked...");
}
}
@Component({
selector: 'app-lifecycle',
template: `
<div>
<p>
<button (click)="changes()">changes</button>
<button (click)="destroy()">destroy</button>
<button (click)="init()">init</button>
</p>
<div *ngIf="flag">
<app-lifecycle-child [count]=count></app-lifecycle-child>
</div>
</div>`
})
export class LifecycleComponent {
count = 0;
flag = true;
changes() {
this.count ++;
}
destroy(){
this.flag = false;
}
init(){
this.flag = true;
}
}

7.4.2 OnInit 和 和 OnDestroy

我们可以通过指令来监听组件的创建和销毁,指令是一种完美的渗透方式。

import { Directive, OnInit, OnDestroy } from '@angular/core';
@Directive({
selector: '[appLifecycle]'
})
export class LifecycleDirective implements OnInit, OnDestroy {
ngOnDestroy(): void {
console.log('组件销毁...');
}
ngOnInit(): void {
console.log('组件创建...');
}
}
7.4.2.1 OnInit() 钩子

使用 ngOnInit()有两个原因:

  1. 在构造函数之后马上执行复杂的初始化逻辑。
  2. 在 Angular 设置完输入属性之后,对该组件进行准备。ngOnInit()是组件获取初始数据的好地方
7.4.2.2 OnDestroy() 钩子

一些清理逻辑必须在 Angular 销毁指令之前运行,把它们放在 ngOnDestroy()中。
这是在该组件消失之前,可用来通知应用程序中其它部分的最后一个时间点。
这里是用来释放那些不会被垃圾收集器自动回收的各类资源的地方。取消那些对可观察对象和 DOM 事件的订阅。停止定时器。注销该指令曾注册到全局服务或应用级服务中的各种回调函数。如果不这么做,就会有导致内存泄露的风险。

7.4.2.3 OnChanges() 钩子

一旦检测到该组件(或指令)的输入属性发生了变化,Angular 就会调用它的ngOnChanges()方法。

ngOnChanges(changes: SimpleChanges) {
for (let propName in changes) {
let chng = changes[propName];
let cur = JSON.stringify(chng.currentValue);
let prev = JSON.stringify(chng.previousValue);
console.log(`${propName}: currentValue = ${cur}, previousValue =
${prev}`);
}
}

ngOnChanges()方法获取了一个对象,它把每个发生变化的属性名都映射到了一个 SimpleChange 对象, 该对象中有属性的当前值和前一个值。我们在这些发生了变化的属性上进行迭代,并记录它们。

7.4.2.4 DoCheck() 钩子

使用 DoCheck 钩子来检测那些 Angular 自身无法捕获的变更并采取行动。是对OnChanges 事件的有力补充,但我们必须小心。 这个 ngDoCheck 钩子被非常频繁的调用 —— 在每次变更检测周期之后,发生了变化的每个地方都会调它。

7.4.2.5 AfterView 钩子

Angular 会在每次创建了组件的子视图后调用它,在每次视图发生数据变化重新渲染时,也会被触发。
Angular 的“单向数据流”规则禁止在一个视图已经被组合好之后再更新视图。而这两个钩子都是在组件的视图已经被组合好之后触发的。如果我们立即更新组件中被绑定的 comment 属性,Angular 就会抛出一个错误。在这里更新视图属性之前,要等上一拍。

comment = '';
ngAfterViewChecked(): void {
console.log("afterViewChecked...");
if(this.count>1000){
setTimeout(()=>{
this.comment = 'This is big number';
},0);
}
}
7.4.2.6 AfterContent 钩子

Angular 会在外来内容被投影到组件中之后调用它们。

内容投影:内容投影是从组件外部导入 HTML 内容,并把它插入在组件模板中指定位置上的一种途径。

对比前一个例子考虑这个变化。 这次,我们不再通过模板来把子视图包含进来,而是改从父组件中导入它。下面是父组件的模板:

@Component({
selector: 'app-after-view-father',
template: `
<app-after-view-cont>
<app-after-view-child [count]="count"></app-after-view-child>
</app-after-view-cont>
`
})

注意,标签被包含在标签中。 永远不要在组件标签的内部放任何内容 —— 除非我们想把这些内容投影进这个组件中。
现在来看下组件的模板:

@Component({
selector: 'app-after-view-cont',
template:`<div>-- projected content begins --</div><ng-content></ng-content><div>-- projected content ends --</div>
`
})

<ng-content>标签是外来内容的占位符。 它告诉 Angular 在哪里插入这些
外 来 内 容 。 在 这 里 , 被 投 影 进 去 的 内 容 就 是 来 自 父 组 件 的
<app-after-view-child>标签。

AfterContent 钩子和 AfterView 相似。关键的不同点是子组件的类型不同。

  • AfterView 钩子所关心的是 ViewChildren,这些子组件的元素标签会出
    现在该组件的模板里面。
  • AfterContent 钩子所关心的是 ContentChildren,这些子组件被
    Angular 投影进该组件中。

使用 AfterContent 时,无需担心单向数据流规则。

7.5 动态组件

组件的模板不会永远是固定的。应用可能会需要在运行期间加载一些新的组件。
下面我们做一个类似 banner 轮播图切换的动态组件。

7.5.1 指令

在添加组件之前,先要定义一个锚点来告诉Angular 要把组件插入到什么地方。
广告条使用一个名叫 AdDirective 的辅助指令来在模板中标记出有效的插入
点。

import { Directive, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[ad-host]',
})
export class AdDirective {
constructor(public viewContainerRef: ViewContainerRef) { }
}

7.5.2 加载组件

广告条的大部分实现代码都在 ads.component.ts 中。 为了让这个例子简单
点,我们把 HTML 直接放在了@Component 装饰器的 template 属性中。
<ng-template>元素就是组件动态加载的地方。

@Component({
selector: 'app-ads',
template: `<div>
<h3>广告位</h3>
<ng-template ad-host></ng-template>
</div>`})

7.5.3 解析组件

AdsComponent 接收一个数组作为输入,对象指定要加载的组件类,以及绑定到该组件上的任意数据。给 AdsComponent 传入一个组件数组可以让我们在模板中放入一个广告的动态列表,而不用写死在模板中。
AdsComponent 可以循环遍历ads的数组 ,并且每三秒调用一次loadComponent()来加载新组件。

export class AdsComponent implements AfterViewInit, OnInit {
ads = [
{component: AdFruitComponent, data:{title:'apple',cont:'big red
apple!!!'}},
{component: AdPhoneComponent, data:{title:'iPhone X',cont:'big
fullscreen phone!!!'}},
{component: AdFruitComponent, data:{title:'banana',cont:'big
yellow banana!!!'}},
{component: AdPhoneComponent, data:{title:'xiaomi
X2',cont:'black technology phone!!!'}}
];
adIndex = -1;
@ViewChild(AdDirective) adHost: AdDirective;
constructor(private componentFactoryResolver:
ComponentFactoryResolver) { }
loadComponent() {
this.adIndex = (this.adIndex+1) % this.ads.length;
console.log(this.adIndex);
let adItem = this.ads[this.adIndex];
let ad =
this.componentFactoryResolver.resolveComponentFactory(adItem.c
omponent);
let viewContainerRef = this.adHost.viewContainerRef;
viewContainerRef.clear();
let componentRef = viewContainerRef.createComponent(ad);
(<AdComponent>componentRef.instance).data = adItem.data;
}
ngOnInit(): void {
this.loadComponent();
}
ngAfterViewInit(): void {
setInterval(()=>{
this.loadComponent();
},3000);
}
}

这里的loadComponent()方法很重要。在loadComponent()选取了一个广告之后,它使用ComponentFactoryResolver来为每个具体的组件解析出一个 ComponentFactory。 然后 ComponentFactory 会为每一个组件创建一个实例。
接下来,我们要把viewContainerRef指向这个组件的现有实例,用来告诉Angular 该把动态组件插入到什么位置。在AdDirective曾在它的构造函数中注入了一个 ViewContainerRef。 因此这个指令可以访问到这个被我们用作动态组件宿主的元素。
要 把 这 个 组 件 添 加 到 模 板 中 , 我 们 可 以 调 用 ViewContainerRef 的createComponent()。
createComponent()方法返回一个引用,指向这个刚刚加载的组件。 使用这个引用就可以与该组件进行交互,比如设置它的属性或调用它的方法。ngOnInit 钩子函数用来初始化一个广告,ngAfterViewInit 钩子函数设置一个定时器 3s 后更新广告。
ad.component.ts

export interface AdComponent {
data: any;
}

panel.component.ts

import { Component, OnInit, Input } from '@angular/core';
import { AdComponent } from './ad.component'
@Component({
selector: 'app-ad-fruit',
template: `<div class="ad">
<h3>{{data.title}}</h3>
<p>
{{data.cont}}
</p>
</div>`,
styles: [`
.ad{
width: 300px;
height: 200px;
background: aqua;
border-radius: 15px;
padding: 20px;
}
`]
})
export class AdFruitComponent implements AdComponent {
@Input() data;
}
@Component({selector: 'app-ad-phone',
template: `<div class="ad">
<h3>{{data.title}}</h3>
<p>
{{data.cont}}
</p>
</div>`,
styles: [`
.ad{
width: 300px;
height: 200px;
background: burlywood;
border-radius: 15px;
padding: 20px;
}
`]
})
export class AdPhoneComponent implements AdComponent {
@Input() data;
}

第8章 管道

每个应用开始的时候差不多都是一些简单任务:获取数据、转换它们,然后把它们显示给用户。 获取数据可能简单到创建一个局部变量就行,也可能复杂到从WebSocket 中获取数据流。
一旦取到数据,我们可以把它们原始值的 toString 结果直接推入视图中。 但这种做法很少能具备良好的用户体验。 比如,几乎每个人都更喜欢简单的日期格式,例如 1988-04-15,而不是服务端传过来的原始字符串格式 —— Fri Apr15 1988 00:00:00 GMT-0700 (Pacific Daylight Time)。通过引入 Angular 管道,我们可以把这种简单的“显示-值”转换器声明在 HTML中。

8.1 使用管道

管道把数据作为输入,然后转换它,给出期望的输出。 我们将把组件的birthday 属性转换成对人类更友好的日期格式,来说明这一点:

import { Component } from '@angular/core';@Component({
selector: 'app-birthday',
template: `<p>The double 11 day is {{ birthday | date }}</p>`
})
export class BirthdayComponent {
birthday = new Date(2017,11,11);//Dec 11, 2017
}

8.2 管道参数化

管道可能接受任何数量的可选参数来对它的输出进行微调。 我们可以在管道名后面添加一个冒号( : )再跟一个参数值,来为管道添加参数(比如currency:‘EUR’)。 如果我们的管道可以接受多个参数,那么就用冒号来分隔这些参数值(比如 slice:1:5)。

template: `<p>The double 11 day is {{ birthday |
date:'yyyy-MM-dd' }}</p>`

属性绑定切换格式化方式:

import { Component } from '@angular/core';
@Component({
selector: 'app-birthday',
template: `<p>The double 11 day is {{ birthday | date:format }}</p><button (click)="toggle()">toggle</button>
`
})
export class BirthdayComponent {
birthday = new Date(2017,11,11);
flag = true;
get format(){
return this.flag ? 'yyyy-MM-dd' : 'yyyy/MM/dd';
}
toggle(){
this.flag = !this.flag;
}
}

8.3 链式管道

我们可以把管道链在一起,以组合出一些潜在的有用功能。 下面这个例子中,我们把 birthday 链到 DatePipe 管道,然后又链到 UpperCasePipe,这样我们就可以把生日显示成大写形式了。

<p>The double 11 day is {{ birthday | date | uppercase }}</p>

8.4 自定义管道

我们还可以写自己的自定义管道,下面定义一个自动添加中括号[]的管道。

import { Pipe, PipeTransform } from '@angular/core'
@Pipe({name: 'myword'})
export class MywordPie implements PipeTransform{
transform(value: any, ...args: any[]) {
if(args.length<2){
args = ['[',']'];
}
return args[0] + ' ' + value + ' ' + args[1];
}
}

要注意的有两点:

  • 我们使用自定义管道的方式和内置管道完全相同。
  • 我们必须在 AppModule 的 declarations 数组中包含这个管道。
<p>The double 11 day is {{ birthday | date:format | myword }}</p>
<p>The double 11 day is {{ birthday | date:format |
myword:'<':'>' }}</p>

8.5 纯(pure) 管道与非纯(impure) 管道

有两类管道:纯的与非纯的。 默认情况下,管道都是纯的。我们以前见到的每
个管道都是纯的。 通过把它的 pure 标志设置为 false,我们可以制作一个非
纯管道。

8.5.1 纯管道

Angular 只有在它检测到输入值发生了纯变更时才会执行纯管道。纯变更是指对原始类型值(String、Number、Boolean、Symbol)的更改, 或者对对象引用(Date、Array、Function、Object)的更改。
Angular 会忽略(复合)对象内部的更改。 如果我们更改了输入日期(Date)中的月份、往一个输入数组(Array)中添加新值或者更新了一个输入对象(Object)的属性,Angular 都不会调用纯管道。

这可能看起来是一种限制,但它保证了速度。 对象引用的检查是非常快的(比递归的深检查要快得多),所以 Angular 可以快速的决定是否应该跳过管道执行和视图更新。
因此,如果我们要和变更检测策略打交道,就会更喜欢用纯管道。 如果不能,我们就可以转回到非纯管道。

8.5.2 非纯管道

Angular 会在每个组件的变更检测周期中执行非纯管道。非纯管道可能会被调用很多次,和每个按键或每次鼠标移动一样频繁。我们必须小心翼翼的实现非纯管道。一个昂贵、迟钝的管道将摧毁用户体验。

第 9 章 依赖注入

依赖注入是重要的程序设计模式。 Angular 有自己的依赖注入框架,离开了它,几乎没法构建 Angular 应用。 它使用得非常广泛,以至于几乎每个人都会把它简称为 DI。
依赖注入,它是一种编程模式,可以让类从外部源中获得它的依赖,而不必亲自创建它们。可以在两个地方注入依赖:

  • 在 NgModule 中注册提供商,全局注册
  • 在 Component 中注册提供商, 局部注册

9.1 创建服务

9.1.1 编写服务

编写一个 uer.service 服务,来提供用户的数据。

import { Injectable } from '@angular/core';
import { User } from './user'
@Injectable()
export class UserListService {
users: User[] = [
{name:'zhangsan', age: 10},
{name:'lisi', age: 12},
{name:'Tim', age: 13},
{name:'Tom', age: 14}
];
//返回用户
getUsers(): User[]{
return this.users;
}
}

@Injectable() 标识一个类可以被注入器实例化。 通常,在试图实例化没有被标识为@Injectable()的类时,注入器会报错。User 是定义好的一个数据模型 model:

export class User {
name: string;
age: number;
}

9.1.2 使用服务

import { Component, OnInit, Injectable } from '@angular/core';
import { UserListService } from './user.service'
@Component({
selector: 'app-user-list',
template: `
<ul>
<li *ngFor="let user of users">
姓名:{{user.name}},年龄:{{user.age}}
</li>
</ul>`
})
export class UserListComponent implements OnInit {
users = [];
constructor(private userListService: UserListService) { }
ngOnInit() {
this.users = this.userListService.getUsers();
}
}

使用服务时,只需要把需要注册的类放在构造函数中,Angular 就会知道我们需要那么些服务,自动帮我们注入。把该服务设置为私有 private,那么只有在类内部可以使用服务。

9.1.3 注册服务

在 NgModule 中注册服务,整个模块都可以使用,在 Component 中注册服务只有在组件内部以及子组件中可以使用。

9.1.3.1 全局注册:
@NgModule({
...
providers: [UserListService],
bootstrap: [AppComponent]
})
9.1.3.2 局部注册:
@Component({
selector: 'app-user-list',
template: `<ul>
<li *ngFor="let user of users">
姓名:{{user.name}},年龄:{{user.age}}
</li>
</ul>`,providers: [UserListService]
})

9.2 带依赖类服务

import { Injectable } from '@angular/core';
import { User } from './user'
import { LoggerService } from './logger.service'
@Injectable()
export class UserListService {
constructor(private loggerService:LoggerService){}
users: User[] = [
{name:'zhangsan', age: 10},
{name:'lisi', age: 12},
{name:'Tim', age: 13},
{name:'Tom', age: 14}
];
//返回用户
getUsers(): User[]{
this.loggerService.log('取数据');
return this.users;
}
}

在获取用户列表中添加日志服务 loggerService 用来记录日志。

9.3 别类名提供商

假设某个旧组件依赖一个 OldLogger 类。OldLogger 和 NewLogger 具有相同的接口,但是由于某些原因, 我们不能升级这个旧组件并使用它。

9.3.1 新服务提供商

import { Injectable } from '@angular/core';
@Injectable()
export class BetterLoggerService {
log(text){
console.log(`====start====`);
console.log(`Logger: ${text} `+new Date().toLocaleString());
console.log(`====end====`);
}
}

9.3.2 替换旧服务

@NgModule({...
providers: [
UserListService,
{ provide: LoggerService, useClass: BetterLoggerService }
],
bootstrap: [AppComponent]
})

9.3.3 类名冲突

我们当然不会希望应用中有两个不同的 NewLogger 实例。 不幸的是,如果尝试通过 useClass 来把 OldLogger 作为 NewLogger 的别名,就会导致这样的
后果。

[ NewLogger,
// Not aliased! Creates two instances of `NewLogger`
{ provide: OldLogger, useClass: NewLogger}]

解决方案:使用 useExisting 选项指定别名。

[ NewLogger,
// Alias OldLogger w/ reference to NewLogger
{ provide: OldLogger, useExisting: NewLogger}]

9.4 值提供商

有时,提供一个预先做好的对象会比请求注入器从类中创建它更容易。

let silentLogger = {
log: (text) => {
console.log(`Logger: ${text} `+new Date().toLocaleString());
console.log('Silent logger, Provided via "useValue"');
}
};

使用 useValue 替换服务
providers: [
UserListService,
{
provide: LoggerService,
useValue: silentLogger
}

9.5 工厂提供商

有时,我们需要动态创建这个依赖值,因为它所需要的信息直到最后一刻才能确定。 也许这个信息会在浏览器的会话中不停地变化。这种情况下,请调用工厂提供商。

9.5.1 改写服务

改写 UserListService 服务,在构造函数中添加字段 isAuthorized 来控制用户是否有权限查看隐藏用户。

@Injectable()
export class UserListService {
constructor(private loggerService:LoggerService, private
isAuthorized: boolean){}
users: User[] = [
{name:'zhangsan', age: 10, isShow: false},
{name:'lisi', age: 12, isShow: true},
{name:'Tim', age: 13, isShow: true},
{name:'Tom', age: 14, isShow: false}
];
//返回用户
getUsers(): User[] {
let auth = this.isAuthorized ? 'authorized ' : 'unauthorized';
this.loggerService.log(`是否有权限显示隐藏用户:${auth}`);
return this.users.filter(user=>this.isAuthorized ||
user.isShow);
}
}

9.5.2 创建提供商

通过工厂提供商创建这个 UserListService 的新实例。

import { LoggerService } from './logger.service';
import { UserListService } from './user.service';
let userListServiceFactory = (logger: LoggerService, userList:
UserListService) => {
let isShow = Math.random()>0.5;
return new UserListService(logger, isShow);
}
export let userListServiceProvider = {
provide: UserListService,
useFactory: userListServiceFactory,
deps: [LoggerService]
}

工厂函数动态创建 UserListService 服务,并在创建服务时传入 isShow 参数。
useFactory字段告诉 Angular:这个提供商是一个工厂方法,它的实现是heroServiceFactory。

deps 属性是提供商令牌数组。 Logger 和 UserService 类作为它们自身类提供商的令牌。 注入器解析这些令牌,把相应的服务注入到工厂函数中相应的参数中去。

9.5.3 修改 user-list.component.ts

import { userListServiceProvider } from './user.service.provider'
@Component({
selector: 'app-user-list',
template: `<ul>
<li *ngFor="let user of users">
姓名:{{user.name}},年龄:{{user.age}}
</li>
</ul>`,providers: [userListServiceProvider]
})

9.6 可选依赖

UserListService 需要一个 Logger,但是如果想不提供 Logger 也能得到它,该怎么办呢? 可以把构造函数的参数标记为@Optional(),告诉Angular 该依赖是可选的:

9.6.1 引入

import { Optional } from '@angular/core';

9.6.2 构造注入

constructor(@Optional() private logger: LoggerService){}

9.6.3 使用

/ /返回用户
getUsers(): User[] {
if (this.logger) {
this.logger.log('取数据');
}
return this.users;
}

当使用@Optional()时,代码必须准备好如何处理空值。 如果其它的代码没有注册一个 logger,注入器会设置该 logger 的值为空 null。

9.7 多级依赖注入器

Angular 有一个多级依赖注入系统。 实际上,应用程序中有一个与组件树平行的注入器树。

9.7.1 注入器树

一个应用中可能有多个注入器。 一个 Angular 应用是一个组件树。每个组件实例都有自己的注入器! 组件的树与注入器的树平行。

9.7.2 注入器冒泡

当一个组件申请获得一个依赖时,Angular 先尝试用该组件自己的注入器来满足它。 如果该组件的注入器没有找到对应的提供商,它就把这个申请转给它父组件的注入器来处理。 如果那个注入器也无法满足这个申请,它就继续转给它的父组件的注入器。 这个申请继续往上冒泡 —— 直到我们找到了一个能处理此申请的注入器或者超出了组件树中的祖先位置为止。 如果超出了组件树中的祖先还未找到,Angular 就会抛出一个错误。

9.7.3 在不同层级再次提供同一个服务

我们可以在注入器树中的多个层次上为指定的依赖令牌重新注册提供商。 但并非必须新注册,事实上,虽然可以重新注册,但除非有很好的理由,否则不应该这么做。
服务解析逻辑会自下而上查找,碰到的第一个提供商会胜出。 因此,注入器树中间层注入器上的提供商,可以拦截来自底层的对特定服务的请求。 这导致它可以“重新配置”和者说“遮蔽”高层的注入器。
如果我们只在顶级(通常是根模块 AppModule),这三个注入器看起来将是“面”的。 所有的申请都会冒泡到根 NgModule 进行处理,也就是我们在bootstrapModule 方法中配置的那个。

9.7.4 组件注入器

在不同层次上重新配置一个或多个提供商的能力,开启了一些既有趣又有用的可
能性。

第 10 章 HttpClient 库

大多数前端应用都需要通过 HTTP 协议与后端服务器通讯。现代浏览器支持使用 两 种 不 同 的 API 发 起 HTTP 请 求 : XMLHttpRequest 接 口和 fetch() API。
@angular/common/http 中的 HttpClient 类,Angular 为应用程序提供了 一 个 简 化 的 API 来 实 现 HTTP 功 能 。 它 基 于 浏 览 器 提 供 的XMLHttpRequest 接口。HttpClient 带来的其它优点包括:可测试性、强类型的请求和响应对象、发起请求与接收响应时的拦截器支持,以及更好的、基于可观察(Observable)对象的错误处理机制。

10.1 安装 http 模块

在使用 HttpClient 之前,要先安装 HttpClientModule 以提供它。这可以在应用模块中做,而且只需要做一次。

import {HttpClientModule} from '@angular/common/http';
@NgModule({
...
imports: [
BrowserModule,
HttpClientModule
],
bootstrap: [AppComponent]
})
export class AppModule { }

10.2 请求 JSON 数据

在应用发给服务器的请求中,最常见的就是获取一个 JSON 数据。比如,假设我们有一个用来获取条目列表的 API 端点 http://localhost/user,它会返回一个如下格式的 JSON 对象:

import { Component, OnInit } from '@angular/core';
import { HttpClient, HttpErrorResponse } from
'@angular/common/http';
@Component({
selector: 'app-http-client',
template: `
<ul>
<li *ngFor="let res of results">
{{ res.name }} -- {{ res.age }}
</li>
</ul>
`
})
export class HttpClientComponent implements OnInit {
results;
constructor(private http: HttpClient) { }
ngOnInit(): void {
let url = 'http://localhost/user';
this.http.get(url).subscribe(data=>{
console.log(data);
this.results = data;
});
}
}

10.2.1 安装 express 框架

npm install express --save

10.2.2 后台服务

var express = require('express');
var app = express();
//设置跨域访问
app.all('*', function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,
OPTIONS");
res.header("X-Powered-By",' 3.2.1')
res.header("Content-Type", "application/json;charset=utf-8");
next();
});
var users = [
{name:'Tim', age:12},
{name:'Tom', age:13},
{name:'Jack', age:14},
{name:'Kitty', age:15}
];
app.get('/',function(req, res){
res.end('Hello World!');
});
app.get('/user',function(req, res){
res.json(users);
res.end();
});
var server = app.listen(80, function(){
var host = server.address().address
var port = server.address().port
console.log("应用实例,访问地址为 http://%s:%s", host, port)
});

10.3 读取完整响应体

响应体可能并不包含我们需要的全部信息。有时候服务器会返回一个特殊的响应头或状态码,以标记出特定的条件,因此读取它们可能是必要的。要这样做,我们就要通过 observe 选项来告诉 HttpClient,你想要完整的响应信息,而不是只有响应体:

let url = 'http://localhost/user';
this.http.get(url, {observe: 'response'}).subscribe(data=>{
console.log(data);
this.results = data.body;
});

10.4 错误处理

如果这个请求导致了服务器错误怎么办?甚至,在烂网络下请求都没到服务器该怎么办?HttpClient 就会返回一个错误(error)而不再是成功的响应。要处理它,可以在.subscribe()调用中添加一个错误处理器:

let url = 'http://localhost/user2';
this.http.get(url, {observe: 'response'}).subscribe(data=>{
console.log(data);
this.results = data.body;
},error=>{
console.error('出错了...');
});

10.5 获取错误详情

检测错误的发生是第一步,不过如果知道具体发生了什么错误才会更有用。上面例子中传给回调函数的 err 参数的类型是 HttpErrorResponse,它包含了这个错误中一些很有用的信息。
可能发生的错误分为两种。如果后端返回了一个失败的返回码(如 404、500等),它会返回一个错误。同样的,如果在客户端这边出了错误(比如在 RxJS操作符中抛出的异常或某些阻碍完成这个请求的网络错误),就会抛出一个Error 类型的异常。

let url = 'http://localhost/user2';
this.http.get(url, { observe: 'response' }).subscribe(data => {
console.log(data);
this.results = data.body;
}, (err: HttpErrorResponse) => {
if (err.error instanceof Error) {
console.log('客户端出错:', err.error.message);
} else {
console.log(`服务器返回码 ${err.status}, 返回 html: ${err.error}`);
}
});

10.6 .retry() 操作符

解决问题的方式之一,就是简单的重试这次请求。这种策略对于那些临时性的而且不大可能重复发生的错误会很有用。
RxJS 有一个名叫.retry()的很有用的操作符,它会在遇到错误时自动重新订阅这个可观察对象,也就会导致再次发送这个请求。

首先,导入它:

import 'rxjs/add/operator/retry';

然后,你可以把它用在 HTTP 的可观察对象上,比如这样:

this.http.get(url, { observe:
'response' }).retry(3).subscribe(data => {
console.log(data);
this.results = data.body;
}, (err: HttpErrorResponse) => {
if (err.error instanceof Error) {
console.log('客户端出错:', err.error.message);
} else {
console.log(`服务器返回码 ${err.status}, 返回 html: ${err.error}`);
}
});

10.7 非 请求非 JSON 数据

并非所有的 API 都会返回 JSON 数据。假如我们要从服务器上读取一个文本文件,那就要告诉 HttpClient 我们期望获得的是文本格式的响应:

let url = 'http://localhost';
this.http.get(url,{responseType: 'text'}).subscribe(data=>{
console.log(data);
});

10.8 把数据发送到服务器

除了从服务器获取数据之外,HttpClient 还支持 “修改” 型请求,也就是说,使用各种格式把数据发送给服务器。

10.8.1 GET 传参

get 传参两种形式,一种写在 url 地址后边,另外一种使用 params 属性定义参数。

let url = 'http://localhost/user/add?num=2';
let user = {name:'Tim'};
this.http.get(url,{params: user}).subscribe(data=>{
console.log(data);
});

后台服务:

app.get('/user/add', function(req, res){
res.json(req.query);
res.end();
});

10.8.2 POST 传参
常用的操作之一就是把数据 POST 到服务器,比如提交表单。下面这段发送POST 请求的代码和发送 GET 请求的非常像:

let url = 'http://localhost/user/update';
let user = {name:'Tim'};
this.http.post(url,user,
{params:new HttpParams().set('id','1')}).subscribe(data=>{
console.log(data);
});

添加 URL 参数的方法也一样,使用 HttpParams 定义传参。
后台:

var express = require('express');
var app = express();
var bodyParser = require('body-parser');
// 解析 json 参数
app.use(bodyParser.json());
// 创建 application/x-www-form-urlencoded 编码解析
app.use(bodyParser.urlencoded({ extended: true }));
//设置跨域访问
app.all('*', function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Content-Type,
Access-Control-Allow-Headers, Authorization, X-Requested-With");
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,
OPTIONS");
res.header("X-Powered-By",' 3.2.1')
res.header("Content-Type", "application/json;charset=utf-8");
next();
});
app.post('/user/update', function(req, res){
console.log(req.query);
console.log(req.body.name);
res.json(Object.assign(req.body,req.query));
res.end();
});
var server = app.listen(80, function(){
var host = server.address().address
var port = server.address().port
console.log("应用实例,访问地址为 http://%s:%s", host, port)
});

第 11 章 路由和导航

在用户使用应用程序时,Angular 的路由器能让用户从一个视图导航到另一个视图。浏览器具有我们熟悉的导航模式:

  • 在地址栏输入 URL,浏览器就会导航到相应的页面。
  • 在页面中点击链接,浏览器就会导航到一个新页面。
  • 点击浏览器的前进和后退按钮,浏览器就会在你的浏览历史中向前或向后导航。

Angular 的 Router(即“路由器”)借鉴了这个模型。

11.1 搭建路由

11.1.1 添加<base href>

在 index.html 中添加路由根标签。大多数带路由的应用都要在 index.html的<head>标签下先添加一个<base>元素,来告诉路由器该如何合成导航用的URL。
如果 app 文件夹是该应用的根目录,那就把 href 的值设置为下面这样:

<base href="/">

11.1.2 导入路由库

import { RouterModule, Routes } from ‘@angular/router’;

11.1.3 配置路由

const appRoutes: Routes = [
{ path: 'hello', component: HelloWorldComponent },
{ path: 'helloeveryone', component: HelloEveryoneComponent }
];
@NgModule({
...
imports: [
...
RouterModule.forRoot(appRoutes)
],
...
})
export class AppModule { }

路 由 数 组 appRoutes 描 述 如 何 进 行 导 航 。 把 它 传 给RouterModule.forRoot方法并传给本模块的imports数组就可以配置路由器。
每个 Route 都会把一个 URL 的 path 映射到一个组件。 注意,path 不能以斜杠(/)开头。 路由器会为解析和构建最终的 URL,这样当我们在应用的多个视图之间导航时,可以任意使用相对路径和绝对路径。
如果我们想要看到在导航的生命周期中发生过哪些事件,可以使用路由器默认配置中的 enableTracing 选项。它会把每个导航生命周期中的事件输出到浏览器的控制台。 这应该只用于调试。我们只需要把 enableTracing: true 选项作为第二个参数传给 RouterModule.forRoot()方法就可以了。

RouterModule.forRoot(
appRoutes,
{ enableTracing: true } // 调试
)

11.1.4 路由器链接

< h1>Angular Router</h1>
<nav>
<a routerLink="/hello" routerLinkActive="active">hello</a>
<a routerLink="/helloeveryone"
routerLinkActive="active">helloeveryone</a>
</nav>
<router-outlet></router-outlet>

a 标签上的 RouterLink 指令让路由器得以控制这个 a 元素。这里的导航路径是固定的,因此可以把一个字符串赋给 routerLink(“一次性”绑定)。
每个a标签上的RouterLinkActive指令可以帮用户在外观上区分出当前选中的“活动”路由。当与它关联的 RouterLink 被激活时,路由器会把 CSS 类active 添加到这个元素上。我们可以把该指令添加到 a 元素或它的父元素上。<router-outlet>标签配置路由的出口,组件将会被渲染在此处。
启动服务器:

ng serve

11.2 通配符路由

添加一个通配符路由来拦截所有无效的 URL,并优雅的处理它们。 通配符路由的 path 是两个星号(**),它会匹配任何 URL。 当路由器匹配不上以前定义的那些路由时,它就会选择这个路由。 通配符路由可以导航到自定义的“404Not Found”组件,也可以重定向到一个现有路由。

11.2.1 定义组件 not-found.component.ts

import { Component } from '@angular/core';
@Component({
template: '<h2>Page not found</h2>'
})
export class PageNotFoundComponent {
}

11.2.2 添加路由

import { PageNotFoundComponent } from './not-found.component';
...
{ path: '**', component: PageNotFoundComponent }

11.2.3 添加路由导航

<a routerLink="/angular" routerLinkActive="active">angular</a>

11.3 重定向路由

目前,项目启动 http://localhost:4200/匹配不到任何路径,会跳转到 404页面。那么需要设置一个默认路由,在通配符路由上方添加。比如:/hello{ path: ‘’, redirectTo: ‘/hello’, pathMatch: ‘full’ },重定向路由需要一个 pathMatch 属性,来告诉路由器如何用 URL 去匹配路由的路径,否则路由器就会报错。

11.4 参数路由

11.4.1 定义组件

import { Component, OnInit, DoCheck, SimpleChanges } from
'@angular/core';
import { Router, ActivatedRoute, ParamMap } from
'@angular/router';
import 'rxjs/add/operator/switchMap';
@Component({
selector: 'app-router-params',
template: `
<div>参数是:{{id}}</div>
`
})
export class RouterParamsComponent implements OnInit, DoCheck {
id;
constructor(
private route: ActivatedRoute,
private router: Router
) {}
ngOnInit() {
}
ngDoCheck(): void {
this.id = this.route.snapshot.paramMap.get('id');
}
}

11.4.2 配置路由

{ path: 'routerparams/:id', component: RouterParamsComponent },

11.4.3 配置路由链接

<a routerLink="/routerparams/12"
routerLinkActive="active">routerparams-12</a>
<a routerLink="/routerparams/13"
routerLinkActive="active">routerparams-13</a>

11.5 路由嵌套

11.5.1 配置子路由

const appRoutes: Routes = [
{
path: 'hello', component: HelloWorldComponent,
children: [
{ path: 'helloeveryone', component: HelloEveryoneComponent },
{ path: 'routerparams/:id', component: RouterParamsComponent }
]
},
{ path: '', redirectTo: '/hello', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent }
];

11.5.2 配置子路由链接

import { Component} from '@angular/core';
@Component({
selector: 'app-hello-world',
template: `
<h3>{{greetText}}</h3>
<p>
<a routerLink="helloeveryone"
routerLinkActive="active">helloeveryone</a>
<a routerLink="routerparams/12"
routerLinkActive="active">routerparams-12</a>
<a routerLink="routerparams/13"
routerLinkActive="active">routerparams-13</a>
</p>
<router-outlet></router-outlet>
`
})
export class HelloWorldComponent {
greetText = 'hello world!!';
}

11.5.3 修改 app.component.html

<h1>Angular Router</h1>
<nav>
<a routerLink="/hello" routerLinkActive="active">hello</a>
<a routerLink="/angular" routerLinkActive="active">angular</a>
</nav><router-outlet></router-outlet>

11.6 路由模块

对于简单的路由,这没有问题。 随着应用的成长,我们使用更多路由器特征,比如守卫、解析器和子路由等,我们很自然想要重构路由。 建议将路由信息移到一个单独的特殊用途的模块,叫做路由模块。
路由模块有一系列特性:

  • 把路由这个关注点从其它应用类关注点中分离出去
  • 测试特性模块时,可以替换或移除路由模块
  • 为路由服务提供商(包括守卫和解析器等)提供一个共同的地方
  • 不要声明组件

11.6.1 将路由配置重构为路由模块

在/app 目录下创建一个名叫 app-routing.module.ts 的文件,以包含这个路由模块。

导入定义的组件,就像app.module.ts中一样。然后把 Router 的导入语句和路由配置以及RouterModule.forRoot 移入这个路由模块中。
遵循规约,添加一个 AppRoutingModule 类并导出它,以便稍后在AppModule中导入它。

最 后 , 可 以 通 过 把 它 添 加 到 该 模 块 的 exports 数 组 中 来 再 次 导 出RouterModule。 通过在 AppModule 中导入 AppRoutingModule 并再次导出 RouterModule,那些声明在 AppModule 中的组件就可以访问路由指令了,比如 RouterLink 和 RouterOutlet。

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HelloWorldComponent } from './hello-world.component';
import { HelloEveryoneComponent } from
'./hello-everyone/hello-everyone.component';
import { PageNotFoundComponent } from './not-found.component';
import { RouterParamsComponent } from
'./router-params/router-params.component';
const appRoutes: Routes = [
{
path: 'hello', component: HelloWorldComponent,
children: [
{ path: 'helloeveryone', component: HelloEveryoneComponent },
{ path: 'routerparams/:id', component: RouterParamsComponent }
]
},
{ path: '', redirectTo: '/hello', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [
RouterModule.forRoot(
appRoutes,
{ enableTracing: true }
)
],
exports: [
RouterModule
]
})
export class AppRoutingModule {}

11.6.2 导入路由模块

import { AppRoutingModule } from './app-routing.module';
@NgModule({
imports: [
AppRoutingModule
],
})

应用继续正常运行,我们可以把路由模块作为为每个特性模块维护路由配置的中心地方。
路由模块在根模块或者特性模块替换了路由配置。在路由模块或者在模块内部配置路由,但不要同时在两处都配置。
路由模块是设计选择,它的价值在配置很复杂,并包含专门守卫和解析器服务时尤其明显。 在配置很简单时,它可能看起来很多余。
在配置很简单时,一些开发者跳过路由模块(例如 AppRoutingModule),并将路由配置直接混合在关联模块中(比如 AppModule )。
我们建议你选择其中一种模式,并坚持模式的一致性。
大多数开发者应该采用路由模块,以保持一致性。
它在配置复杂时,能确保代码干净。 它让测试特性模块更加容易。 它的存在让我们一眼就能看出这个模块是带路由的。 开发者可以很自然的从路由模块中查找和扩展路由配置。

angular5基础笔记相关推荐

  1. JavaScript基础笔记集合(转)

    JavaScript基础笔记集合 JavaScript基础笔记集合   js简介 js是脚本语言.浏览器是逐行的读取代码,而传统编程会在执行前进行编译   js存放的位置 html脚本必须放在< ...

  2. Python初学者零碎基础笔记(一)

    Python初学者零碎基础笔记 一行代码输入多个参数 方法1.) a,b,c=map(类型,input("请输入").split()) #默认空格分隔,若要转其他类型,把类型换成需 ...

  3. Jmeter使用基础笔记-写一个http请求

    前言 本篇文章主要讲述2个部分: 搭建一个简单的测试环境 用Jmeter发送一个简单的http请求 搭建测试环境 编写flask代码(我参考了开源项目HttpRunner的测试服务器),将如下的代码保 ...

  4. UWP入门(二) -- 基础笔记

    UWP入门(二) -- 基础笔记 原文:UWP入门(二) -- 基础笔记 不错的UWP入门视频,1092417123,欢迎交流 UWP-04 - What i XMAL? XAML - XML Syn ...

  5. [云炬创业基础笔记]第五章创业机会评估测试2

    [云炬创业基础笔记]第五章创业机会评估测试1

  6. [云炬创业基础笔记] 第四章测试17

    [云炬创业基础笔记] 第四章测试7

  7. [云炬创业基础笔记] 第四章测试15

    [云炬创业基础笔记] 第四章测试7

  8. [云炬创业基础笔记] 第四章测试8

    [云炬创业基础笔记] 第四章测试7

  9. [云炬创业基础笔记] 第四章测试5

    [云炬创业基础笔记] 第四章测试1

最新文章

  1. poj2528贴海报(线段树离散化)
  2. 你不能忽视的HTML语言3
  3. Array Elimination 运算,gcd,思维
  4. pandas(五) -- 文本处理
  5. NodeJs Express 4.x 入门
  6. 最大子序列和问题的解(共4种,层层推进)
  7. tomcat9-jenkins:insufficient free space available after evicting expired cache entries-consider
  8. 使用数据库镜像保障高可用的数据库应用(下)
  9. 浮点数 字符串 java_Java如何将浮点数转换为字符串
  10. 《Ruminations on C++/C++沉思录》学习笔记一————koening和Moo夫妇访谈
  11. R实战 Nomogram(诺莫图列线图)及其Calibration校准曲线绘制
  12. 三种百度网盘加速器,轻松突破10M/S,总有一款适合你!
  13. 国外开放的硕博论文、期刊、数据库下载网站
  14. Android设置状态栏的字体颜色
  15. thread-specific stroage模式 一个线程一个储物柜
  16. Scrapped or attached views may not be recycled. isScrap:false isAttached:true android.support.v7.wid
  17. rls算法matlab实现,第5章基于RLS算法的数据预测与MATLAB实现MATLAB实现.PDF
  18. 史上20大计算机病毒
  19. 汉诺塔系列问题: 汉诺塔II、汉诺塔III、汉诺塔IV、汉诺塔V、汉诺塔VI、汉诺塔VII
  20. 【笔记】LaTex安装及使用(五)Endnote批量查找、导入和导出参考文献

热门文章

  1. Visual Studio Community 2015:设置网站
  2. 加密相册-加密保护照片视频账号日记通讯录密码锁住隐私相片相册卫士图片管家隐藏小电影的保险箱柜
  3. C语言串口与网口转换,单片机通过串口与电脑连接通信C语言源代码
  4. OSChina 周三乱弹 —— 我不是长耳朵的猕猴桃
  5. 用ArcGIS制作专题地图
  6. matlab计算空间桁架,基于MATLAB的三维桁架有限元分析_宋志安.pdf
  7. STM32芯片读保护解锁
  8. android 蜂巢平台,Android 3.2来临 蜂巢系统平板详解析
  9. 商品库存周转率详解及计算方式
  10. 奥格斯堡水利系统被列入教科文组织世界遗产名录