【Angular 基础入门】——知识点整合
文章目录
- 快速入门
- 一、Angular 环境搭建
- 二、基本概念
- 模块简介
- 组件简介
- 服务与依赖注入
- 路由
- 三、基础教程
- 使用路由
- 自定义组件
- 常用结构型指令
- 事件绑定
- 模板引用
- 模板表达式中的运算符
- 注入服务
- 指令概览
- 输入和输出属性
- 使用双向绑定
- 组件样式
- attribute、class 和 style 绑定
- 服务篇
- 依赖注入的概念
- 依赖注入的应用
- 提供服务
- DI 令牌
- 依赖提供者
- 组件篇
- 生命周期钩子
- 动态创建组件
- 组件样式
- 视图封装
- :host
- :: ng-deep
- :host-context
- 把样式加载进组件中
- 自定义属性型指令实践
- HostListener
- 向指令传递值
- HostBinding
- 自定义结构型指令实践
- 自定义管道实践
- Element篇
- ElementRef 简介
- TemplateRef 简介
- ViewContainerRef 简介
- ngTemplateOutlet 简介
- ngComponentOutlet 简介
- ViewChild 和 ViewChildren
- ng-container vs ng-template
- ng-content 内容投影
- constructor vs ngOnInit
- RxJS篇
- Observable 简介
- Observer Pattern(观察者模式)
- Iterator Pattern(迭代器模式)
- Observable(可观察对象)
- Pull(拉取) vs Push(推送)
- Observable vs Promise
- 创建 Observable
- Observer(观察者)
- Subscription(订阅)
- 常见创建操作符
- Angular 中的 Observable
- Subject
- 订阅 Observable
- RxJS Subject(主题)
- RxJS Subject & Observable
- BehaviorSubject
- ReplaySubject
- AsyncSubject
- RxJS Subject 在 Angular 中的应用
- 何时应该取消 Subscription?
- 不需要手动取消的场景
- 需要手动取消的场景
- 参考资源
快速入门
一、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) returnthis.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'); //创建观察者Bsubject.registerObserver(observer1); //注册观察者A
subject.registerObserver(observer2); //注册观察者Bsubject.notifyObservers(); //通知观察者subject.unregisterObserver(observer1); //移除观察者Asubject.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
【Angular 基础入门】——知识点整合相关推荐
- C# 零基础入门知识点汇总
C# 零基础入门 知识点汇总 前言 一,基础语法(1~10) 二,流程控制(11~20) 三,数组相关(21~30) 四,函数介绍(31~40) 五,类和对象(41~50) 六,面向对象(51~60) ...
- javascript基础入门知识点整理
学习目标:- 掌握编程的基本思维- 掌握编程的基本语法 typora-copy-images-to: media JavaScript基础 HTML和CSS 京东 课前娱乐 众人皆笑我疯癫,我笑尔等看 ...
- MyBatis基础入门--知识点总结
对原生态jdbc程序的问题总结 下面是一个传统的jdbc连接oracle数据库的标准代码: public static void main(String[] args) throws Exceptio ...
- Pandas基础入门知识点总结
目录 1.pandas 常用类 1.1 Series 1.1.1创建 Series 1.1.2 访问 Series 数据 1.1.3 更新.插入和删除 1.2 DataFrame 1.2.1 创建 D ...
- Python基础入门知识点——Python中的异常
前言 在先前的一些章节里你已经执行了一些代码,你一定遇到了程序"崩溃"或因未解决的错误而终止的情况.你会看到"跟踪记录(traceback)"消息以及随后解释器 ...
- insert exec 语句不能嵌套_Python基础入门知识点——if 语句简介
前言 if 语句是最简单的选择结构.如果满足条件就执行设定好的操作,不满足条件就执行其他其他操作. PS:如有需要Python学习资料的小伙伴可以加下方的群去找免费管理员领取 点击加群即可免费获取Py ...
- 【JAVA】基础入门知识点回顾
1. public static String与 public static final String的差异 String 为不可变对象,一旦产生,就不可以改变其值. public static St ...
- Matplotlib 基础入门知识点总结
目录 1.绘图的一些基本命令展示 2.Matplotlib 绘制网格 3.plt.gca() 对坐标轴的操作 4. 图表的样式参数设计 5.创建图形对象 6.绘制多子图 1.add_axes():添加 ...
- Python零基础入门,纯干货!【Python基础知识点汇总整理】
目录 第一章 认识Python.Python常用集成开发环境PyCharm 一.认识 Python 01. Python 的起源 1.2 Python 的设计目标 1.3 Python 的设计哲学 0 ...
最新文章
- 修改python plot折线图的坐标轴刻度
- Java Socket通信编程
- 怎样学好Oracle子查询,Oracle学习(六):子查询
- Spring-data-redis 反序列化异常
- uboot的环境变量分析(printenv)
- 机器人能翻转汉堡肉饼 短暂上岗后将“休息”四天
- 对大量转载贴识别算法的研究
- git工具 将源码clone到本地指定目录的三种方式
- linux平台设备驱动模型是什么意思,Linux设备驱动模型之我理解
- keras + tensorflow —— 文本处理
- [转]Intent跳转到系统应用中的拨号界面、联系人界面、短信界面及其他
- pyside6的MQTT客户端
- 放慢你的额脚步_放慢脚步使我成为更好的领导者
- 化学实验室改造方案怎么做?
- Ubuntu 安装 XDM 2018 ( Xtreme Download Manager 2018 )
- LVGL8学习之row and a column layout with flexbox
- 二类分类器构造多类分类器
- 六月:手动学数据分析(task02)
- 说出我国的超级计算机的发展历程,中国超级计算机发展史
- 弘辽科技:淘宝新店提升销量可以吗?怎么提升关键词?
热门文章
- 在Hbulider中点击事件会出现两次
- 主外键constraint、primary key、foreign key、check、default的用法和理解
- pytorch dali 加速 dali支持的数据处理列表,mxnet tensorflow caff读取数据转换 pytorch训练
- Java 使用wps将word文件转换pdf文件
- OA开发很简单 OA实施很复杂
- 描述一下脚本<script>放在<head>和放到<body>底部的区别
- D3D Surface/Texture SDL DDraw渲染视频的区别和疑问
- 字符串处理 扩展的脚本技巧 正则表达式
- Windows7 IIS7.5部署ASP网站
- lqc_软件仓库部署及应用