快速入门
一、Angular 环境搭建
前提条件
请先确保开发环境中包括Node.js 和 npm包管理器。
Angular需要 Node.js 版本10.9.0 或以上。Node.js 已经默认安装了 npm 客户端。想检查版本请在终端/控制台运行 node -v
,npm -v
。
步骤
- 使用 npm命令全局安装 Angular CLI:
npm install -g @angular/cli
- 使用 CLI命令创建新的项目:
ng new 项目名称
选项 | 说明 |
---|---|
–skipInstall=true / false | 若为true,则不安装依赖包。默认:false。 |
–routing=true / false | 若为true,则为初始项目生成路由模块。 |
–skipTests=true / false | 若为true,则不生成“spec.ts”测试文件。默认:false。 |
–style= css/scss/sass/less/styl | 样式文件的文件扩展名或预处理器。 |
- 进入项目目录,使用 CLI命令运行应用:
ng serve
补充:
Yarn
由于本人的网络有时比较龟速,使用npm new 项目名称
或者npm install
时可能会出错,因此通常使用 yarn来下载并安装 npm包。不再碰到以前 ng new 创建新项目时出现的问题(此为文章链接)。
- 简介(中文文档)
yarn也是一款包管理工具,它会缓存每个下载过的包,再次使用时无需重复下载。同时利用并行下载以最大化资源利用率,因此安装速度更快。 - 安装
使用npm安装:npm install -g yarn
- 和npm命令的比较:
NPM | YARN |
---|---|
npm init | yarn init |
npm install | yarn |
npm install lodash --save | yarn add lodash |
npm install lodash --save-dev | yarn add lodash --dev |
npm install lodash --global | yarn global add lodash |
npm uninstall | yarn remove |
二、基本概念
模块简介
NgModule 是一个带有 @NgModule
装饰器的类。
Angular CLI 在创建新应用时会生成一个基本模块 AppModule,该根模块就是你用来启动此应用的模块。
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 { }
- declarations —— 该应用所拥有的组件。
- imports —— 导入 BrowserModule 以获取浏览器特有的服务。
- providers —— 各种服务提供者。
- bootstrap —— 根组件,Angular 创建它并插入 index.html 宿主页面。
declarations 数组
只能接受可声明对象,包括组件、指令和管道。
imports 数组
所需模块。
providers 数组
所需服务。当直接把服务列在这里时,它们是全应用范围的。 当使用特性模块和惰性加载时,它们是范围化的。
组件简介
一个带有 @Component()
装饰器的类,和它的伴生模板关联在一起。组件类及其模板共同定义了一个 视图。
组件是 指令 的一种特例。@Component() 装饰器扩展了 @Directive() 装饰器,增加了一些与模板有关的特性。
Angular 的组件类负责暴露数据,并通过 数据绑定机制 来处理绝大多数视图的显示和用户交互逻辑。
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.less']
})
export class AppComponent {
title = 'angular-demo';
}
服务与依赖注入
服务 是一个广义的概念,它包括应用所需的任何值、函数或特性。
组件应该把诸如从服务器获取数据、验证用户输入或直接往控制台中写日志等工作委托给各种服务。把组件和服务区分开,以提高模块性和复用性。
依赖注入(dependency injection),简称 DI,既是设计模式,同时又是一种机制:当应用程序的一些部件(即一些依赖)需要另一些部件时, 利用依赖注入来创建被请求的部件,并将它们注入到需要它们的部件中。
在 Angular 中,依赖通常是服务,但是也可以是值,比如字符串或函数。组件是服务的消费者,要把一个类定义为服务,就要用 @Injectable()
装饰器来提供元数据,以便让 Angular 可以把它作为 依赖 注入到组件中,让组件类得以访问该服务类。
@Injectable({
providedIn: 'root',
})
路由
用来配置和实现 Angular 应用中各个状态和视图之间的导航。要想实现这种导航,你可以使用 Angular 的Router(路由器)。路由器会把浏览器 URL 解释成改变视图的操作指南,以完成导航。
三、基础教程
使用路由
使用命令 ng new routing-app --routing
会生成一个带有路由模块(AppRoutingModule)的基本 Angular 应用。
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [];
@NgModule({
declarations: [],
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
默认跳转至 admin
router-outlet
指令用于告诉 Angular 在哪里加载组件,当路由匹配到响应路径,并成功找到需要加载的组件时,它将动态创建对应的组件,并将其作为兄弟元素,插入到 router-outlet 元素中。
import { Component } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-root',
template: `<router-outlet></router-outlet>`,
styleUrls: ['./app.component.less']
})
export class AppComponent {
constructor(private router: Router) {}
ngOnInit() {
this.router.navigateByUrl('/admin');
}
}
导入路由模块和所需模块
// ...
import { AppRoutingModule } from './app-routing.module';
import { LayoutModule } from './layout/layout.module';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule,LayoutModule,AppRoutingModule],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
配置路由信息
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LayoutComponent } from './layout.component';
const routes: Routes = [
{ path: 'admin', component: LayoutComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)]
})
export class LayoutRoutingModule { }
自定义组件
创建 LayoutComponent 组件
定义组件的元信息和定义组件类,如下:
@Component({
selector: 'app-hero',
templateUrl: './hero.component.html',
styleUrls: ['./hero.component.less']
})
export class HeroComponent {
constructor() { }
ngOnInit(): void {
}
}
定义 Hero 接口
interface Hero {
id: string;
name: string;
}
使用 Hero 接口
export class HeroComponent{
address: string;
hero: Hero;
// ...
}
在构造函数中执行数据初始化
@Component({...})
export class HeroComponent {
address: string;
hero: Hero;
constructor() {
this.address = '华山';
this.hero = {
id: '中神通',
name: '王重阳'
}
}
}
可以使用插值语法实现数据绑定。
绑定普通文本和绑定对象属性,如下:
<h2>大家好,欢迎来到 {{ address }}</h2>
<p>第一次论剑的最终胜利者是 {{hero.id}} {{hero.name}}!</p>
常用结构型指令
结构型指令的职责是 HTML 布局。通过添加和移除 DOM 元素改变 DOM 布局。
NgIf 指令
从模板中创建或销毁子视图。
<div *ngIf="hero" class="name">{{hero.name}}</div>
指令名的星号(*
)前缀是语法糖。从内部实现来说,Angular 把 *ngIf 属性翻译成一个 < ng-template > 元素,并用它来包裹宿主元素,如下:
<ng-template [ngIf]="hero">
<div class="name">{{hero.name}}</div>
</ng-template>
*ngIf 指令被移到了 元素上。在那里它变成了一个属性绑定 [ngIf]。
< div > 上其余部分,包括它的 class 属性在内,移到了内部的 < ng-template > 元素上。
NgFor 指令
用于基于可迭代对象中的每一项。
<li *ngFor="let hero of heroes">...</li>
使用示例
import { Component, OnInit } from '@angular/core';
import { HEROES, Hero } from '../mock-heroes';
@Component({...})
export class HeroComponent {
address: string;
showHeroes: boolean = true;
heroes: Hero[] = HEROES;
constructor() {
this.address = '华山';
}
}
<h2>大家好,欢迎来到 {{ address }}</h2>
<div *ngIf="showHeroes">
<h3>五绝名单</h3>
<ul>
<li *ngFor="let hero of heroes">
{{hero.name}}
</li>
</ul>
</div>
我们使用 let item of items;
语法迭代数组中的每一项,另外我们使用 index as i
访问数组中每一项的索引值。除此之外,我们还可以获取以下的值:
- first: boolean —— 若当前项是可迭代对象的第一项,则返回 true
- last: boolean —— 若当前项是可迭代对象的最后一项,则返回 true
- even: boolean —— 若当前项的索引值是偶数,则返回 true
- odd: boolean —— 若当前项的索引值是奇数,则返回 true
NgSwitch 指令
一组在备用视图之间切换的指令。它根据切换条件显示几个可能的元素中的一个。Angular 只会将选定的元素放入 DOM。
<div *ngFor="let hero of heroes" [ngSwitch]="hero?.name">
<div *ngSwitchCase="'东邪'">...</div>
<div *ngSwitchCase="'西毒'">...</div>
<div *ngSwitchCase="'南帝'">...</div>
<div *ngSwitchCase="'北丐'">...</div>
<div *ngSwitchDefault>...</div>
</div>
NgSwitch 实际上是三个协作指令的集合:NgSwitch、NgSwitchCase 和 NgSwitchDefault。
NgSwitch 本身不是结构型指令,而是一个属性型指令,它控制其它两个 switch 指令的行为。 因此写成 [ngSwitch]
而不是 *ngSwitch。
NgSwitchCase 和 NgSwitchDefault 都是结构型指令。 因此使用星号(*)前缀来把它们附着到元素上。 *ngSwitchCase
会在它的值匹配上选项值时显示它的宿主元素。 当 NgSwitchCase 没有匹配上时,会显示*ngSwitchDefault
的宿主元素。
事件绑定
通过 (eventName)
的语法,实现事件绑定。
使用示例
import { Component, OnInit } from '@angular/core';
import { HEROES, Hero } from '../mock-heroes';
@Component({...})
export class HeroComponent {
// ...
showSkill: boolean = false;
// ...
toggleSkill() {
this.showSkill = !this.showSkill;
}
}
<div *ngIf="showHeroes">
<h3>五绝名单</h3>
<button (click)="toggleSkill()">
{{ showSkill ? "隐藏技能" : "显示技能" }}
</button>
<ul>
<li *ngFor="let hero of heroes">
{{hero.name}}<span *ngIf="showSkill">:{{hero.skill}}</span>
</li>
</ul>
</div>
模板引用
可以使用 #variableName
的语法,定义模板引用。
<div>
<input #myInput type="text">
<button (click)="onClick(myInput.value)">点击</button>
</div>
模板表达式中的运算符
管道运算符( | )
管道运算符会把左侧的表达式结果传给右侧的管道函数,从而对表达式的结果进行一些转换。
例如:
<p>Item json pipe: {{item | json}}</p>
安全导航运算符( ? )
安全导航运算符 ?
可以对在属性路径中出现 null 和 undefined 值进行保护,防止视图渲染失败。
<p>The item name is: {{item?.name}}</p>
非空断言运算符(!)
如果无法在运行类型检查器期间确定变量是否 null 或 undefined,则会抛出错误。你可以通过应用后缀非空断言运算符 !
来告诉类型检查器不要抛出错误。
例如,在使用 *ngIf 检查过 item 是否已定义之后,就可以断言 item 属性也已定义。
<!-- Assert color is defined, even if according to the `Item` type it could be undefined. -->
<p>The item's color is: {{item.color!.toUpperCase()}}</p>
与 安全导航运算符 不同的是,非空断言运算符不会防止出现 null 或 undefined。 它只是告诉 TypeScript 的类型检查器对特定的属性表达式,不做 “严格空值检测”。
非空断言运算符 !,是可选的,但在打开严格空检查选项时必须使用它。
注入服务
组件中注入服务步骤:
- 创建服务
@Injectable({
providedIn: 'root'
})
export class HeroService {
- 导入已创建的服务
import { HeroService } from '../hero.service';
- 在构造函数里注入服务
constructor(private heroService: HeroService) {}
使用示例
import { Injectable } from '@angular/core';
import { Hero } from './hero';
@Injectable({
providedIn: 'root'
})
export class HeroService {
heroes: Hero[] = [
{ id: 0, name: '东邪', skills: ['弹指神通'] },
{ id: 1, name: '西毒', skills: ['蛤蟆功'] },
{ id: 2, name: '南帝', skills: ['一阳指'] },
{ id: 3, name: '北丐', skills: ['降龙十八掌'] },
{ id: 4, name: '中神通', skills: ['先天功'] }
];
}
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({...})
export class HeroComponent implements OnInit {
// ...
heroes: Hero[];
constructor(private heroService: HeroService) {
this.heroes = this.heroService.heroes;
}
}
指令概览
在 Angular 中有三种类型的指令:
- 组件:拥有模板的指令
- 结构型指令:修改视图的结构的指令。
- 属性型指令:改变元素、组件或其它指令的外观和行为的指令。
输入和输出属性
@Input()
和 @Output()
允许 Angular 在父子组件之间共享数据。
@Input()
允许将数据从父组件输入到子组件中。使用 [message]="message"
属性绑定的语法,实现数据传递。
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'app-hero-detail',
template: `
<p>{{hero.name}} 信息</p>
`,
styleUrls: ['./hero-detail.component.less']
})
export class HeroDetailComponent implements OnInit {
@Input() hero;
// ...
}
<div *ngFor="let hero of heroes">
<app-hero-detail [hero]="hero"></app-hero-detail>
</div>
@Output()
允许数据从子级流出到父级。通常将 @Output() 属性初始化为 Angular EventEmitter,并将值作为 事件 从组件中向外发送。
在子组件中使用 Output 装饰器
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-hero-handle',
template: `
<label>Add a skill: <input #newSkill></label>
<button (click)="addSkill(newSkill.value)">Add to parent's skill</button>
`,
styleUrls: ['./hero-handle.component.less']
})
export class HeroHandleComponent implements OnInit {
@Output() addNewSkill = new EventEmitter();
addSkill(value: string) {
this.addNewSkill.emit(value);
}
}
在父组件中使用 (eventName)
事件绑定的语法,监听我们自定义的事件。当在 HeroHandleComponent
组件中点击新增按钮,将会调用 AppComponent
组件类中的 onAddSkill()
方法,更新对应信息。
<ul>
<li *ngFor="let hero of heroes">
{{hero.name}}: <span *ngFor="let skill of hero.skills">{{skill}} </span>
<br/>
<app-hero-handle (addNewSkill)="onAddSkill(hero.id, $event)"></app-hero-handle>
</li>
</ul>
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({...})
export class LayoutComponent implements OnInit {
heroes: Hero[];
constructor(private heroService: HeroService) {
this.heroes = this.heroService.heroes;
}
onAddSkill(id, newSkill) {
if(!newSkill) return
this.heroes.forEach(ele => {
if(ele.id === id) ele.skills.push(newSkill);
})
}
}
使用双向绑定
双向绑定提供了一种在组件类及其模板之间共享数据的方式。
通过 [(...)]
,来实现双向绑定。
拿官网中改变文本大小的 SizerComponent 组件来举例说明。
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-sizer',
templateUrl: './sizer.component.html',
styleUrls: ['./sizer.component.less']
})
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);
}
}
<div>
<button (click)="dec()">-</button>
<button (click)="inc()">+</button>
<label [style.font-size.px]="size">FontSize: {{size}}px</label>
</div>
修改 HeroComponent
<h2>大家好,欢迎来到 {{ address }}</h2>
<div *ngIf="showHeroes" [style.font-size.px]="fontSizePx">
<h3>五绝名单</h3>
<button (click)="toggleSkill()">
{{ showSkill ? "隐藏技能" : "显示技能" }}
</button>
<ul>
<li *ngFor="let hero of heroes">
{{hero.name}}<span *ngIf="showSkill">:{{hero.skill}}</span>
</li>
</ul>
</div>
<app-sizer [(size)]="fontSizePx"></app-sizer>
在 hero.component.ts 中添加
fontSizePx = 16;
单击按钮就会通过双向绑定更新 AppComponent.fontSizePx。
双向绑定做了两件事:设置特定的元素属性和监听元素的变更事件。实际上就是 属性绑定 和 事件绑定 的语法糖。 Angular 将 SizerComponent 的绑定分解成这样:
<app-sizer [size]="fontSizePx" (sizeChange)="fontSizePx=$event"></app-sizer>
[(ngModel)] : 双向绑定
记住,要导入 FormsModule 才能让 [(ngModel)]
可用。
<input [(ngModel)]="currentItem.name" id="example-ngModel">
为了简化语法,ngModel 指令把技术细节隐藏在其输入属性 ngModel 和输出属性 ngModelChange 的后面:
<input [ngModel]="currentItem.name" (ngModelChange)="currentItem.name=$event" id="example-change">
组件样式
设置组件元数据时通过 styles 或 styleUrls 属性,来设置组件的内联样式和外联样式。
NgClass
用 ngClass 同时添加或删除几个 CSS 类。
<div [ngClass]="isSpecial ? 'special' : ''">This div is special</div>
要添加或删除单个类,请使用类绑定而不是 NgClass。
NgStyle
通过 Angular 表达式,设置 DOM 元素的 CSS 属性。
用法:
<div [ngStyle]="{color: 'white', 'background-color': 'blue'}">...</div>
attribute、class 和 style 绑定
attribute
通常, 使用 Property 绑定设置元素的 Property 优于使用字符串设置 Attribute。但是,当没有要绑定的元素的 Property 时,可以采用 Attribute 绑定。
<button [attr.aria-label]="actionName">...</button>
类绑定
使用类绑定来为一个元素添加和移除 CSS 类。
绑定类型 | 语法 | 输入类型 | 输入值范例 |
---|---|---|---|
单个类绑定 | [class.foo]=“hasFoo” | boolean / undefined / null | true, false |
多个类绑定 | [class]=“classExpr” | string | “my-class-1 my-class-2” |
: | {[key: string]: boolean / undefined / null} | {foo: true, bar: false} | |
: | Array< string > | [‘foo’, ‘bar’] |
需要同时管理多个类名时,请考虑使用 NgClass 指令。
样式绑定
通过样式绑定来动态设置样式。
绑定类型 | 语法 | 输入类型 | 输入值范例 |
---|---|---|---|
单一样式绑定 | [style.width]=“width” | boolean / undefined / null | “100px” |
带单位的单一样式绑定 | [style.width.px]=“width” | number / undefined / null | 100 |
多个样式绑定 | [style]=“styleExpr” | string | “width: 100px; height: 100px” |
: | {[key: string]: string / undefined / null} | {width: ‘100px’, height: ‘100px’} | |
: | Array< string > | [‘width’, ‘100px’] |
NgStyle 指令可以作为 [style] 绑定的替代指令。但是,应该把 [style] 样式绑定语法作为首选,因为随着 Angular 中样式绑定的改进,NgStyle 将不再提供重要的价值,并最终在未来的某个版本中删除。
服务篇
依赖注入的概念
控制反转(IoC)是一种设计思想。传统的程序设计,我们直接在对象(客户端)内部通过 new 创建对象,是客户端主动创建依赖对象。而 IoC 意味着将你设计好的对象交给容器控制,即由 IoC容器 来控制对象的创建。传统应用程序是由对象主动去创建和获取依赖对象,当由容器来创建和注入依赖对象,对象只是被动地接受依赖对象,因此,依赖对象的获取被反转了。
IoC 很好的体现了面向对象设计法则之一—— 好莱坞法则:“别打给我们,我们会打给你 (don’t call us, we’ll call you)”;
在软件工程中,依赖注入是对 IoC 的一种实现。一般情况下,如果服务A需要服务B,那就意味着服务A要在内部创建服务B的实例,也就是说服务A依赖于服务B。Angular 利用依赖注入机制改变了这一点,在该机制下,如果服务A依赖于服务B,那么我们希望服务B能被自动注入到服务A中。
依赖注入的应用
当 Angular 创建组件类的新实例时,它会查看该组件类的构造函数,来决定该组件依赖哪些服务或其它依赖项。比如 HeroComponent 需要 HeroService:
constructor(private service: HeroService) { }
当 Angular 发现某个组件依赖某个服务时,它会首先检查是否该注入器中已经有了那个服务的任何现有实例。如果所请求的服务尚不存在,注入器就会使用以前注册的服务提供者来制作一个,并把它加入注入器中,然后把该服务返回给 Angular。
当所有请求的服务已解析并返回时,Angular 可以用这些服务实例为参数,调用该组件的构造函数。
HeroService 的注入过程如下所示:
提供服务
对于要用到的任何服务,必须至少注册一个 提供者。你可以在三种位置之一设置元数据,以便在应用的不同层级使用提供者来配置注入器:
- 在服务本身的 @Injectable() 装饰器中:默认情况下,
ng generate service
命令会在 @Injectable() 装饰器里一个名叫 providedIn 的元数据选项中把提供者注册到根注入器中。在根一级提供服务时,会创建一个单一的共享实例,并且可以把它注入到任何想要它的类中。
这种在 @Injectable 元数据中注册提供者的方式还让 Angular 能够通过移除那些从未被用过的服务来优化大小。
@Injectable({
providedIn: 'root',
})
- 在 NgModule 的 @NgModule() 装饰器中:请用 @NgModule() 装饰器中的 providers 属性。当使用这种方式注册提供者时,该服务的同一个实例将会对该 NgModule 中的所有组件可用。
@NgModule({
providers: [
HeroService
],
...
})
- 在组件的 @Component() 装饰器中:即在 @Component() 元数据的 providers 属性中注册服务提供者。当使用这种方式注册提供者时,会为该组件的每一个新实例提供该服务的一个新实例。
@Component({
selector: 'app-hero-list',
templateUrl: './hero-list.component.html',
providers: [ HeroService ]
})
DI 令牌
当使用提供者配置注入器时,就会把提供者和一个 DI 令牌关联起来。注入器维护一个内部令牌-提供者的映射表,当请求一个依赖项时就会引用它。令牌就是这个映射表的键。
通过把 HeroService 类型作为令牌,可以直接从注入器中获得一个 HeroService 实例。
heroService: HeroService;
当使用 HeroService 类的类型来定义构造函数参数时,Angular 就会知道要注入与 HeroService 类这个令牌相关的服务。
constructor(heroService: HeroService)
依赖提供者
类提供者
类提供者的语法实际上是一种简写形式,它会扩展成一个由 Provider 接口定义的提供者配置对象。如下:
providers: [Logger]
[{ provide: Logger, useClass: Logger }]
扩展的提供者配置是一个具有两个属性的对象字面量。
- provide 属性存有令牌,它作为一个 key,在定位依赖值和配置注入器时使用。
- 第二个属性是一个提供者定义对象,它告诉注入器要如何创建依赖值。 提供者定义对象中的 key 可以是 useClass —— 如上例子同。 也可以是 useExisting、useValue 或 useFactory。 每一个 key 都用于提供一种不同类型的依赖。
值提供者
并非所有的依赖都是类。 也可以注入字符串、函数或对象。
@NgModule({
declarations: [
AppComponent,
],
providers: [
{ provide: 'api', useValue: '/api/pizzas' }
]
})
export class AppModule {}
工厂提供者
有时候你需要动态创建依赖值,创建时需要的信息你要等运行期间才能拿到。
假设你不希望直接把 UserService 注入到 HeroService 中,因为你不希望把这个服务与那些高度敏感的信息牵扯到一起。 这样 HeroService 就无法直接访问到用户信息,来决定谁有权访问,谁没有。
要解决这个问题,我们给 HeroService 的构造函数一个逻辑型标志,以控制是否显示秘密英雄。
HeroService:
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);
}
你可以注入 Logger 但是不能注入 isAuthorized 标志。不过你可以改用工厂提供者来为 HeroService 创建一个新的 logger 实例。工厂提供者需要一个工厂函数:
let heroServiceFactory = (logger: Logger, userService: UserService) => {
return new HeroService(logger, userService.user.isAuthorized);
};
虽然 HeroService 不能访问 UserService,但是工厂函数可以。 你把 Logger 和 UserService 注入到了工厂提供者中,并让注入器把它们传给这个工厂函数。
providers: [
{
provide: HeroService,
useFactory: heroServiceFactory,
deps: [Logger, UserService]
}
]
- useFactory 字段告诉 Angular 该提供者是一个工厂函数,该函数的实现代码是 heroServiceFactory。
- deps 属性是一个提供者令牌数组。注入器解析这些令牌,并把与之对应的服务注入到相应的工厂函数参数表中。
组件篇
生命周期钩子
一种接口,使用它来监听指令和组件的生命周期,比如创建、更新和销毁等。
每个接口只有一个钩子方法,方法名是接口名加前缀 ng。不必实现所有生命周期钩子,只要实现你所需要的就可以了。Angular 会按以下顺序调用钩子方法。
钩子方法 | 用途 | 时机 |
---|---|---|
ngOnChanges() | 当数据绑定输入属性的值发生变化时响应 | 在 ngOnInit() 之前以及所绑定的一个或多个输入属性的值发生变化时都会调用。 |
ngOnInit() | 初始化指令/组件 | 在第一轮 ngOnChanges() 完成之后调用,只调用一次。 |
ngDoCheck() | 用于检测和处理值的改变 | 每次执行变更检测时的 ngOnChanges() 和 首次执行变更检测时的 ngOnInit() 后调用。 |
ngAfterContentInit() | 把外部内容投影进组件视图或指令所在的视图之后调用 | 第一次 ngDoCheck() 之后调用,只调用一次。 |
ngAfterContentChecked() | 每检查完被投影到组件或指令中的内容之后调用 | ngAfterContentInit() 和每次 ngDoCheck() 之后调用。 |
ngAfterViewInit() | 初始化完组件视图及其子视图或包含该指令的视图之后调用 | 第一次 ngAfterContentChecked() 之后调用,只调用一次。 |
ngAfterViewChecked() | 每做完组件视图和子视图或包含该指令的视图的变更检测之后调用 | ngAfterViewInit() 和每次 ngAfterContentChecked() 之后调用。 |
ngOnDestroy() | 指令销毁前调用 | 销毁指令之前立即调用。 |
ngOnChanges
当数据绑定输入属性的值发生变化时,Angular 就会调用 ngOnChanges() 方法。它会获得一个 SimpleChanges
对象,该对象包含绑定属性的新值和旧值等,它主要用于监测组件输入属性的变化。
import { Component, OnInit} from '@angular/core';
@Component({
selector: 'on-changes-parent',
template: `
<div>
<h2>OnChanges</h2>
<p>姓名(hero.name):<input type="text" [(ngModel)]="hero.name"></p>
<p>技能(skills):<input type="text" [(ngModel)]="skills"></p>
</div>
<on-changes [hero]="hero" [skills]="skills"></on-changes>
`,
})
export class OnChangesParentComponent{
hero: { name: string } = { name: 'Windstorm' };
skills: string = 'sing,fly';
}
OnChangesComponent 组件有两个输入属性:hero 和 skills。
import { Component, OnChanges, SimpleChanges, Input } from '@angular/core';
@Component({
selector: 'on-changes',
templateUrl: './on-changes.component.html',
styleUrls: ['./on-changes.component.less']
})
export class OnChangesComponent implements OnChanges {
@Input() hero: { name: string };
@Input() skills: string;
changeLog: any[] = [];
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}`);
this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
}
}
}
请注意:ngChanges() 不会捕获对 hero.name 的更改。这是因为只有当输入属性的值发生变化时,才会调用该钩子。在这里, hero 属性的值是对 hero 对象的引用。
动态创建组件
使用 ComponentFactoryResolver
来动态添加组件。
定义指令
定义一个名叫 AdDirective 的指令来在模板中标记插入点。
import { Directive, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[ad-host]'
})
export class AdDirective {
constructor(public viewContainerRef: ViewContainerRef) { }
}
AdDirective 注入了 ViewContainerRef 来获取对容器视图的访问,这个容器就是动态加入的组件的宿主。
创建指令容器
放置指令/组件的地方称为 容器。创建一个模板元素< ng-template>作为我们的指令容器,把选择器 ad-host
应用到该容器上,大部分代码都在 ad-banner.component.ts 中。
动态创建组件
AdItem 对象指定要加载的组件类,以及绑定到该组件上的任意数据。ads 为要动态加载的组件类数组。通过点击按钮,调用 loadComponent() 来加载新组件。
- 注入
ComponentFactoryResolver
服务对象,该服务对象提供一个resolveComponentFactory()
方法,该方法接收一个组件类作为参数,并返回ComponentFactory
实例。 - 接下来,把 viewContainerRef 指向这个组件的现有实例。怎样找到这个实例呢?它指向 AdHost,而 adHost 是我们设置的把动态组件插入什么位置的指令。
- 调用 ViewContainerRef 实例 的
createComponent()
来创建对应组件,并将组件添加到容器中。 - 每次创建组件时,需要删除之前的视图,否则组件容器会出现多个视图(若允许多个组件的话。则不需要执行清除操作)。
- createComponent() 方法返回一个引用,指向这个刚刚加载的组件。使用这个引用就可以与该组件进行交互,比如设置它的属性或调用它的方法。
import { Component, ComponentFactoryResolver, Input, ViewChild, ViewContainerRef } from '@angular/core';
import { AdItem } from '../ad-item';
import { AdDirective } from '../directives/ad.directive';
@Component({
selector: 'ad-banner',
template: `
<div>
<h3>Advertisements</h3>
<ng-template ad-host></ng-template>
<button (click)="loadComponent()">Load Component</button>
</div>
`
})
export class AdBannerComponent{
@Input() ads: AdItem[];
currentAdIndex = -1;
@ViewChild(AdDirective, {static: true}) adHost: AdDirective;
constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
loadComponent() {
this.currentAdIndex = (this.currentAdIndex + 1) % this.ads.length;
const adItem = this.ads[this.currentAdIndex];
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(adItem.component);
const viewContainerRef = this.adHost.viewContainerRef;
viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(componentFactory);
componentRef.instance.data = adItem.data;
}
}
< ng-template > 元素是动态加载组件的最佳选择,因为它不会渲染任何额外的输出。
import { Type } from '@angular/core';
export class AdItem {
constructor(public component: Type<any>, public data: any) {}
}
最后需要把动态组件添加到 NgModule 的 entryComponents 数组中:
entryComponents: [HeroProfileComponent, HeroJobAdComponent]
其余代码
import { Component, OnInit } from '@angular/core';
import { AdService } from './ad.service';
import { AdItem } from './ad-item';
@Component({
selector: 'app-root',
template: `
<div>
<ad-banner [ads]="ads"></ad-banner>
</div>
`
})
export class AppComponent implements OnInit {
ads: AdItem[];
constructor(private adService: AdService) {}
ngOnInit() {
this.ads = this.adService.getAds();
}
}
import { Injectable } from '@angular/core';
import { AdItem } from '../ad-item';
import { HeroProfileComponent } from '../hero-profile/hero-profile.component';
import { HeroJobAdComponent } from '../hero-job-ad/hero-job-ad.component';
@Injectable({
providedIn: 'root'
})
export class AdService {
getAds() {
return [
new AdItem(HeroProfileComponent, {name: 'Bombasto', bio: 'Brave as they come'}),
new AdItem(HeroProfileComponent, {name: 'Dr IQ', bio: 'Smart as they come'}),
new AdItem(HeroJobAdComponent, {headline: 'Openings in all departments', body: 'Apply today'})
]
}
}
import { Component, Input } from '@angular/core';
import { AdComponent } from '../ad.component';
@Component({
template: `
<div>
<h3>Featured Hero Profile</h3>
<h4>{{data.name}}</h4>
<p>{{data.bio}}</p>
</div>
`
})
export class HeroProfileComponent implements AdComponent {
@Input() data: any;
}
import { Component, Input } from '@angular/core';
import { AdComponent } from '../ad.component';
@Component({
template: `
<div>
<h4>{{data.headline}}</h4>
{{data.body}}
</div>
`
})
export class HeroJobAdComponent implements AdComponent {
@Input() data: any;
}
export interface AdComponent {
data: any;
}
组件样式
视图封装
import { Component } from '@angular/core';
@Component({
selector: 'app-hero',
template: `
<h1>The hero group</h1>
<hero-main></hero-main>
`,
styles: ['h1 { font-weight: normal; }']
})
export class HeroComponent { }
从页面生成的 HTML 结构,我们发现了 _nghost-aqp-c50
、_ngcontent-aqp-c50
等属性。
- 当应用程序启动的时候,宿主元素将会拥有一个唯一的属性,该属性的值取决于组件的处理顺序,比如
_nghost-c0
,_nghost-c1
。 - 每个组件内的元素,将会应用唯一的属性,比如
_ngcontent-c0
,_ngcontent-c1
。
这些属性是如何进行视图封装的呢?接下来介绍特殊的选择器。
:host
当我们只想为宿主元素设置样式,而不影响到宿主元素下的其它元素时,可以使用 : host
。
:host 是把宿主元素作为目标的唯一方式。因为宿主不是组件自身模板的一部分,而是父组件模板的一部分,除此之外,没有办法指定它。
import { Component, Input } from '@angular/core';
@Component({
selector: 'hero-details',
template: `
<hero-team [heroes]="heroes"></hero-team>
<ng-content></ng-content>
`,
styleUrls: ['./hero-details.component.less']
})
export class HeroDetailsComponent {
@Input() heroes: any;
}
//hero-details.component.less
:host {
display: inline-block;
border: 1px solid black;
width: 200px;
}
:host 可以结合其它选择器,比如:
//hero-details.component.less
:host h3 {
color: red;
}
把宿主样式作为条件,就要像 函数 一样把条件选择器放在 :host 后面的括号中。
//hero-details.component.less
:host(.active) {
border-width: 3px;
}
:: ng-deep
把 伪类 ::ng-deep
应用到任何一条 css 规则上就会禁止对那条规则的视图包装。任何带有 ::ng-deep 的样式都会变成全局样式。
为了把指定的样式限定在当前组件及其下级组件,请在 ::ng-deep 之前带上 :host 选择器,避免该样式污染其它组件。
//hero-details.component.less
:host ::ng-deep h3 {
font-style: italic;
}
::ng-deep 还有两个别名: /deep/ 和 >>> 。
:host-context
对于开发主题样式很有用。:host-context()
在当前组件宿主元素的祖先节点中查找 CSS 类,直到文档的根节点为止。
在下面的例子中,只有当某个祖先元素有 css类 red-theme(/blue-theme) 时,才会把 相应 border-color 和 background 样式应用到组件内部的所有相应元素中。
import { Component } from '@angular/core';
@Component({
selector: 'hero-main',
template: `
<h2>hero-main</h2>
<div class="red-theme">
<hero-details [heroes]="group1" [class.active]="group1.active">
<hero-controls [heroes]="group1"></hero-controls>
</hero-details>
</div>
<div class="blue-theme">
<hero-details [heroes]="group2" [class.active]="group2.active">
<hero-controls [heroes]="group2"></hero-controls>
</hero-details>
</div>
`,
styles: ['div { display: inline-block;}']
})
export class HeroMainComponent {
//...
}
//hero-details.component.less
:host-context(.red-theme) {
border-color: red;
}
:host-context(.blue-theme) {
border-color: blue;
}
再来看看 HeroControlsComponent:
import { Component, Input } from '@angular/core';
@Component({
selector: 'hero-controls',
template: `
<style>
.btn {
color: white;
border: 1px solid #777;
}
:host-context(.red-theme) .btn-theme {
background: red;
}
:host-context(.blue-theme) .btn-theme {
background: blue;
}
</style>
<h3>Controls</h3>
<button class="btn btn-theme" (click)="activate()">Activate</button>
`
})
export class HeroControlsComponent {
@Input() heroes;
activate() {
this.heroes.active = !this.heroes.active;
}
}
把样式加载进组件中
以上例子中,我们使用了不同的方式将样式加载进组件中。现在我们说说常用的把样式加入组件的方式:
设置 styles 或 styleUrls 元数据
styles: ['h1 { font-weight: normal; }']
styleUrls: ['./hero-details.component.less']
注意:这些样式只对当前组件有效,它们既不会作用于嵌入的任何组件,也不会作用于投影进来的组件(如 ng-content)。
模板内联样式
CSS @imports 语法
//hero-details.component.less
@import './hero-details-box.less';
外部以及全局样式文件
当使用 CLI 进行构建时,必须配置 angular.json 文件,使其包含所有外部资源(包括外部的样式表文件)。
在它的 styles 区注册这些全局样式文件,默认情况下,它会有一个预先配置的全局 styles.css 文件。
自定义属性型指令实践
属性型指令用于改变元素的外观或行为。
创建一个简单的属性型指令,当鼠标悬停在一个元素上时,改变它的背景色:
ng g directive highlight
CLI 会创建 highlight.directive.ts 及相应测试文件(highlight.directive.spec.ts),使用 --skipTests=true 将不会创建测试文件,并且在模块中声明这个指令类。
import { Directive, ElementRef } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
constructor(el: ElementRef) {
console.log(this.el);
}
}
<p appHighlight>Highlight me!</p>
HostListener
属性装饰器,一般用来为宿主元素添加事件监听。此外,也可以监听 window 或 document 对象上的事件。
现在我们来监听宿主元素的鼠标进入和离开,并设置它的背景色:
//highlight.directive.ts
@HostListener('mouseenter')
onMouseEnter() {
this.highlight('yellow');
}
@HostListener('mouseleave')
onMouseLeave() {
this.highlight(null);
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
向指令传递值
使用 @Input 数据绑定向指令传递值。
可以指定要用哪种颜色高亮:
<p [appHighlight]="'orange'">Highlight me!</p>
[appHighlight] 属性同时做了两件事:把高亮指应用到了元素上,并且通过属性绑定设置了该指令的高亮颜色。这是清爽、简约的语法。
在指令内部,使用 @Input 别名来反映该属性的意图,保持可读性:
@Input('appHighlight') highlightColor: string;
@HostListener('mouseenter')
onMouseEnter() {
//如果未指定高亮颜色,就用红色高亮
this.highlight(this.highlightColor || 'red');
}
通常真实的应用会有多个可定制属性。
目前,默认颜色被硬编码为红色,我们来允许设置默认颜色:
<p [appHighlight]="color" defaultColor="violet">Highlight me!</p>
@Input() defaultColor: string;
@HostListener('mouseenter')
onMouseEnter() {
this.highlight(this.highlightColor || this.defaultColor || 'red');
}
HostBinding
属性装饰器,用来动态设置宿主元素的属性值。
我们来为 HighlightDirective 新增一个 border 属性:
import { Directive, ElementRef, HostListener, Input, HostBinding } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
@Input('appHighlight') highlightColor: string;
@Input() defaultColor: string;
@HostBinding('style.border')
border: string;
@HostListener('mouseenter')
onMouseEnter() {
this.highlight(this.highlightColor || this.defaultColor || 'red');
this.border = '2px solid grey';
}
@HostListener('mouseleave')
onMouseLeave() {
this.highlight(null);
this.border = null;
}
//...
}
自定义结构型指令实践
结构型指令用于塑造或重塑 DOM 的结构。比如添加、移除或维护这些元素。
创建一个名为 UnlessDirective 的结构型指令,它的功能与 NgIf 相反,即在条件为 false 时显示模板内容:
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({ selector: '[appUnless]'})
export class UnlessDirective {
}
1、指令的属性名应该采用小驼峰形式,带有一个前缀,但不能用 ng,因为它只属于 Angular 本身。请选择一些简短的,适合的前缀。
2、使用 TemplateRef 取得 < ng-template>的内容,并通过 ViewContainerRef 来访问这个视图容器。
由于我们会把一个 true/false 条件绑定到 [appUnless] 属性上,因此该指令需要一个带有 @Input 的 appUnless 属性。
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({ selector: '[appUnless]'})
export class UnlessDirective {
private hasView = false;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) { }
@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 就会设置 appUnless 属性。因为不能用 appUnless 属性,所以为它定义一个设置器(setter)。
由于不会读取 appUnless 属性,因此它不需要定义 getter。
试用该指令:
<p *appUnless="condition" class="unless a">
(A) This paragraph is displayed because the condition is false.
</p>
<p *appUnless="!condition" class="unless b">
(B) Although the condition is true,
this paragraph is displayed because appUnless is set to false.
</p>
自定义管道实践
管道用来对输入的数据进行转换和格式化,如大小写转换、数值和日期格式化等。
内建管道及分类
String -> String | – |
---|---|
UpperCasePipe | 把文本全部转换成大写 |
LowerCasePipe | 把文本全部转换成小写 |
TitleCasePipe | 把文本转换成标题形式 |
Number -> String | – |
---|---|
DecimalPipe | 把数字转换成带小数点字符串, 根据本地环境中的规则进行格式化。 |
PercentPipe | 把数字转换成百分比字符串, 根据本地环境中的规则进行格式化。 |
CurrencyPipe | 把数字转换成金额字符串 |
Object -> String | – |
---|---|
JsonPipe | 把一个值转换成 JSON 字符串格式 |
DatePipe | 根据区域设置规则格式化日期值 |
Tools | – |
---|---|
KeyValuePipe(v6.1.0) | 将对象或映射转换为键值对数组 |
SlicePipe | 从一个 Array 或 String 中创建其元素一个新子集 |
AsyncPipe | 从一个异步回执中解出一个值 |
I18nPluralPipe | 将值映射到字符串,该字符串根据地区规则对值进行多元化处理。 |
I18nSelectPipe | 显示与当前值匹配的字符串的通用选择器 |
在模板中使用管道:
<p>{{ 'Angular' | uppercase }}</p><!-- Output: ANGULAR -->
<p>{{ 'Angular' | lowercase }}</p><!-- Output: angular -->
<p>{{ { name: 'semlinker' } | json }}</p><!-- Output: { "name": "semlinker" } -->
使用参数和管道链来格式化数据:
<p>{{ 3.14159265 | number: '1.4-4' }}</p><!-- Output: 3.1416 -->
<p>{{ today | date: 'shortTime' }}</p><!-- Output: 2:13 PM -->
<p>{{ 'semlinker' | slice:0:3 }}</p><!-- Output: sem -->
<p>{{ 'semlinker' | slice:0:3 | uppercase }}</p><!-- Output: SEM -->
创建一个指数级转换管道
ng g p exponential-strength
实现 PipeTransform
接口中定义的 transform 方法
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'exponentialStrength'
})
export class ExponentialStrengthPipe implements PipeTransform {
transform(value: number, exponent?: number): number {
return Math.pow(value, isNaN(exponent) ? 1 : exponent);
}
}
<p>Super power boost: {{2 | exponentialStrength: 10}}</p>
管道分类
- pure管道:仅当输入值变化的时候,才执行转换操作,为默认类型。(输入值变化是指原始数据类型如:string、number、boolean 等的数值或对象的引用值发生变化)
- impure 管道:在每次变化检测期间都会执行,如鼠标点击或移动都会执行 impure 管道。
Element篇
为了支持跨平台,Angular 通过抽象层封装了不同平台的差异,统一了 API 接口。如定义了抽象类 Renderer 、抽象类 RootRenderer 等。此外还定义了以下引用类型:ElementRef、TemplateRef、ViewRef 、ComponentRef 和 ViewContainerRef 等。
ElementRef 简介
通过 ElementRef 可以封装视图层中的 native 元素(在浏览器中, native 元素通常指 DOM 元素)。
应用
需求:在页面渲染成功以后,获取页面的 div 元素,并改变该元素的背景色。
import { Component, ElementRef, AfterViewInit } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<div #greet>Hello {{name}}</div>
`,
})
export class AppComponent {
name:string = 'Angular';
@ViewChild('greet') greetDiv: ElementRef;
constructor() { }
ngAfterViewInit() {
this.greetDiv.nativeElement.style.backgroundColor = 'red';
}
}
以上设置 div 元素背景的代码,我们是默认应用的运行环境在浏览器中。在应用层直接操作 DOM,会造成应用层与渲染层之间强耦合,导致应用无法运行在不同环境。
当需要直接访问 DOM时,请把本 API 作为最后选择。直接操作 DOM,会造成应用与渲染层之间强耦合。优先使用 Angular 提供的模板和数据绑定机制。或者使用 Renderer2。
import { Component, ElementRef, AfterViewInit } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<div #greet>Hello {{name}}</div>
`,
})
export class AppComponent {
// ...
constructor(private renderer: Renderer2) { }
ngAfterViewInit() {
//this.greetDiv.nativeElement.style.backgroundColor = 'red';
this.renderer.setStyle(this.greetDiv.nativeElement, 'backgroundColor', 'red');
}
}
最后,我们通过Renderer2 实例提供的 API 优雅地设置了 div 元素的背景颜色。
Renderer2 API 有哪些常用的方法?
export abstract class Renderer2 {
abstract createElement(name: string, namespace?: string | null): any; //创建元素
abstract createComment(value: string): any; //创建注释元素
abstract createText(value: string): any; //创建文本元素
abstract setAttribute(el: any, name: string, value: string,
namespace?: string | null): void; //设置属性
abstract removeAttribute(el: any, name: string, namespace?: string|null): void; //移除属性
abstract addClass(el: any, name: string): void; //添加样式类
abstract removeClass(el: any, name: string): void; //移除样式类
abstract setStyle(el: any, style: string, value: any,
flags?: RendererStyleFlags2): void; //设置样式
abstract removeStyle(el: any, style: string, flags?: RendererStyleFlags2): void; //移除样式
abstract setProperty(el: any, name: string, value: any): void; //设置 DOM 对象属性,不同于元素属性
abstract setValue(node: any, value: string): void; //设置元素值
abstract listen(
target: 'window'|'document'|'body'|any, eventName: string,
callback: (event: any) => boolean | void): () => void; //注册事件
}
TemplateRef 简介
< template>
模板元素是一种机制,允许其包含内容在加载页面时不渲染,可将模板视为存储在页面上稍后使用的内容。TemplateRef,表示可用于实例化内嵌视图的内嵌模板。
应用
利用 TemplateRef 实例,可以灵活地创建内嵌视图。
@Component({
selector: "hello-world",
template: `
<ng-template #tpl>
<span>I am span in template</span>
</ng-template>
`
})
export class HelloWorldComponent implements AfterViewInit {
@ViewChild("tpl") tplRef: TemplateRef<HTMLElement>;
ngAfterViewInit(){
// 模板中的<ng-template>元素会被编译为<!---->元素
let commentElement = this.tplRef.elementRef.nativeElement;
// 创建内嵌视图
let embeddedView = this.tplRef.createEmbeddedView(null);
// 动态添加子节点
embeddedView.rootNodes.forEach(node => {
commentElement.parentNode.insertBefore(node, commentElement.nextSibling);
});
}
}
ViewContainerRef 简介
有没有发现上例显示出模板元素中的内容整个流程太复杂了。接下来,我们说说 ViewContainerRef。
ViewContainerRef 表示一个视图容器,可添加一个或多个视图。通过 ViewContainer
Ref 实例,可基于 TemplateRef 实例创建内嵌视图,并指定内嵌视图的插入位置。简而言之,ViewContainerRef 的主要作用是创建和管理内嵌视图或组件视图。
应用
@Component({
selector: "hello-world",
template: `
<ng-template #tpl>
<span>I am span in template</span>
</ng-template>
`
})
export class HelloWorldComponent implements AfterViewInit {
@ViewChild("tpl") tplRef: TemplateRef<HTMLElement>;
@ViewChild("tpl", { read: ViewContainerRef }) tplVcRef: ViewContainerRef;
ngAfterViewInit(){
// let commentElement = this.tplRef.elementRef.nativeElement;
// //创建内嵌视图
// let embeddedView = this.tplRef.createEmbeddedView(null);
// //动态添加子节点
// embeddedView.rootNodes.forEach(node => {
// commentElement.parentNode.insertBefore(node, commentElement.nextSibling);
// });
this.tplVcRef.createEmbeddedView(this.tplRef);
});
}
}
ngTemplateOutlet 简介
用于标识指定的 DOM 元素作为视图容器,然后自动地插入设定的内嵌视图。不需要像 ViewContainerRef 示例那样,手动创建内嵌视图。
应用
@Component({
selector: "hello-world",
template: `
<ng-container *ngTemplateOutlet="tpl"></ng-container>
<ng-template #tpl>
<span>I am span in template</span>
</ng-template>
`
})
export class HelloWorldComponent implements AfterViewInit {
@ViewChild("tpl") tplRef: TemplateRef<HTMLElement>;
@ViewChild("tpl", { read: ViewContainerRef }) tplVcRef: ViewContainerRef;
ngAfterViewInit(){}
}
ngComponentOutlet 简介
我们使用过 ComponentFactoryResolver 对象来动态创建组件,但过程有些繁琐,为了提高开发者体验和开发效率,引入了 ngComponentOutlet 指令。该指令用于使用声明式的语法,动态加载组件。
应用
@Component({
selector: "hello-world",
template: `
<div>
<div *ngComponentOutlet="authFormComponent"></div>
</div>
`
})
export class HelloWorldComponent {
authFormComponent = AuthFormComponent;
}
ViewChild 和 ViewChildren
ViewChild 和 ViewChildren 装饰器用于获取模板视图中匹配的元素。视图查询在 ngAfterViewInit 钩子函数调用前完成,因此在 ngAfterViewInit 钩子函数中,就能正常获取查询的元素。
ViewChild
一个简单的例子,通过 @ViewChild
来获取 AuthMessageComponent 组件,并且在 ngAfterViewInit 中重新设置天数:
import { Component } from '@angular/core';
@Component({
selector: 'auth-message',
template: `<div>保持登录 {{days}} 天</div>`,
})
export class AuthMessageComponent {
days: number = 7;
}
import { Component, AfterViewInit, ViewChild, Output, EventEmitter, ViewChildren, QueryList, ChangeDetectorRef, ElementRef, Renderer2 } from '@angular/core';
import { AuthMessageComponent } from '../auth-message/auth-message.component';
import { Hero } from '../../hero';
@Component({
selector: 'auth-form',
template: `
<div>
<form (ngSubmit)="onSubmit(form.value)" #form="ngForm">
<label>
姓名<input name="name" ngModel>
</label>
<label>
技能<input name="skills" ngModel #skills>
</label>
<auth-message [style.display]="(showMessage ? 'inherit' : 'none')">
</auth-message>
</form>
</div>
`
})
export class AuthFormComponent implements AfterViewInit {
showMessage: boolean = true;
@ViewChild(AuthMessageComponent) message: AuthMessageComponent;
@ViewChild('skills') skills: ElementRef;
constructor(
private cd: ChangeDetectorRef
) { }
ngAfterViewInit() {
this.message.days = 30;
this.cd.detectChanges(); // !注意
}
}
倘若没加 this.cd.detectChanges();
,控制台会抛出以下异常:
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '7'. Current value: '30'.
详细解答请参考 Angular-关于ExpressionChangedAfterItHasBeenCheckedError你需要知道的一切。
ViewChildren
该装饰器用来从模板视图中获取匹配的多个元素,返回一个 QueryList 集合。
@ViewChildren(AuthMessageComponent) message2: QueryList<AuthMessageComponent>;
ng-container vs ng-template
ng-container
逻辑容器,可用于对节点进行分组,但它不会添加额外的标签,因为它不会被放进 DOM 中。
<div [ngSwitch]="value">
<ng-container *ngSwitchCase="0">Text one</ng-container>
<ng-container *ngSwitchCase="1">Text two</ng-container>
</div>
ng-template
表示 Angular 模板。NG 模板标签只是定义了一个模板, 如果没有使用结构型指令,那些元素是不可见的。在渲染视图之前,Angular 会把 ng-template 及其内容替换为一个注释。
<p>Hip!</p>
<ng-template><p>Hip!</p></ng-template>
<p>Hooray!</p>
ng-content 内容投影
使用 ng-content
来实现内容投影的功能。
import { Component, Output, EventEmitter } from '@angular/core';
import { User } from '../../user';
@Component({
selector: 'auth-form',
template: `
<div>
<form (ngSubmit)="onSubmit(form.value)" #form="ngForm">
<ng-content></ng-content>
<label>
姓名<input name="name" ngModel>
</label>
<label>
密码<input type="password" name="password">
</label>
<button type="submit">提交</button>
</form>
</div>
`
})
export class AuthFormComponent {
@Output() submitted: EventEmitter<User> = new EventEmitter<User>();
onSubmit(user: User) {
this.submitted.emit(user);
}
}
import { Component } from "@angular/core";
import { User } from '../../user';
@Component({
selector: "app-root",
template: `
<div>
<auth-form (submitted)="createUser($event)">
<h3>注册</h3>
</auth-form>
<auth-form (submitted)="loginUser($event)">
<h3>登录</h3>
</auth-form>
</div>
`
})
export class AppComponent {
createUser(user: User) {
console.log("Create account", user);
}
loginUser(user: User) {
console.log("Login", user);
}
}
包含在 auth-form 标签内的内容,会被投影到 AuthFormComponent 组件的 ng-content 所在区域。
select 属性
select 属性用于匹配想要的内容,进行选择性内容投影。
import { Component, Output, EventEmitter } from '@angular/core';
import { User } from '../../user';
@Component({
selector: 'auth-form',
template: `
<div>
<form (ngSubmit)="onSubmit(form.value)" #form="ngForm">
<ng-content select="h3"></ng-content>
<label>
姓名<input name="name" ngModel>
</label>
<label>
密码<input type="password" name="password">
</label>
<ng-content select="button"></ng-content>
</form>
</div>
`
})
export class AuthFormComponent {
@Output() submitted: EventEmitter<User> = new EventEmitter<User>();
onSubmit(user: User) {
this.submitted.emit(user);
}
}
import { Component } from "@angular/core";
import { User } from '../../user';
@Component({
selector: "app-root",
template: `
<div>
<auth-form (submitted)="createUser($event)">
<h3>注册</h3>
<button type="submit">注册</button>
</auth-form>
<auth-form (submitted)="loginUser($event)">
<h3>登录</h3>
<button type="submit">登录</button>
</auth-form>
</div>
`
})
export class AppComponent {
createUser(user: User) {
console.log("Create account", user);
}
loginUser(user: User) {
console.log("Login", user);
}
}
ContentChild
使用 ContentChild 装饰器来获取投影的元素。
@Component({
selector: "auth-form",
template: `
<div>
<form (ngSubmit)="onSubmit(form.value)" #form="ngForm">
<ng-content select="h3"></ng-content>
<label>
姓名:<input name="name" ngModel>
</label>
<label>
密码:<input type="password" name="password">
</label>
<ng-content select="auth-remember"></ng-content>
<div *ngIf="showMessage">保持登录状态30天</div>
<ng-content select="button"></ng-content>
</form>
</div>
`
})
export class AuthFormComponent implements AfterContentInit {
showMessage: boolean;
@ContentChild(AuthRememberComponent) remember: AuthRememberComponent;
ngAfterContentInit() {
if (this.remember) {
this.remember.checked.subscribe(
(checked: boolean) => (this.showMessage = checked)
);
}
}
// ...
}
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'auth-remember',
template: `
<label>
<input type="checkbox" (change)="onChecked($event.target.checked)">
Keep me logged in
</label>
`,
})
export class AuthRememberComponent {
@Output() checked: EventEmitter<boolean> = new EventEmitter<boolean>();
onChecked(value: boolean) {
this.checked.emit(value);
}
}
在生命周期钩子 ngAfterContentInit 中通过订阅 remember 的 checked 输出属性来监听 checkbox 输入框的变化。同时根据 AuthRememberComponent 组件中 checkbox 的值来控制是否显示 ”保持登录30天“ 的提示消息。
ContentChildren
用来从通过 Content Projection 方式设置的视图中获取匹配的多个元素,返回的结果是一个 QueryList 集合。
constructor vs ngOnInit
constructor
constructor(构造函数)是类中的特殊方法,在进行类实例化操作时,会被自动调用。尽量保持简单明了,只执行一些简单的数据初始化操作或依赖注入。
ngOnInit
ngOnInit 是 Angular 组件生命周期中的一个钩子。把其它的初始化操作放在这里面执行。
构造函数会优先执行,当组件的输入属性变化时会自动触发 ngOnChanges 钩子,然后再调用 ngOnInit 钩子方法。
RxJS篇
Observable 简介
我们先来了解两个设计模式:观察者模式和迭代器模式。这两个模式是 Observable 的基础。
Observer Pattern(观察者模式)
在观察者模式中,一个目标对象管理所有相依于它的观察者对象,并且当目标对象本身的状态改变时主动向观察者对象发出通知。
观察者模式相似于发布订阅模式(Publish/Subscribe),它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当该主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。
观察者模式中有两个重要角色: Subject(主题)和 Observer(观察者)。它们的关系就好像日常生活中的期刊订阅:
- 期刊出版方:负责期刊的出版和发行工作。
- 订阅者:只需执行订阅操作,新的期刊发布后,就会主动收到通知,若取消订阅,则不再收到通知。
实战
定义一个 主题 类:
class Subject {
observerCollection;
constructor() {
this.observerCollection = [];
}
registerObserver(observer) {
this.observerCollection.push(observer);
}
unregisterObserver(observer) {
let index = this.observerCollection.indexOf(observer);
if(index >=0) this.observerCollection.splice(index, 1);
}
notifyObservers() {
this.observerCollection.forEach(observer => observer.notify());
}
}
定义一个 观察者 类:
class Observer {
name;
constructor(name) {
this.name = name;
}
notify(){
console.log(`${this.name} has been notified.`);
}
}
使用示例:
let subject = new Subject();
let observer1 = new Observer('angular'); // 创建观察者A
let observer2 = new Observer('rxjs'); //创建观察者B
subject.registerObserver(observer1); //注册观察者A
subject.registerObserver(observer2); //注册观察者B
subject.notifyObservers(); //通知观察者
subject.unregisterObserver(observer1); //移除观察者A
subject.notifyObservers(); //验证是否成功移除
在观察者模式中,通常调用注册观察者后,会返回一个函数,用于移除监听。
Iterator Pattern(迭代器模式)
迭代器模式又称游标模式。它提供一种方法顺序访问一个集合对象中的各个元素,而又不需要暴露该对象的内部表示。可以把迭代的过程从业务逻辑中分离出来。
在 JavaScript 中迭代器是一个对象,它提供一个 next() 方法,返回序列中的下一项。该方法返回包含 done
和 value
两个属性的对象。对象取值如下:
非最后一个元素 | { done: false, value: elementValue } |
最后一个元素 | { done: true, value: undefined } |
在 ES 6 中可以通过 Symbol.iterator
来创建可迭代对象的内部迭代器:
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();
调用 next() 方法来获取数组中的元素:
ES 6 中可迭代的对象:Arrays 、Strings 、Maps 、Sets 、DOM data structures (work in progress)。
优缺点
- 无论是对象集合或者数组和 hash 表,在使用迭代器模式后,即使不关心内部构造,也可以顺序访问其中的每个元素。
- 封装性良好,只需得到迭代器就可以遍历,而不用去关心算法。
- 缺点是遍历过程是一个单向且不可逆的遍历。
Observable(可观察对象)
RxJS 中有两个基本概念:Observable 和 Observer。Observables 作为被观察者,是一个值或事件的流集合;而 Observer 则作为观察者,根据 Observable进行处理。
Observable 与Observer之间的订阅发布关系如下:
- 订阅:Observer 通过 Observable 提供的 subscribe() 方法订阅 Observable。
- 发布:Observable 通过回调 next() 方法向 Observer 发布事件。
Pull(拉取) vs Push(推送)
拉取 和 推送 是两种不同的协议,用来描述数据生产者如何与数据消费者进行通信的。
pull
在“拉取”体系中,消费者决定何时从生产者那里获取数据,而生产者不知道数据什么时候将会被发送给消费者。
每个 JavaScript 函数都是拉取体系。函数是数据的生产者,调用该函数的代码通过从函数调用中“取出”一个单个返回值来对该函数进行消费。
push
在“推送”体系中,生产者决定何时发送数据给消费者,消费者不知道何时会接收到数据。
Promise(承诺)是 当今的 JS 中最常见的“推送”体系,一个Promise(数据生产者)发送一个解析过的值来执行一个回调(数据消费者)。不同于函数的是,Promise 决定着何时把值“推送”给回调函数。
RxJS 引入了 Observables(可观察对象),一个全新的“推”体系。一个可观察对象是一个产生多值的生产者,当产生新数据时,会主动“推送给” Observer(观察者)。
Observable vs Promise
Observable(可观察对象)是基于推送(Push)运行时执行(lazy)的多值集合。
— | 单值 | 多值 |
---|---|---|
拉取(Pull) | 函数 | 遍历器 |
推送(Push) | Promise | Observable |
Promise | Observable |
---|---|
创建时就立即执行 | 声明式的,当订阅的时候才会开始执行 |
返回单个值 | 随着时间推移发出多个值 |
不可取消 | 可以取消 |
— | 支持 map、filter、reduce 等操作符 |
把错误推送给它的子承诺 | 错误处理工作交给了订阅者的错误处理器 |
创建 Observable
RxJS 提供了很多创建 Observable 对象的方法,其中 create
是最基本的方法。
要执行所创建的可观察对象,并开始从中接收通知,就要调用它的 subscribe() 方法,并传入一个观察者(observer)。这是一个 JavaScript 对象,它定义了收到的这些消息的处理器(handler)。 subscribe() 调用会返回一个 Subscription 对象,该对象具有一个 unsubscribe() 方法。 当调用该方法时,就会停止接收通知。
import { Observable } from "rxjs";
const observable$ = Observable.create(observer => {
observer.next('Angular');
observer.next('RxJS');
});
observable$.subscribe(value => { //执行订阅操作
console.log(value)
});
提示:
- 虽然 Angular 框架没有对可观察对象的强制性命名约定,不过建议可观察对象的名字以“$”符号结尾。
- 同样的,如果希望用某个属性来存储可观察对象的最近一个值,它的命名惯例是与可观察对象同名,但不带“$”后缀。
RxJS 的核心特性是它的异步处理能力,但它也可以用来处理同步的行为。示例如下:
const observable$ = Observable.create(observer => {
observer.next('Angular');
observer.next('RxJS');
});
console.log('start');
observable$.subscribe(value => { //执行订阅操作
console.log(value)
});
console.log('end');
处理异步行为:
const observable$ = Observable.create(observer => {
observer.next('Angular');
observer.next('RxJS');
setTimeout(() => {
observer.next('RxJS Observable');
}, 300);
});
console.log('start');
observable$.subscribe(value => { //执行订阅操作
console.log(value)
});
console.log('end');
结论:Observable 可以应用于同步和异步的场合。
Observer(观察者)
Observable 可以被订阅,或者说可以被观察。 Observer(观察者) 中包含三个方法,每当 Observable 触发事件时,便会自动调用观察者的对应方法。
通知类型 | 说明 |
---|---|
next | 必要。每当 Observable 发送新值时,next 方法会被调用。 |
error | 可选。当 Observable 内发生错误时,error 方法会被调用。 |
complete | 可选。用来处理执行完毕通知。调用 complete 方法之后,next 方法就不会再次被调用。 |
具体示例:
const observable$ = Observable.create(observer => {
observer.next('Angular');
observer.next('RxJS');
observer.complete();
observer.next('not work');
});
// 创建一个观察者
const observer = {
next: function (value) {
console.log(value);
},
error: function (error) {
console.log(error);
},
complete: function () {
console.log('complete');
}
}
//执行订阅操作
observable$.subscribe(observer);
我们也可以在调用 Observable 对象的 subscribe 方法时,依次传入 next、error、complete 三个函数,来创建观察者:
observable.subscribe(
value => { console.log(value); },
error => { console.log('Error: ', error); },
() => { console.log('complete'); }
);
注意:next() 方法可以接受消息字符串、事件对象、数字值或各种结构。为了更通用一点,我们把由可观察对象发布出来的数据统称为 流。任何类型的值都可以表示为可观察对象,而这些值会被发布为一个流。
Subscription(订阅)
对于一些 Observable 对象,当我们不需要的时候,要释放相关的资源,以避免资源浪费。针对这种情况,我们可以调用 Subscription 对象的 unsubscribe
方法来释放资源。示例如下:
const source$ = timer(1000, 1000);
// 取得subscription对象
const subscription = source$.subscribe({
next: function (value) {
console.log(value);
},
complete: function () {
console.log('complete!');
},
error: function (error) {
console.log('Throw Error: ' + error);
}
});
setTimeout(() => {
subscription.unsubscribe();
}, 5000);
Subscription 还可以合在一起,这样的一个 Subscription 调用 unsubscribe()
方法,可能会有多个 Subscription 取消订阅 。
const subscription = observable1.subscribe(x => console.log('first: ' + x));
const childSubscription = observable2.subscribe(x => console.log('second: ' + x));
subscription.add(childSubscription);
setTimeout(() => {
// subscription 和 childSubscription 都会取消订阅
subscription.unsubscribe();
}, 1000);
Subscriptions 还有一个
remove(otherSubscription)
方法,用来撤销一个已添加的子 Subscription 。
常见创建操作符
除了上面介绍的 create 方法,RxJS 还提供了很多操作符,用于创建 Observable 对象,比如:of 、from、fromEvent 、interval 、range 、empty 、throw 、timer 等等。
详情见:https://cn.rx.js.org/manual/overview.html#h39
Angular 中的 Observable
Angular 使用可观察对象作为处理各种常用异步操作的接口。比如:
- EventEmitter 类派生自 Observable:EventEmitter 扩展了 RxJS Subject,并添加了一个 emit() 方法,这样它就可以发送任意值了。当你调用 emit() 时,就会把所发送的值传给订阅上来的观察者的 next() 方法。
- HTTP 模块使用可观察对象来处理 AJAX 请求和响应:Angular 的 HttpClient 从 HTTP 方法调用中返回了可观察对象。
- 路由器和表单模块使用可观察对象来监听对用户输入事件的响应。
Subject
订阅 Observable
在介绍 RxJS Subject 之前,先来看个例子:
import { interval } from "rxjs";
import { take } from "rxjs/operators";
const interval$ = interval(1000).pipe(take(3));
interval$.subscribe(value => console.log("Observer A get value:" + value));
setTimeout(() => {
interval$.subscribe(value => console.log("Observer B get value:" +value));
}, 1000);
通过上面示例,得出以下结论:
- Observable 对象可以被重复订阅。
- Observable 对象每次被订阅后,都会创建一次新的、独立的执行。
Observable 对象的默认行为,适用于大部分场景。但有时候,我们希望第二次订阅时,不从头开始接收 Observable 发出的值,而是从第一次订阅当前正在处理的值开始发送,这种处理方式叫 多播。
那么,以上需求如何实现呢?观察者模式定义了一对多的关系,我们可以让多个观察者同时监听同一主题。当数据源发出新值的时候,所有的观察者就能接收到新的值。
RxJS Subject(主题)
我们利用 RxJS 的 Subject 来实现上述需求:
const interval$ = interval(1000).pipe(take(3));
const subject = new Subject();
const observerA = {
next: value => console.log("Observer A get value: " + value),
error: error => console.log("Observer A error: " + error),
complete: () => console.log("Observer A complete!")
};
const observerB = {
next: value => console.log("Observer B get value: " + value),
error: error => console.log("Observer B error: " + error),
complete: () => console.log("Observer B complete!")
};
subject.subscribe(observerA); // 添加观察者A
interval$.subscribe(subject); // 订阅interval$对象
setTimeout(() => {
subject.subscribe(observerB); // 添加观察者B
}, 1000);
通过上面示例,得出 Subject 的特点:
- Subject 既是 Observable,又是 Observer。
- 当有新消息时,Subject 会通知内部的所有观察者。
RxJS Subject & Observable
Subject 是观察者模式的实现,当观察者订阅 Subject 对象时,Subject 对象会把订阅者添加到观察者列表中,每当 subject 对象接收到新值时,它会遍历观察者列表,依次调用观察者内部的 next() 方法,把值一一送出。
允许将值多播给多个观察者,所以 Subject 是多播的,而普通的 Observables 是单播的(每个已订阅的观察者都拥有 Observable 的独立执行)。
Subject 具有 Observable 所有的方法,因为它继承了 Observable 类,在 subject 类中有五个重要方法:
方法名 | 说明 |
---|---|
next | 每当 Subject 对象接收到新值时,next 方法会被调用。 |
error | 运行中出现异常,error 方法会被调用。 |
complete | Subject 订阅的 Observable 对象结束后,complete 方法会被调用。 |
subscribe | 添加观察者 |
unsubscribe | 取消订阅 |
因为 Subject 是观察者,这也意味着可以把 Subject 作为参数传给任何 Observable 的 subscribe
方法:
import { Subject, from } from 'rxjs';
const subject = new Subject();
subject.subscribe({
next: (value) => console.log('observerA ' + value)
});
subject.subscribe({
next: (value) => console.log('observerB ' + value)
});
const observable = from([1,2,3]);
observable.subscribe(subject); //可以提供一个 Subject 进行订阅
BehaviorSubject
有时候我们希望 Subject 能够保存发送给消费者的最新值,而不是单纯的发送事件,也就是说当有新的观察者订阅时,会立即接收到“当前值”。
举例说明:
import { Subject } from "rxjs";
const subject = new Subject();
subject.subscribe({
next: (value) => console.log('observerA: ' + value)
});
subject.next(1);
subject.next(2);
subject.next(3);
setTimeout(() => {
subject.subscribe({
next: (value) => console.log('observerB: ' + value)
}); // 1秒后订阅
}, 1000);
注意: 在 observerB 订阅 Subject 对象之后,它并没有收到值,因为Subject 对象没有再调用 next() 方法。
BehaviorSubject 有一个“当前值”的概念,它保存了发送给消费者的最新值。 并且当有新的观察者订阅时,会立即从 BehaviorSubject 那接收到“当前值”。
BehaviorSubject 跟 Subject 最大的不同就是 BehaviorSubject 是用来保存当前最新的值,而不是单纯的发送事件。
示例:
import { BehaviorSubject } from "rxjs";
const subject = new BehaviorSubject(0); // 0是初始值
subject.subscribe({
next: (value) => console.log('observerA: ' + value)
});
subject.next(1);
subject.next(2);
subject.next(3);
setTimeout(() => {
subject.subscribe({
next: (value) => console.log('observerB: ' + value)
}); // 1秒后订阅
}, 1000);
ReplaySubject
新增订阅者的时候,如果想接收到数据源最近发送的几个值,可以使用 ReplaySubject。ReplaySubject 记录 Observable 执行中的多个值并将其回放给新的订阅者。
import { ReplaySubject } from "rxjs";
const subject = new ReplaySubject(2); // 为新的订阅者缓冲2个值
subject.subscribe({
next: (value) => console.log('observerA: ' + value)
});
subject.next(1);
subject.next(2);
subject.next(3);
subject.subscribe({
next: (value) => console.log('observerB: ' + value)
});
subject.next(5);
AsyncSubject
AsyncSubject 会在 Observable 执行完成时(执行 complete()),将执行的最后一个值发送给观察者。示例如下:
import { AsyncSubject } from "rxjs";
const subject = new AsyncSubject();
subject.subscribe({
next: (value) => console.log("Observer A get value: " + value),
complete: () => console.log("Observer A complete!")
});
subject.next(1);
subject.next(2);
subject.next(3);
subject.complete();
setTimeout(() => {
subject.subscribe({
next: (value) => console.log("Observer B get value: " + value),
complete: () => console.log("Observer B complete!")
}); // 1秒后订阅
}, 1000);
RxJS Subject 在 Angular 中的应用
在 Angular 中,我们可以使用 RxJS Subject 来实现组件间的通信。示例如下:
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class MessageService {
private subject = new Subject<any>();
sendMessage(message: string) {
this.subject.next({ text: message });
}
clearMessage() {
this.subject.next();
}
getMessage(): Observable<any> {
return this.subject.asObservable();
}
}
import { Component } from '@angular/core';
import { MessageService } from '../services/message.service';
@Component({
selector: 'app-home',
template: `
<div>
<h1>Home</h1>
<button (click)="sendMessage()">Send Message</button>
<button (click)="clearMessage()">Clear Message</button>
</div>
`
})
export class HomeComponent {
constructor(private messageService: MessageService) { }
sendMessage() {
this.messageService.sendMessage('Message from Home Component to App Component!');
}
clearMessage() {
this.messageService.clearMessage();
}
}
import { Component, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { MessageService } from './message.service';
@Component({
selector: 'my-app',
template: `
<div *ngIf="message">{{message.text}}</div>
<app-home></app-home>
`
})
export class AppComponent implements OnDestroy {
message: any;
subscription: Subscription;
constructor(private messageService: MessageService) {
this.subscription = this.messageService.getMessage().subscribe(message => {
this.message = message;
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
何时应该取消 Subscription?
为了避免内存泄露或其它不必要的操作,我们应该及时取消订阅。判断依据为 Observable产生 有限值 或者 无限值。
不需要手动取消的场景
Angular 中有些场景已进行 unsubscribe 或通过 Observable.complete() 结束订阅数据流,在开发中不需要手动取消订阅。
1)Async pipe
AsyncPipe 会订阅一个可观察对象或承诺,并返回其发出的最后一个值。当组件销毁时,自动取消订阅。
2)HostListener
通过@HostListener进行订阅的事件,和直接在模板里订阅事件一样,也是自动取消的。
3)EventEmitter
EventEmitter 类派生自 Observable,Angular 提供了一个 EventEmitter 类,它用来从组件的 @Output() 属性中发布一些值。
4)有限的Observable
有限的Observable指的是发出的值是有限的,如timer。
5)Http
Angular 通过 HttpClient 执行 Http Request 返回的 Observables 是 Cold Observable 并且只发送一个值。在 Http Response 结束时,如果请求成功会调用 responseObserver.complete() ,自动结束数据流。如果请求失败会调用 responseObserver.error(response),自动结束数据流。
需要手动取消的场景
1)Router
Angular 在组件销毁时并没有取消router的所有订阅事件。
2)Forms
表单中的 valueChanges 和 statusChanges 等 Observable 都需要手动取消。
3)Renderer Service
4)无限的 Observable
当使用 fromEvent() 、interval() 等操作符时,输出值可能为无限的可观察对象。
5)自定义Observable
所有自定义 Observable 必须在组件销毁前手动取消。
比如 Subject,BehaviorSubject,AsyncSubject,ReplaySubject 这四种 subject,都是Hot Observable,Hot Observable 不管有没有被订阅都会源源不断的发送值,如果订阅者要主动取消订阅,就需要调用 unsubscribe() 取消订阅。
参考资源
站在巨人肩膀上学习~
- 阿宝哥的 Angular 修仙之路 教程
- Angular 中文文档 https://angular.cn/
- Rxjs 中文网 https://cn.rx.js.org/
- Angular4最佳实践之unsubscribe