# 初始化项目
一般使用 ng new 项目名 这样的命令进行初始化项目。会比较慢,他会使用 npm 进行安装依赖。可以使用如下命令:
可以指定 css 预编译以及不需要路由模块等。
ng new project --skip-install --style css --routing false
对于 package.json 文件中版本号的一些说明:
# 前面有 ~ 就是说锁定前端两个版本号,6.5.x,最后一个如果有更新就会安装最新的
"rxjs": "~6.5.2"
# 前面有 ^ 就是说锁定前面的大版本号,1.x.x;后面的两个版本号如果有最新就安装
"tslib": "^1.9.0"
# 严格锁定版本
"zone.js": "0.9.0"
# 指令相关
ngFor 相关:获取索引、是否第一个元素、是否最后一个元素、可迭代对象中的索引号为奇数、是否偶数
<li *ngFor="let menu of menus;let i = index;let first = first;let last = last;let odd = odd; let even = even">
{{munu.item}}
</li>
https://angular.cn/api/common/NgForOf
ngIf 相关
<div *ngIf="条件表达式 else elseContent">
条件为真展示
</div>
<ng-template #elseContent>
条件为假时展示
</ng-template>
# 事件处理和样式绑定
[class.样式类名]
= "判断表达式" 是在应用单个 clas 时的常用技巧;- ngStyle
<div [ngStyle]="{ 'color': someColor }"></div>
ngStyle 由于是嵌入式样式,他会覆盖掉其他样式,使用时需谨慎; - 使用方括号 [] 是数据绑定,如果带方括号,等号后面就是一个对象或表达式;
- 不适用方括号,等号后面 angular 认为是一个字符串,但如果我们此时在等号后使用 {{}} 就是和方括号等效的;
- 圆括号 () 用于事件绑定,等号后可以接表达式也可以是一个定义在类中的函数;
# 组件
我们一般封装组件,如果需要属性的绑定通过 @input 进行表示,对于事件,如果需要增加事件回调。告诉外部调用者执行。通过 @output 来标识。
# 组件的声明周期
- constructor:构造函数永远首先被调用
- ngOnChanges:输入属性变化时被调用
- ngOnInit:组件初始化被调用
- ngDoCheck:脏值检测时调用
- ngAfterContentInit:当内容投影 ng-content 完成时调用
- ngAfterContentChecked:angular 检测投影内容时调用(多次)
- ngAfterViewInit:当组件视图(子视图)初始化完成时
- ngAfterViewChecked:当检测视图变化时(多次)
- ngOnDestroy:当组件销毁时
import { AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
export class ScrollableTabComponent implements OnInit, OnChanges, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked {
/**
* 构造函数永远首先被调用
*/
constructor() { }
/**
* 组件初始化完成,在这个函数中,我们可以安全的使用组件的属性和方法
*/
ngOnInit(): void {
}
// 监听自己组件本身数值变化-输入值的变化 @Input 属性发生变化
// 参数为索引对象,key 为属性的名字,value 为 SimpleChanges
ngOnChanges(value: SimpleChanges) {
console.log(value)
}
/**
* 组件内容初始化
*/
ngAfterContentInit() {
}
/**
* 组件内容脏值检测
*/
ngAfterContentChecked() {
}
/**
* 组件视图初始化-组件和他的子组件都初始化完成
*/
ngAfterViewInit() {
}
/**
* 组件视图的脏值检测
*/
ngAfterViewChecked() {
}
/**
* 组件销毁时执行
*/
ngOnDestroy() {
}
// ngDoCheck() {
// console.log('组件脏值监测')
// }
}
ng-content 相当于 vue 中的 slot 插槽。父组件向子组件传递内容。
# 模板在组件类中的引用
#
后面是给模板或者 dom 元素起一个引用名字以便可以在组件类或模板中进行引用
<div #helloDiv>
</div>
然后在类中引用:
@ViewChild 是一个选择器,用来查找要引用的 dom 元素或者组件,ElementRef 是 dom 元素的一个包装类,因为 dom 元素不是 angular 中的类,所以需要一个包装类以便在 angular 中使用和标识其类型。
export class AppComponent {
@ViewChild('helloDiv') helloDivRef: ElementRef
}
如果想获取组件,可以在 @ViewChild 中直接使用组件的名称,也就是定义组件的类名:
<app-image-slider></app-image-slider>
@ViewChild(ImageSliderComponent) imageSlider: ImageSliderComponent
如果想引用多个模板元素,需要使用 @ViewChildren
<img
#img
*ngFor="let slider of sliders"
[src]="slider.imgUrl"
[alt]="slider.caption"
>
@ViewChildren('img') imgs: QueryList<ElementRef>;
# 获取 DOM 节点修改样式
可以通过 Renderer2 去修改样式。
import { AfterViewInit, Component, ElementRef, Input, OnInit, QueryList, Renderer2, ViewChild, ViewChildren } from '@angular/core';
export interface ImageSlider {
imgUrl: string
link: string
caption: string
}
@Component({
selector: 'app-image-slider',
templateUrl: './image-slider.component.html',
styleUrls: ['./image-slider.component.css']
})
export class ImageSliderComponent implements OnInit, AfterViewInit {
@Input() sliders: ImageSlider[] = []
// static 是否为静态,在 if 或者 for 中包含的是 false
@ViewChild('imageSlider', { static: true }) imgSlider: ElementRef
@ViewChildren('img') imgs: QueryList<ElementRef>
constructor(
// 依赖注入
private rd2: Renderer2
) { }
ngAfterViewInit() {
this.imgs.forEach(item => {
this.rd2.setStyle(item.nativeElement, 'height', '100px')
})
}
}
# 双向绑定
<input [value]="username" (input)="username = $event.target.value">
还有 ngModel,是 FormsModel 中提供的指令,需要将其引入。使用 [(ngModel)] = "变量"
形式进行双向绑定。
我们也可以自己实现双向绑定,如下代码:
private _username
// 子组件
@Output() usernameChange = new EventEmitter()
@input()
public get username(): string {
return this._username
}
public set username(value: string) {
this._username = value
this.usernameChange.emit(value)
}
// 父组件
<child [(username)]="username"></child>
# 模块
# @NgModule 注解
- declarations 数组:为模块拥有的组件、指令或管道。注意每个组件、指令、管道只能在一个模块中声明;
- providers 数组:是模块中需要使用的服务;
- exports 数组:暴露给其他模块使用的组件、指令或管道等;
- imports 数组:导入本模块需要的依赖模块,注意只能是模块;
# 模块的 “坑”
导入其他模块时,需要知道使用该模块的目的:
- 如果是组件,那么需要在每一个需要模块都进入导入;
- 如果是服务,那么一般来说在根模块中导入一次即可;
需要在每个需要的模块中进行导入的:
- CommonModule:提供绑定、*ngIf 和 *ngFor 等基础指令,基本上每个模块都需要导入它。
- FormsModule / ReactiveFormsModule:表单模块需要在每个需要的模块导入。
- 提供组件、指令或管道的模块。
只在根模块导入一次的:
- HttpClientModule / BrowserAnimationsModule / NoopAnimationsModule
- 只提供服务的模块。
# 装饰器
如下例子:
// 对字符串
export function Emoji() {
return (target: Object, key: string) => {
let val = target[key]
const getter = () => {
return val
}
const setter = (value: string) => {
val = `1 ${value} 1`
}
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
})
}
}
// 对函数
export function Confirmable(message: string) {
return (target: Object, key: string, descriptor: PropertyDescriptor) => {
const original = descriptor.value
descriptor.value = (...args: any) => {
const allow = window.confirm(message)
if (allow) {
const result = original.apply(this, args)
return result
}
return null
}
return descriptor
}
}
使用:
import { Component, OnInit } from '@angular/core';
import { Confirmable, Emoji } from '../../decorators';
@Component({
selector: 'app-horizontal-grid',
templateUrl: './horizontal-grid.component.html',
styleUrls: ['./horizontal-grid.component.css']
})
export class HorizontalGridComponent implements OnInit {
constructor() { }
// 实现自定义注解
@Emoji() result = 'Hello'
@Confirmable('您确认要执行吗')
handleClick() {
console.log('点击执行')
}
}
# 组件嵌套和投影组件
避免组件嵌套导致冗余数据和事件传递,可以通过下面几种方式:
- 内容投影
- 路由
- 指令
- 服务
# 投影组件
投影组件就是 ng-content 标签。简单来说 ng-content 就是动态内容;跟 vue 的插槽是很类似的。如下格式:
<ng-content select="样式类/HTML标签/指令"></ng-content>
select 可以选择很多,比如选择有 AppGridItem 的指令元素:
<ng-content select="[AppGridItem]"></ng-content>
选择 class 还有 span-wrapper 的元素:
<ng-content select=".span-wrapper"></ng-content>
# 指令
# 自定义指令
自定义指令,如下代码:
<div appGridItem *ngFor="let item of channels">
<img [src]="item.icon" alt="" appGridItemImage [fitMode]="'none'">
<span appGridItemTitle>{{item.title}}</span>
</div>
对应指令:appGridItem
import { Directive, ElementRef, Renderer2 } from '@angular/core';
@Directive({
selector: '[appGridItem]'
})
export class GridItemDirective {
constructor(private elr: ElementRef, private rd2: Renderer2) {
this.rd2.setStyle(this.elr.nativeElement, 'display', 'grid')
this.rd2.setStyle(this.elr.nativeElement, 'grid-template-areas', `'image' 'title'`)
this.rd2.setStyle(this.elr.nativeElement, 'place-items', 'center')
this.rd2.setStyle(this.elr.nativeElement, 'width', '4rem')
}
}
对应指令:appGridItemImage,可以传递参数。指令跟组件一样,也是有声明周期的。
import { Directive, ElementRef, Input, Renderer2, OnInit } from '@angular/core';
@Directive({
selector: '[appGridItemImage]'
})
export class GridItemImageDirective implements OnInit {
// 可以接收传入的值
@Input() appGridItemImage = '2rem'
@Input() fitMode = 'cover'
constructor(private elr: ElementRef, private rd2: Renderer2) {}
ngOnInit() {
this.rd2.setStyle(this.elr.nativeElement, 'grid-srea', 'image')
this.rd2.setStyle(this.elr.nativeElement, 'width', this.appGridItemImage)
this.rd2.setStyle(this.elr.nativeElement, 'width', this.appGridItemImage)
this.rd2.setStyle(this.elr.nativeElement, 'object-fit', this.fitMode)
}
}
# 指令的样式和事件绑定
指令没有模板,指令要寄宿在一个元素之上——宿主(Host)
- @HostBinding 绑定宿主的属性或者样式
- @HostListener 绑定宿主的事件
如下代码:
import { Directive, ElementRef, Renderer2, OnInit, HostBinding } from '@angular/core';
@Directive({
selector: '[appGridItem]'
})
export class GridItemDirective implements OnInit {
// 替换下面的写法
@HostBinding('style.display') display = 'grid'
@HostBinding('style.grid-template-areas') template = `'image' 'title'`
@HostBinding('style.place-items') align = 'center'
@HostBinding('style.width') width = '4rem'
constructor(private elr: ElementRef, private rd2: Renderer2) {}
ngOnInit() {
// this.rd2.setStyle(this.elr.nativeElement, 'display', 'grid')
// this.rd2.setStyle(this.elr.nativeElement, 'grid-template-areas', `'image' 'title'`)
// this.rd2.setStyle(this.elr.nativeElement, 'place-items', 'center')
// this.rd2.setStyle(this.elr.nativeElement, 'width', '4rem')
}
}
对于如果有外部需要传递参数的可以使用多个装饰器进行修饰,代码如下:
import { Directive, ElementRef, Renderer2, HostBinding, Input } from '@angular/core';
@Directive({
selector: '[appGridItemTitle]'
})
export class GridItemTitleDirective {
@HostBinding('style.font-size') @Input() appGridItemTitle = '1rem'
@HostBinding('style.grid-area') area = 'title'
constructor(private elr: ElementRef, private rd2: Renderer2) {}
}
# 组件中的 Host
我们在组件的样式中直接写: 是作用在组件的样式。
:host {
/* */
}
# 路由
# 路径参数配置以及路由配置
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeContainerComponent, HomeDetailComponent } from './components';
const routes: Routes = [
{
path: 'home',
component: HomeContainerComponent,
children: [
{
/**
* 路由节点可以没有 component
* 一般用于重定向到一个默认子路由
*/
path: '',
redirectTo: 'hot',
pathMatch: 'full'
},
{
/**
* 路径参数,看起来是 URL 的一部分
*/
path: ':tabLink',
component: HomeDetailComponent
}
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HomeRoutingModule {}
# 激活路由
- 导航式:通过 routerLink 属性:
<a [routerLink]="['home', 'hot', { name: 'val1' }]">热门</a>
注意,routerLinkActive 属性跟 react router 一样,是给激活的路由添加一个 active 的类。
<a [routerLink]="['grand']" routerLinkActive="active">link</a>
- 编程式导航
this.router.navigate(['home', 'hot', { name: 'val1' }])
URL: http://localhost:4200/home/hot;name=val1
读取参数:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
export class HomeDetailComponent implements OnInit {
constructor(private route: ActivatedRoute) { }
ngOnInit(): void {
// 路径参数
this.route.paramMap.subscribe(params => {
this.selectedTabLink = params.get('tabLink')
})
}
}
如果是通过 queryParams 传递参数:
<a [routerLink]="['/home']" [queryParams]= { name: 'val1' }>热门</a>
this.router.navigate(['home'], { queryParams: { name: 'val1' } })
URL: http://localhost:4200/home?name=val1
读取参数:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
export class HomeDetailComponent implements OnInit {
constructor(private route: ActivatedRoute) { }
ngOnInit(): void {
// 查询参数
this.route.queryParamMap.subscribe(params => {
this.selectedTabLink = params.get('tabLink')
})
}
}
# 管道
自定义管道:ng g p 管道名称;
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'ago'
})
export class AgoPipe implements PipeTransform {
transform(value: any) {
if (value) {
const seconds = Math.floor((+new Date() - +new Date(value)) / 1000)
if (seconds < 30) {
return '刚刚'
}
const intervals = {
年: 3600 * 24 * 365,
月: 3600 * 24 * 30,
周: 3600 * 24 * 7,
天: 3600 * 24,
小时: 3600,
分钟: 60,
秒: 1,
}
let counter = 0
for (const unitName in intervals) {
if (Object.prototype.hasOwnProperty(unitName)) {
const unitValue = intervals[unitName]
counter = Math.floor(seconds / unitValue)
if (counter > 0) {
return `${counter} ${unitName} 前`
}
}
}
}
return value
}
}
# 依赖注入
我们在组件中使用服务的时候,直接是在组件的构造函数中通过属性的形式注入到组件中:没有通过 new 实例化,我们在组件中就可以直接使用。这其实就是依赖注入。
constructor(private route: ActivatedRoute) { }
# 自定义注入类
我们自定义一个服务,首先需要标记服务为 @injectable() 标记为可输入的服务,然后在模块中 provides 数组或则 import 对应模块进行声明。最后就可以在组件的构造函数中声明使用,angular 框架会帮你完成服务的注入。
如下例子,是在组件中实现一个依赖注入:
import { Component, Injectable, Injector, OnInit } from '@angular/core';
@Injectable()
class Product {
constructor(private name: string) {
}
}
@Injectable()
class PurchaseOrder {
private amount: number
constructor(
private product: Product
) {}
}
@Component({
selector: 'app-home-grand',
templateUrl: './home-grand.component.html',
styleUrls: ['./home-grand.component.css']
})
export class HomeGrandComponent implements OnInit {
constructor() { }
ngOnInit(): void {
const injector = Injector.create({
providers: [
{
provide: Product,
// useClass: Product,
useFactory: () => {
return new Product('j')
},
},
{
provide: PurchaseOrder,
useClass: PurchaseOrder,
// 依赖
deps: [
Product
]
}
]
})
// 获取
console.log(injector.get(Product))
console.log(injector.get(PurchaseOrder))
}
}
# 通过模块的形式进行注入
我们写一个依赖注入的时候,是不需要这样的引入的,可以直接在模块的 providers 数组中进行引入(如果是 useClass),如果是 useFactory 的服务: 如下:
@NgModule({
providers: [
PurchaseOrder,
{
provide: Product,
useFactory: () => {
return new Product('j')
},
},
]
})
# 注入数值
当然我们可以注入一个数值;如下代码:
ngOnInit(): void {
const injector = Injector.create({
providers: [
{
provide: 'baseUrl',
useValue: 'http://localhost:3000/api'
}
]
})
这样注入一个值是不太好的,因为如果项目很大;会出现重名。我们可以通过创建一个 InjectionToken,angular 框架会进行处理;避免冲突。如下代码:
ngOnInit(): void {
const token = new InjectionToken<string>('baseUrl')
const injector = Injector.create({
providers: [
{
provide: token,
useValue: 'http://localhost:3000/api'
}
]
})
}
可以将一个值通过在模块中进行注册,然后在组件中使用,如下代码:
import { NgModule } from '@angular/core';
import { token } from './services';
@NgModule({
providers: [
{
provide: token,
useValue: 'http://api'
}
]
})
export class HomeModule { }
组件中使用:
import { Component, Inject, OnInit } from '@angular/core';
import { token } from '../../services';
@Component({
selector: 'app-home-container',
templateUrl: './home-container.component.html',
styleUrls: ['./home-container.component.css']
})
export class HomeContainerComponent implements OnInit {
constructor(
@Inject(token) private baseUrl: string
) { }
ngOnInit(): void {
console.log(this.baseUrl, 'baseUrl')
}
}
一般我们写注入,会使用:注入到根模块,每个模块都可以使用,推荐写法。就不用将注入的服务放入到模块的 providers 中。
@Injectable({
providedIn: 'root'
})
如果需要注入到指定模块:
@Injectable({
providedIn: HomeModule
})
# 脏值检测
什么是脏值检测:
- 首先脏值检测就是当数据改变时进行更新视图。类似其他框架,比如 vue 是使用数据代理。进行监听数值变化去生成虚拟 dom 更新视图。而 react 是直接刷新视图。
什么时候会触发:
- 浏览器事件(如 click、mouseover、keyup 等)
- setTimeout 和 setInterval
- http 请求
如何进行检测:检查两个状态值:当前状态和新状态。
angular 会根据视图中 dom 绑定属性生成一个绑定的一个树状结构(就是哪个属性绑定哪个值的一种对象结构,绑定名,绑定表达式),从根组件到子组件。对于检查是单向的数据流。
对于组件的声明周期中。也有脏值检测过程。我们一般是在更新 DOM 之前进行数值属性的绑定,以避免引起无线循环的脏值检测。
如下代码,会导致无限循环,抛出异常:
import { Component, OnInit, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit, AfterViewInit {
_title
public get title() {
console.log('脏值检测')
return this._title
}
constructor() {
this._title = 'hi'
}
ngOnInit(): void {
}
ngAfterViewInit() {
this._title = 'hee'
}
}
// ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value for 'textContent': 'hi'. Current value: 'hee'
如果我们需要在脏值检测之后重新赋值,可以使用 ngZone 来实现; Zone 是一种用于拦截和跟踪异步工作的机制;Zone.js 将会对每一个异步操作创建一个 task。一个 task 运行于一个 Zone 中。通常来说, 在 Angular 应用中,每个 task 都会在 “Angular” Zone 中运行,这个 Zone 被称为 NgZone。一个 Angular 应用中只存在一个 Angular Zone,而变更检测只会由运行于这个 NgZone 中的异步操作触发。如下代码:
import { Component, OnInit, AfterViewChecked, NgZone } from '@angular/core';
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit, AfterViewChecked {
_title
public get title() {
console.log('脏值检测')
return this._title
}
constructor(private ngZone: NgZone) {
this._title = 'hi'
}
ngOnInit(): void {
}
ngAfterViewChecked() {
// 运行在 angular 之外的 task
this.ngZone.runOutsideAngular(() => {
setInterval(() => {
this._title = 'hee'
}, 100)
})
}
}
具体场景:倒计时展示;
import { formatDate } from '@angular/common';
import { Component, OnInit, AfterViewChecked, NgZone, ViewChild, ElementRef, Renderer2 } from '@angular/core';
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit, AfterViewChecked {
_title
_time
@ViewChild('timeRef', { static: true }) timeRef: ElementRef
public get title() {
console.log('脏值检测')
return this._title
}
public get time() : number {
return this._time
}
constructor(private ngZone: NgZone, private rd2: Renderer2) {
this._title = 'hi'
}
ngOnInit(): void {
}
ngAfterViewChecked() {
// 运行在 angular 之外的 task
this.ngZone.runOutsideAngular(() => {
setInterval(() => {
// this.timeRef.nativeElement.innerText = Date.now()
this.rd2.setProperty(
this.timeRef.nativeElement,
'innerText',
formatDate(Date.now(), 'HH:mm:ss:SSS', 'en-US')
)
}, 100)
})
}
}
注意这里的细节,可以在 Renderer2 中使用管道。
脏值检测的检查是如果整个树结构有值的变化,那么脏值检测会将整个树结构跑一遍。如果树很大,会引起性能问题。可以使用脏值检测的 OnPush 策略去优化。 OnPush 策略是只对有 @input 注解的组件进行脏值检测。如果注解的值改变就会引发一次脏值检测。
import { formatDate } from '@angular/common';
import { Component, OnInit, AfterViewChecked, NgZone, ViewChild, ElementRef, Renderer2, ChangeDetectionStrategy, Input } from '@angular/core';
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit, AfterViewChecked {
@Input() str: string = ''
public get title() {
console.log('脏值检测')
return this._title
}
public get time() : number {
return this._time
}
constructor(private ngZone: NgZone, private rd2: Renderer2) {
this._title = 'hi'
}
ngOnInit(): void {
}
}
他在之后 Input 的 str 属性改变的时候才会触发脏值检测进行更新页面。如果我们想强制更新。可以使用下面的方法:
import { formatDate } from '@angular/common';
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit, AfterViewChecked {
@Input() str: string = ''
constructor(private cd: ChangeDetectorRef) {}
ngOnInit(): void {
// 手动触发,进行脏值检测
this.cd.markForCheck()
}
}
# Angular HttpClient
# 使用
他是 angular 中进行发送 http 请求的 API,需要在根模块中导入。返回的值是 observable 对象。必须通过订阅,才会发送请求,否则不会发送。如下例子:
首先在根模块进行引入:
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
// 该模块有哪些组件
declarations: [
AppComponent
],
// 该模块依赖哪些模块
imports: [
HttpClientModule,
],
providers: [],
// 根模块显示的组件
bootstrap: [AppComponent]
})
export class AppModule { }
使用:
// service 中
getBanners() {
return this.http.get<ImageSlider[]>(`${environment.baseUrl}/banners`, {
params: {
iCode: environment.icode
}
})
}
// 组件中调用
this.service.getBanners().subscribe(tabs => {
this.imageSliders = tabs
})
# 拦截器
如下例子:注意,需要在模块中进行导入。
使用命令创建拦截器:ng g interceptor interceptors/notification
拦截器返回的是 Observable 对象,可以通过 pipe 方法监听数据返回,然后通过 tap 继续下发执行。下面这个例子是每次响应成功后触发。
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpResponse
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators'
@Injectable()
export class NotificationInterceptor implements HttpInterceptor {
constructor() {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(
tap((event: HttpEvent<any>) => {
if (event instanceof HttpResponse && event.status > 200 && event.status < 300) {
console.log('请求成功')
}
})
);
}
}
下面这个拦截器是在每个请求上添加参数:
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
@Injectable()
export class ParamsInterceptor implements HttpInterceptor {
constructor() {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const modifiedReq = request.clone({
setParams: {
icode: environment.icode
}
})
return next.handle(modifiedReq)
}
}
在根模块进行导入:
import { NgModule } from '@angular/core';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { ParamsInterceptor, NotificationInterceptor } from './home';
@NgModule({
providers: [
{
// HTTP_INTERCEPTORS 用于多个对象的一个令牌
provide: HTTP_INTERCEPTORS,
useClass: ParamsInterceptor,
multi: true
},
{
// HTTP_INTERCEPTORS 用于多个对象的一个令牌
provide: HTTP_INTERCEPTORS,
useClass: NotificationInterceptor,
multi: true
}
],
})
export class AppModule { }
# rxjs 响应式编程类库
rxjs 是一个响应式编程类库。rxjs 主要的编程思维是将事件或数据看成一个流。响应式编程也就是随着事件流中的元素的变化随之做出响应的动作。流的状态有 next、error、complete;所有的操作都是异步的。
如下简单例子:
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { fromEvent } from 'rxjs';
@Component({
selector: 'app-parent',
templateUrl: './parent.component.html',
styleUrls: ['./parent.component.css']
})
export class ParentComponent implements OnInit {
@ViewChild('inputRef', { static: true }) inputRef: ElementRef
constructor() { }
ngOnInit(): void {
// 将 input 事件转换成事件流;观察 input 事件
fromEvent(this.inputRef.nativeElement, 'input').subscribe((ev: any) => console.log(ev.target.value))
}
}
# observable 的三种状态
如下代码:
this.route.paramMap.subscribe(params => {
// next 区块
console.log('路径参数:', params)
}, err => {
// 错误处理区块
}, () => {
// complete 区块
// 无论错误还是最终结束,都会走到 complete 这里
})
observable 有很多修饰符,如下代码:
ngOnInit(): void {
this.route.paramMap
// 判断过滤有 tabLink 的参数
.pipe(
filter(params => params.has('tabLink')),
// 只获取 tabLink 参数
map(params => params.get('tabLink'))
)
// 最后得到的数据是过滤的结果,tabLink 字符串
.subscribe(tabLink => {
this.selectedTabLink = tabLink
})
this.imageSliders = this.service.getBanners()
// this.service.getBanners().subscribe(tabs => {
// this.imageSliders = tabs
// })
this.channels = this.service.getChannels()
}
我们也可以不用写 .subscribe,用下面更简便的语法:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
@Component({
selector: 'app-home-detail',
templateUrl: './home-detail.component.html',
styleUrls: ['./home-detail.component.css']
})
export class HomeDetailComponent implements OnInit {
constructor(private route: ActivatedRoute) { }
selectedTabLink$: Observable<string>
ngOnInit(): void {
this.selectedTabLink$ = this.route.paramMap
// 判断过滤有 tabLink 的参数
.pipe(
filter(params => params.has('tabLink')),
// 只获取 tabLink 参数
map(params => params.get('tabLink'))
)
}
}
使用的时候,因为是异步获取的,需要加上 async 管道:
<div *ngIf="(selectedTabLink$ | async) === 'hot' else other"></div>
使用 observable async,如果组件使用了 onPush 策略,也不需要使用 markForCheck() 去触发脏值检测。
修改 http 请求:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { HomeService } from '../../services';
@Component({
selector: 'app-home-detail',
templateUrl: './home-detail.component.html',
styleUrls: ['./home-detail.component.css']
})
export class HomeDetailComponent implements OnInit {
imageSliders$: Observable<ImageSlider[]>
constructor(private route: ActivatedRoute, private service: HomeService) { }
selectedTabLink$: Observable<string>
ngOnInit(): void {
this.selectedTabLink$ = this.route.paramMap
// 判断过滤有 tabLink 的参数
.pipe(
filter(params => params.has('tabLink')),
// 只获取 tabLink 参数
map(params => params.get('tabLink'))
)
this.imageSliders$ = this.service.getBanners()
}
}
service 中的 getBanners 方法:
getBanners() {
return this.http.get<ImageSlider[]>(`${environment.baseUrl}/banners`, {
params: {
iCode: environment.icode
}
})
}
页面中使用:
<app-image-slider [sliders]="imageSliders$ | async"></app-image-slider>
我们在前面没有使用 async 管道的时候,通过 observable 的 subscribe 去订阅数据的修改。其实我们还需要进行取消订阅,防止内存泄漏: 使用 async 管道是不需要进行取消订阅的。
ngOnInit(): void {
this.sub = this.route.queryParamMap.subscribe(params => {
console.log(params)
})
}
ngOnDestroy(): void {
this.sub.unsubscribe()
}
← 基础