Angular基础

一、创建应用

首先需要系统中有node环境 其次安装npm

安装angular-cli npm install @angular/cli -g

创建应用 ng new angular-test --minimal --inline-template false

  1. --skipGit=true

  1. --minimal=true 创建没有单元测试的简易脚手架

  1. --skip-install

  1. --style=css

  1. --routing=false

  1. --inlineTemplate

  1. --inlineStyle

  1. --prefix

运行应用 ng serve

  1. --open=true 应用构建完成后在浏览器中运行

  1. --hmr=true 开启热更新

  1. hmrWarning=false 禁用热更新警告

  1. --port 更改应用运行端口

二、组件

命令创建模板

ng g c <component-name>

// ng g c user// 一个模板一般拥有以下内容import { Component } from'@angular/core';

@Component({
  selector: 'app-user',
  template: `
    <h1>
      user works!
    </h1>
  `,
  styles: ['h1 { font-weight: normal; }']
})
exportclassUserComponent {
}

// template 为单独文件时 改为templateUrl: ''// styles 为单个或多个文件时 styleUrls: []

组件生命周期(按以下顺序执行)

  • constructor 实例化组件类的构造函数,用于接收注入服务

  • ngOnChanges() 输入属性值发生变化时调用 ngOnInit()之前会调用一次

  • ngOnInit() 只调用一次,第一次显示数据绑定和设置指令/组件输入属性之后,初始化指令/组件

  • ngDoCheck() 紧跟ngOnChanges和首次执行ngOnInit()后调用

  • ngAfterContentInit() 当angular把外部内容投影进组件视图或指令所在的视图后调用(只调用一次)

  • ngAfterContentChecked() 当检查完被投影到组件或指令中的内容之后 ngAfterContentInit() 和每次 ngDoCheck() 之后调用。

  • ngAfterViewInit() 初始化完组件视图及其子视图或指令视图后 第一次 ngAfterContentChecked() 之后调用,只调用一次。

  • ngAfterViewChecked() ngAfterViewInit() 和每次 ngAfterContentChecked() 之后调用。

  • ngOnDestory()

ngOnInit() 执行http请求 获取初始化数据 设置指令 绑定数据等操作

ngOnDestory() 取消订阅可观察对象和Dom事件 停止计时器 反注册该指令在全局或应用服务中注册过的所有回调。

@ViewChild(ChildViewComponent) viewChild!: ChildViewComponent; 获取子视图的组件实例

@ContentChild(ChildComponent) contentChild!: ChildComponent; 获取子组件内容投影的实例

视图封装

Component 的装饰器提供了 encapsulation 选项

ViewEncapsulation.ShadowDom:Angular 使用浏览器内置的 Shadow DOM API 将组件的视图包含在 ShadowRoot(用作组件的宿主元素)中,并以隔离的方式应用所提供的样式。

ViewEncapsulation.Emulated 只应用于组件的视图

ViewEncapsulation.None 全局应用样式

组件交互

父子组件传值

子组件接受父组件的传值

@Input('master') masterName = '';

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

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

@Component({
  selector: 'app-name-child',
  template: '<h3>"{{name}}"</h3>'
})
exportclassNameChildComponent {
  @Input()
  getname(): string { returnthis._name; }
  setname(name: string) {
    this._name = (name && name.trim()) || '<no name set>';
  }
  private _name = '';
}

通过onChanges()监听输入属性的变化

ngOnChanges(changes: SimpleChanges) {}

父组件传值

<child [master]="value"></child>

父组件接收子组件发出的通知

// 子组件@Output() voted = newEventEmitter<boolean>();
vote(agreed: boolean) {
    this.voted.emit(agreed);
  }
// 父组件
<child (vote)="onVote($event)"></child>
onVoted(agreed: boolean) {}

父组件获取子组件的数据方法

1.组件模板中获取

<app-countdown-timer #timer>

import { Component } from'@angular/core';
import { CountdownTimerComponent } from'./countdown-timer.component';

@Component({
  selector: 'app-countdown-parent-lv',
  template: `
    <h3>Countdown to Liftoff (via local variable)</h3>
    <button type="button" (click)="timer.start()">Start</button>
    <button type="button" (click)="timer.stop()">Stop</button>
    <div class="seconds">{{timer.seconds}}</div>
    <app-countdown-timer #timer></app-countdown-timer>
  `,
  styleUrls: ['../assets/demo.css']
})
export classCountdownLocalVarParentComponent { }

2.本地变量的方法 (组件类获取 ViewChild)

@ViewChild(CountdownTimerComponent)  private timerComponent!: CountdownTimerComponent;
start() { this.timerComponent.start(); }  
stop() { this.timerComponent.stop(); }

3.通过服务进行通讯

// 在构造函数中注入服务constructor(private missionService: MissionService) {}

组件样式

:host

:host-context

:host ::ng-deep h3 {}

内容投影

单插槽内容投影

<ng-content></ng-content>

多插槽内容投影

<ng-contentselect="[question]"></ng-content>
//父组件: 
<child><pquestion></p></child>

或者
<ng-contentselect=".question"></ng-content><child><pclass="question"></p></child>

有条件的内容投影

 

动态组件

先创建一个指令

// ad.directive.ts// viewContainerRef 获取对容器视图的访问权import { Directive, ViewContainerRef } from'@angular/core';

@Directive({
  selector: '[adHost]',
})
exportclassAdDirective {
  constructor(public viewContainerRef: ViewContainerRef) { }
}

加载组件

// ad-banner.component.ts
template: `
  <div class="ad-banner-example">
    <h3>Advertisements</h3>
    <ng-template adHost></ng-template>
  </div>
`
export classAdBannerComponentimplementsOnInit, OnDestroy {@Input() ads: AdItem[] = [];

  currentAdIndex = -1;

  @ViewChild(AdDirective, {static: true}) adHost!: AdDirective;
  interval: number|undefined;

  ngOnInit(): void {
    this.loadComponent();
    this.getAds();
  }

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

  loadComponent() {
    this.currentAdIndex = (this.currentAdIndex + 1) % this.ads.length;
    const adItem = this.ads[this.currentAdIndex];

    const viewContainerRef = this.adHost.viewContainerRef;
    viewContainerRef.clear();

    const componentRef = viewContainerRef.createComponent<AdComponent>(adItem.component);
    componentRef.instance.data = adItem.data;
  }

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

三、模板

文本插值

<p>{{title}}</p>
<div><img alt="item" src="{{itemImageUrl}}"></div

模板语句

模板上下文中的名称优先于组件上下文中的名称

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

绑定

属性绑定:<p [attr.attribute-you-are-targeting]="expression">

类和样式绑定:

绑定到单个类:[class.sale]="onSave" onSave为真时添加类

绑定到多个类: [class]="classExpression" classExpression: 数组,对象,字符串(空格分割)

绑定到单一样式: [style.background-color]="expression"

绑定到多个样式: [style]="styleExpression"

  • 样式的字符串列表,比如 "width: 100px; height: 100px; background-color: cornflowerblue;"。

  • 一个对象,其键名是样式名,其值是样式值,比如 {width: '100px', height: '100px', backgroundColor: 'cornflowerblue'}。

事件绑定:(click)="eventHandler" (keydown.shift.t)="eventHandler"

属性绑定: [src]="imgSrc" [disabled]="isDisabled"

双向绑定: [(size)] = "fontSize"

双向绑定的原理:@OutPut()属性的名称必须遵循inputChange模式,其中input是@Input()属性的名称,比如@Input()属性为size,则@Output属性必须为sizeChange

管道

使用管道去转换字符串,货币金额,日期等数据

  • DatePipe:根据本地环境中的规则格式化日期值。

  • CurrencyPipe :把数字转换成货币字符串,根据本地环境中的规则进行格式化。

  • DecimalPipe:把数字转换成带小数点的字符串,根据本地环境中的规则进行格式化。

  • PercentPipe :把数字转换成百分比字符串,根据本地环境中的规则进行格式化。

  • JsonPipe 格式化json数据

{{ birthday | date:'YYYY-MM-DD' }} 多个参数用:分割

{{ birthday | date | uppercase}}

自定义管道

指定字符串长度不能超过规定长度

// summary.pipe.tsimport { Pipe, PipeTransform } from'@angular/core';

@Pipe({
  name: 'summary'
})
exportclassSummaryPipeimplementsPipeTransform {

  transform(value: string, ...args: number[]): unknown {
    if (!value) returnnull;
    let actualLimit = args.length > 0 ? args[0] : 20;
    return value.substring(0, actualLimit) + '...';
  }

}

// app.module.tsimport { SummaryPipe } from'./summary.pipe'@NgModule({
declarations: [
SummaryPipe
]
});

模板引用变量

<input #phoneplaceholder="phone number" /><buttontype="button" (click)="callPhone(phone.value)">Call</button>
  • 如果在组件上声明变量,该变量就会引用该组件实例。

  • 如果在标准的 HTML 标记上声明变量,该变量就会引用该元素。

  • 如果你在 元素上声明变量,该变量就会引用一个 TemplateRef 实例来代表此模板

如果该变量在右侧指定了一个名字,比如 #var="ngModel",那么该变量就会引用所在元素上具有这个 exportAs 名字的指令或组件。

<form #itemForm="ngForm" (ngSubmit)="onSubmit(itemForm)"><labelfor="name">Name</label><inputtype="text"id="name"class="form-control"name="name"ngModelrequired /><buttontype="submit">Submit</button></form><div [hidden]="!itemForm.form.valid"><p>{{ submitMessage }}</p></div>

模板输入变量

<ng-template ngFor let-hero let-i="index" [ngForOf]="heroes">
    <li>Hero number {{i}}: {{hero.name}}
  </ng-template>

SVG作为模板

可生成动态交互式图形

四、指令

  • 组件

带有模板的指令。这种指令类型是最常见的指令类型。

  • 属性型指令

更改元素、组件或其他指令的外观或行为的指令

NgClass 添加和删除一组CSS类

添加或删除单个类使用类绑定

This div is special

NgStyle 添加和删除一组HTML样式

<div [ngStyle]="currentStyles">
  This div is initially italic, normal weight, and extra large (24px).
</div>    
this.currentStyles = {
    'font-style':  this.canSave      ? 'italic' : 'normal',
    'font-weight': !this.isUnchanged ? 'bold'   : 'normal',
    'font-size':   this.isSpecial    ? '24px'   : '12px'
  };

NgModel 将双向绑定数据添加到HTML表单

<input [(ngModel)]="currentItem.name" id="example-ngModel">

  • 结构型指令

通过添加和删除 DOM 元素来更改 DOM 布局

*ngIf *ngFor *ngSwitch [hidden]

<div *ngIf="nullCustomer">Hello, <span>{{nullCustomer}}</span></div><div *ngFor="let item of items; let i=index">{{i + 1}} - {{item.name}}</div><li
*ngFor="
let item of list;
let i = index;
let isEven = even;
let isOdd = odd;
let isFirst = first;
let isLast = last;
"
></li><div [hidden]="data.length == 0">课程列表</div><div [ngSwitch]="currentItem.feature"><app-stout-item    *ngSwitchCase="'stout'"    [item]="currentItem"></app-stout-item><app-device-item   *ngSwitchCase="'slim'"     [item]="currentItem"></app-device-item><app-lost-item     *ngSwitchCase="'vintage'"  [item]="currentItem"></app-lost-item><app-best-item     *ngSwitchCase="'bright'"   [item]="currentItem"></app-best-item><!-- . . . --><app-unknown-item  *ngSwitchDefault           [item]="currentItem"></app-unknown-item></div>

// *ngFor和ngFor的对比
<div
  *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById"
  [class.odd]="odd">
  ({{i}}) {{hero.name}}
</div><ng-templatengForlet-hero [ngForOf]="heroes"let-i="index"let-odd="odd" [ngForTrackBy]="trackById"><div [class.odd]="odd">
    ({{i}}) {{hero.name}}
  </div></ng-template>

当没有单个元素承载指令时,可以使用 。

自定义指令

// hightlight.directive.tsimport { Directive, ElementRef, HostListener, Input } from'@angular/core';

@Directive({
  selector: '[appHighlight]'
})
exportclassHighlightDirective {
  @Input() appHighlight = '';

  constructor(private el: ElementRef) { }

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

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

  privatehighlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }

}

// <p [appHighlight]="color">Highlight me!</p>// 与组件一样,你可以将指令的多个属性绑定添加到宿主元素上

指令组合API

@Component({
  selector: 'admin-menu',
  template: 'admin-menu.html',
  hostDirectives: [{
    directive: MenuBehavior,
    inputs: ['menuId'], // 别名  ['menuId: id'],outputs: ['menuClosed'],
  }],
})
export class AdminMenu { }

<admin-menu menuId="top-menu" (menuClosed)="logMenuClosed()">

五、依赖注入

依赖注入简称DI, 是面向对象编程中的一种设计原则,用来减少代码之间的耦合度。 在 Angular 的 DI 框架中有四个核心概念:

  1. Dependency:组件要依赖的实例对象,服务实例对象

  1. Token:获取服务实例对象的标识

  1. Injector:注入器,负责创建维护服务类的实例对象并向组件中注入服务实例对象。

  1. Provider:配置注入器的对象,指定创建服务实例对象的服务类和获取实例对象的标识。

在组件中:

@Component({
  selector: 'hero-list',
  template: '...',
  providers: [HeroService]
})

在模块中

@NgModule({
  declarations: [HeroListComponent]
  providers: [HeroService]
})

注入依赖项

@Component({ … })classHeroListComponent{
  constructor(private service: HeroService) {}
}

六、服务

创建服务

ng g s xxx/xxx

服务的作用域

  1. 在根注入器中注册服务,所有模块使用同一个服务实例对象。

 @Injectable({
    providedIn: 'root', //root表示所有模块使用
    })
  1. 在模块级别注册服务 providedIn: CarModule 或 @NgModule({ providers: [CarListService], })

  1. 在组件级别注入服务 providers: [ CarListService ]

七、表单

模板驱动

  1. 引入依赖模块FormsModule

  1. 将DOM表单转换为ngForm <form #f="ngForm" (submit)="onSubmit(f)"></form>

  1. 声明表单字段为ngModel <input type="text" name="username" ngModel>

  1. 获取表单字段值

onSubmit(form: NgForm) {
     console.log(form.value)
  }
  1. 表单分组

<form #f="ngForm" (submit) = "onSubmit(f)"><divngModelGroup="user"><inputtype="text"name="username"ngModel /></div><divngModelGroup="contact"><inputtype="text"name="phone"ngModel /></div></form>
  1. 表单验证

  • required 必填字段

  • minlength 字段最小长度

  • maxlength 字段最大长度

  • pattern 验证正则 例如:pattern="\d" 匹配一个数值

模型驱动

概念

1. FormControl: 表单项
2. FormGroup: 表单组
3. FormArray: 表单列表

快速上手

  1. 引入 ReactiveFormsModule

  1. 在组件类中创建FormGroup contactForm: FormGroup = new FormGroup({ name: new FormControl('xxx')})

  1. 关联组件模板中的表单

<form [formGroup]="contactForm"><inputformControlName="name"/></form>
  1. 获取表单值 this.cntactForm.value

表单验证器

内置表单验证器 Validators.required, minLength等 name: new FormControl('xxx', [Validators.required]) 自定义同步表单验证器

import { AbstractControl, ValidationErrors } from"@angular/forms"
export class NameValidators {
// 字段值中不能包含空格
static cannotContainSpace(control: AbstractControl): ValidationErrors | null {
// 验证未通过
if (/\s/.test(control.value)) return { cannotContainSpace: true }
// 验证通过
return null
}
}  
import { NameValidators } from"./Name.validators"
contactForm: FormGroup = newFormGroup({
name: newFormControl("", [
Validators.required,
NameValidators.cannotContainSpace
])
})

自定义异步表单验证器

import { AbstractControl, ValidationErrors } from"@angular/forms"
export class NameValidators {
static shouldBeUnique(control: AbstractControl): Promise<ValidationErrors |
null> {
returnnewPromise(resolve => {
if (control.value == "admin") {
resolve({ shouldBeUnique: true })
} else {
resolve(null)
}
})
}
}
contactForm: FormGroup = newFormGroup({
name: newFormControl("",
[Validators.required],
NameValidators.shouldBeUnique
)
})   

FormBuilder 创建表单的快捷方式

  1. this.fb.control :表单项

  1. this.fb.group :表单组,表单至少是一个 FormGroup

  1. this.fb.array :用于复杂表单,可以动态添加表单项或表单组,在表单验证时,FormArray 中有一项没通过,整体没通过。

import { FormBuilder, FormGroup, Validators } from "@angular/forms"
export classAppComponent{
contactForm: FormGroup
constructor(private fb: FormBuilder) {
this.contactForm = this.fb.group({
fullName: this.fb.group({
firstName: ["😝", [Validators.required]],
lastName: [""]
}),
phone: []
})
}
}

方法:

  1. patchValue:设置表单控件的值(可以设置全部,也可以设置其中某一个,其他不受影响)

  1. setValue:设置表单控件的值 (设置全部,不能排除任何一个)

  1. valueChanges:当表单控件的值发生变化时被触发的事件

  1. reset:表单内容置空

八、路由

路由是以模块为单位的,每个模块都可以有自己的路由

快速上手

  1. 创建组件

  1. 创建路由规则

// app.module.ts
import { Routes } from "@angular/router"
const routes: Routes = [
{
path: "home",
component: HomeComponent
},
{
path: "about",
component: AboutComponent
}
]
  1. 引入路由模块

// app.module.ts
import { RouterModule, Routes } from "@angular/router"
@NgModule({
imports: [RouterModule.forRoot(routes, { useHash: true })],
})
export class AppModule {}
  1. 添加路由插座

<router-outlet></router-outlet>

  1. 导航组件中定义链接

<a routerLink="/home">首页</a>
<a routerLink="/about">关于我们</a>

匹配规则

重定向:

{
path: "",
// 重定向
redirectTo: "home",
// 完全匹配
pathMatch: "full"
}

匹配符

{
path: "**",
component: NotFoundComponent
}

路由传参

查询参数

<a routerLink="/about" [queryParams]="{ name: 'kitty' }">关于我们</a>

import { ActivatedRoute } from "@angular/router"
export class AboutComponent implements OnInit {
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
this.route.queryParamMap.subscribe(query => {
query.get("name")
})
}
}

动态参数

// 在路由参数上体现:
path: "about/:name",
<a [routerLink]="['/about', 'zhangsan']">关于我们</a>

import { ActivatedRoute } from "@angular/router"
export class AboutComponent implements OnInit {
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
this.route.paramMap.subscribe(params => {
params.get("name")
})
}
}

导航路由

// app.component.ts
import { Router } from "@angular/router"
export class HomeComponent {
constructor(private router: Router) {}
jump() {
this.router.navigate(["/about/history"], {
queryParams: {
name: "Kitty"
}
})
}
}

路由懒加载

// app-routing.module.ts
const routes: Routes = [
{
path: "user",
loadChildren: () => import("./user/user.module").then(m => m.UserModule)
}
]

路由守卫

路由可以应用多个守卫,所有守卫方法都允许,路由才被允许访问,有一个守卫方法不允许,则路由不

允许被访问

canActivate

检查用户是否可以访问某一个路由

创建路由守卫 ng g guard guards/auth

import { Injectable } from "@angular/core"
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree,
Router } from "@angular/router"
@Injectable({providedIn: "root"})
export class AuthGuard implements CanActivate {
constructor(private router: Router) {}
canActivate(): boolean | UrlTree {
// 用于实现跳转
return this.router.createUrlTree(["/user/login"])
// 禁止访问目标路由
return false
// 允许访问目标路由
return true
}
}

canActivateChild

检查用户是否方可访问某个子路由。

创建路由守卫: ng g guard guards/admin 注意:选择 CanActivateChild

import { Injectable } from "@angular/core"
import { CanActivateChild, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree
} from "@angular/router"
@Injectable({providedIn: "root"})
export class AdminGuard implements CanActivateChild {
canActivateChild(): boolean | UrlTree {
return true
}

canDeactivate

检查用户是否可以退出路由。比如用户在表单中输入的内容没有保存,用户又要离开路由,此时可以调

用该守卫提示用户。

import { Injectable } from "@angular/core"
import {
CanDeactivate,
ActivatedRouteSnapshot,
RouterStateSnapshot,
UrlTree
} from "@angular/router"
import { Observable } from "rxjs"
export interface CanComponentLeave {
canLeave: () => boolean
}
@Injectable({
providedIn: "root"
})
export class UnsaveGuard implements CanDeactivate<CanComponentLeave> {
canDeactivate(component: CanComponentLeave): boolean {
if (component.canLeave()) {
return true
}
return false
}
}

Resolve

允许在进入路由之前先获取数据,待数据获取完成之后再进入路由

ng g resolver <name>

import { Injectable } from "@angular/core"
import { Resolve } from "@angular/router"
type returnType = Promise<{ name: string }>
@Injectable({
providedIn: "root"
})
export class ResolveGuard implements Resolve<returnType> {
resolve(): returnType {
return new Promise(function (resolve) {
setTimeout(() => {
resolve({ name: "张三" })
}, 2000)
})
}
}

// 获取到的数据在组件中使用
export class HomeComponent {
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
console.log(this.route.snapshot.data.user)
}
}

在路由表中,路由守卫的写法:

{
path: "about",
component: AboutComponent,
canActivate: [AuthGuard],
canActivateChild: [AdminGuard],
canDeactivate: [UnsaveGuard],
resolve: {
user: ResolveGuard
},
children: [
{
  path: 'test',
  ...
}
]
}

九、RxJS

RxJS 是一个用于处理异步编程的 JavaScript 库,目标是使编写异步和基于回调的代码更容易。

基本概念

  • Observable:可观察对象,是一个可调用的未来值或事件的集合

  • Observe:观察者,一个回调函数的集合,它知道如何去监听由Observable提供的值

  • Subscription:订阅,主要用于取消Observable的执行

  • Operators: 操作符,采用函数式编程风格的纯函数

  • Subject:主体,相当于EventEmitter,并且是将值或事件多路推送给多个Observer的唯一方式

  • Schedulers:调度器,用来控制并发并且是中央集权的调度员,允许我们在发生计算时进行协调,比如setTimeout,requestAnimationFrame等

可观察对象

Observable

Observable对象内部可多次调用next方法向外发送数据

可使用complete方法停止数据的发送

可使用error方法发送错误消息

可观察对象是惰性的,只有被订阅才会执行

可观察对象可以有很多的订阅者

const observable = new Observable(observer => {
   let index = 0;
   let timer = setInterval(() => {
        observer.next(index++);
        if(index === 3) {
           observer.complete()
           // 发生错误 发送错误消息
           // observer.error('xxx')  clearInterval(timer)
        }
   }, 1000)
})

const observer = {
  next: function(value) {
    console.log(value)
  },
  complete: function() { // 数据发送完成}
}

observable.subscribe(observer)

Subject

创建空的可观察对象,在订阅后不会立即执行,next方法可以在可观察对象外部调用

每个 Subject 都是观察者

每个 Subject 都是 Observable 。

这个 Subject 可能有多个订阅者,然而普通的 “单播 Observable” 只发送通知给单个观察者。

import { Subject } from "rxjs"
const demoSubject = new Subject()
demoSubject.subscribe({next: function (value) {console.log(value)}})
demoSubject.subscribe({next: function (value) {console.log(value)}})
setTimeout(function () {
demoSubject.next("hahaha")
}, 3000)

BehaviorSubject

拥有Subject的全部功能,但是在创建Observable对象时可传默认值,观察者订阅后可以直接拿到默认值

import { BehaviorSubject } from "rxjs"
const demoBehavior = new BehaviorSubject("默认值")
demoBehavior.subscribe({next: function (value) {console.log(value)}})
demoBehavior.next("Hello")

ReplaySubject

功能类似 Subject,但有新订阅者时两者处理方式不同,Subject 不会广播历史结果,而 ReplaySubject

会广播所有历史结果。

import { ReplaySubject } from "rxjs"
const rSubject = new ReplaySubject()
rSubject.subscribe(value => {
console.log(value)
})
rSubject.next("Hello 1")
rSubject.next("Hello 2")
setTimeout(function () {
rSubject.subscribe({next: function (value) {console.log(value)}})
}, 3000)

辅助方法

of:参数列表作为数据流返回

of("a", "b", [], {}, true, 20).subscribe(v => console.log(v))

form:将Array,Promise,Iterator转为Observable对象

import { from } from "rxjs"
function p() {
return new Promise(function (resolve) {
resolve([100, 200])
})
}
from(p()).subscribe(v => console.log(v)) 
// [100, 200]

interval,timer

timer:间隔时间过去以后发出数值,行为终止,或间隔时间发出数值后,继续按第二个参数的时间间

隔继续发出值 timer(1000,2000)

zip 组合数据

将多个 Observable 中的数据流进行组合。和将来最新的进行组合。

import { zip, of } from "rxjs"
import { map } from "rxjs/operators"
let age = of(27, 25, 29)
let name = of("Foo", "Bar", "Beer")
let isDev = of(true, true, false)
zip(name, age, isDev)
.pipe(map(([name, age, isDev]) => ({ name, age, isDev })))
.subscribe(console.log)
// { name: 'Foo', age: 27, isDev: true }
// { name: 'Bar', age: 25, isDev: true }
// { name: 'Beer', age: 29, isDev: false }

forkJoin 类似Promise.all

forkJoin 是 Rx 版本的 Promise.all(),即表示等到所有的 Observable 都完成后,才一次性返回值。

import axios from "axios"
import { forkJoin, from } from "rxjs"
axios.interceptors.response.use(response => response.data)
forkJoin({
goods: from(axios.get("http://localhost:3005/goods")),
category: from(axios.get("http://localhost:3005/category"))
}).subscribe(console.log)

retry:重试次数

如果 Observable 对象抛出错误,则该辅助方法会重新订阅 Observable 以获取数据流,参数为重新订

阅次数。

import { interval, of, throwError } from "rxjs"
import { mergeMap, retry } from "rxjs/operators"
interval(1000)
.pipe(
mergeMap(val => {
if (val > 2) {
return throwError("Error!")
}
return of(val)
}),
retry(2)
)
.subscribe({
next: console.log,
error: console.log
})

race

接收并同时执行多个可观察对象,只将最快发出的数据流传递给订阅者。

import { race, timer } from "rxjs"
import { mapTo } from "rxjs/operators"
const obs1 = timer(1000).pipe(mapTo("fast one"))
const obs2 = timer(3000).pipe(mapTo("medium one"))
const obs3 = timer(5000).pipe(mapTo("slow one"))
race(obs3, obs1, obs2).subscribe(console.log)

fromEvent

将事件转换为 Observable。

import { fromEvent } from "rxjs"
const btn = document.getElementById("btn")
// 可以将 Observer 简写成一个函数,表示 next
fromEvent(btn, "click").subscribe(e => console.log(e))

操作符

当操作符被调用时,它们不会改变已经存在的 Observable 实例。相反,它们返回一个新的 Observable

操作符本质上是一个纯函数 (pure function),它接收一个 Observable 作为输入,并生成一个新的 Observable 作为输出。

操作符分类

创建操作符
  • ajax

转换操作符
过滤操作符
组合操作符
多播操作符
错误处理操作符
工具操作符
  • finally

  • let

条件布尔操作符
数学和聚合操作符

十、HttpClientModule

快速开始

// 引入模块
import { httpClientModule } from '@angular/common/http';
imports: [
httpClientModule
]
//注入http服务在component里
// app.component.ts
import { HttpClient } from '@angular/common/http';
export class AppComponent {
constructor(private http: HttpClient) {}
}
// 发送请求
import { HttpClient } from "@angular/common/http"
export class AppComponent implements OnInit {
constructor(private http: HttpClient) {}
ngOnInit() {
this.getUsers().subscribe(console.log)
}
getUsers() {
return this.http.get("https://jsonplaceholder.typicode.com/users")
}
}

请求方法

this.http.get(url [, options]);
this.http.post(url, data [, options]);
this.http.delete(url [, options]);
this.http.put(url, data [, options]);

请求参数

使用HttpParams类

import { HttpParams } from '@angular/common/http';
let params = new HttpParams({ fromObject: {name: "zhangsan", age: "20"}})
params = params.append("sex", "male")
let params = new HttpParams({ fromString: "name=zhangsan&age=20"})

// 类定义
export declare class HttpParams {
constructor(options?: HttpParamsOptions);
has(param: string): boolean;
get(param: string): string | null;
getAll(param: string): string[] | null;
keys(): string[];
append(param: string, value: string): HttpParams;
set(param: string, value: string): HttpParams;
delete(param: string, value?: string): HttpParams;
toString(): string;
}
declare interface HttpParamsOptions {
fromString?: string;
fromObject?: {
[param: string]: string | ReadonlyArray<string>;
};
encoder?: HttpParameterCodec;
}

请求头

使用HttpHeaders类

let headers = new HttpHeaders({ test: "Hello" })
// 类定义
export declare class HttpHeaders {
constructor(headers?: string | {
[name: string]: string | string[];
});
has(name: string): boolean;
get(name: string): string | null;
keys(): string[];
getAll(name: string): string[] | null;
append(name: string, value: string | string[]): HttpHeaders;
set(name: string, value: string | string[]): HttpHeaders;
delete(name: string, value?: string | string[]): HttpHeaders;
}

响应内容

declare type HttpObserve = 'body' | 'response';
// response 读取完整响应体
// body 读取服务器端返回的数据

this.http.get(
"https://jsonplaceholder.typicode.com/users",
{ observe: "body" }
).subscribe(console.log)

拦截器

拦截器是 Angular 应用中全局捕获和修改 HTTP 请求和响应的方式。(Token、Error)

拦截器将只拦截使用 HttpClientModule 模块发出的请求。

ng g interceptor <name>

请求拦截器

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor() {}
// 拦截方法
intercept(
// unknown 指定请求体 (body) 的类型
request: HttpRequest<unknown>,
next: HttpHandler
// unknown 指定响应内容 (body) 的类型
): Observable<HttpEvent<unknown>> {
// 克隆并修改请求头
const req = request.clone({
setHeaders: {
Authorization: "Bearer xxxxxxx"
}
})
// 通过回调函数将修改后的请求头回传给应用
return next.handle(req)
}
}

响应拦截器

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor() {}
// 拦截方法
intercept(
request: HttpRequest<unknown>,
next: HttpHandler
): Observable<any> {
return next.handle(request).pipe(
retry(2),
catchError((error: HttpErrorResponse) => throwError(error))
)
}
}

拦截器注入

import { AuthInterceptor } from "./auth.interceptor"
import { HTTP_INTERCEPTORS } from "@angular/common/http"
@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
]
})

Proxy

// 在项目的根目录下创建 proxy.conf.json 文件并加入如下代码
{
"/api/*": {
"target": "http://localhost:3070",
"secure": false,
"changeOrigin": true
}
}

// secure:如果服务器端 URL 的协议是 https,此项需要为 true
// changeOrigin:如果服务器端不是 localhost, 此项需要为 true

// 指定proxy配置文件
"scripts": {
"start": "ng serve --proxy-config proxy.conf.json",
}
// 或
"serve": {
"options": {
"proxyConfig": "proxy.conf.json"
},

NgRx状态管理器

1. 下载 NgRx

npm install @ngrx/store @ngrx/effects @ngrx/entity @ngrx/router-store

@ngrx/store-devtools @ngrx/schematics

2. 配置 NgRx CLI

ng config cli.defaultCollection @ngrx/schematics

// angular.json
"cli": {
"defaultCollection": "@ngrx/schematics"
}

3. 创建 Store

ng g store State --root --module app.module.ts --statePath store --stateInterface

AppState

import { isDevMode } from '@angular/core';
import {
  ActionReducer,
  ActionReducerMap,
  createFeatureSelector,
  createSelector,
  MetaReducer
} from '@ngrx/store';
import * as fromList from './reducers/list.reducer';


export interface AppState {

  [fromList.listFeatureKey]: fromList.State;
}

export const reducers: ActionReducerMap<AppState> = {

  [fromList.listFeatureKey]: fromList.reducer,
};


export const metaReducers: MetaReducer<AppState>[] = isDevMode() ? [] : [];

4. 创建 Action

ng g action store/actions/counter --skipTests

import { createAction, props } from '@ngrx/store';

export const addList = createAction('addList', props<{ value: string}>());
export const deleteList = createAction('deleteList', props<{ id: string }>());

5. 创建 Reducer

ng g reducer store/reducers/counter --skipTests --reducers=../index.ts

import { createReducer, on } from '@ngrx/store';
import { addList, deleteList } from '../actions/list.actions';
import { v4 as uuidv4} from 'uuid';

export interface ToDo {
  id: string,
  value: string
}
export interface State {
  list: ToDo[]
}
export const listFeatureKey = 'list';


export const initialState: State = {
  list: [
    {
      id: '111', value: '122'
    },
    { id: 'ssd', value: 'dsds'}
  ]
};

export const reducer = createReducer(
  initialState,
  on(addList, (state, action) => {
    return {
      ...state,
      list: [
        ...state.list,
        {
          id: uuidv4(),
          value: action.value
        }
      ]
    }
  }),
  on(deleteList, (state, action) => {
    const index = state.list.findIndex(i => i.id === action.id);
    let newList = [...(state.list)]
    newList.splice(index, 1)
    return {
      ...state,
      list: newList
    }
  })
);

6.创建 Selector

ng g selector store/selectors/counter --skipTests

import { createFeatureSelector, createSelector } from '@ngrx/store';
import { AppState } from '..';
import { listFeatureKey, State } from '../reducers/list.reducer';

export const selectList = createFeatureSelector<AppState, State>(listFeatureKey)
export const selectLists = createSelector(selectList, state => state.list)

7.组件使用

import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { fromEvent, Observable } from 'rxjs';
import { AppState } from 'src/app/store';
import { selectLists } from 'src/app/store/selectors/list.selectors';
import { filter, map } from 'rxjs/operators'
import { addList, deleteList } from 'src/app/store/actions/list.actions';
import { slide, todoAnimations } from 'src/app/animations';
@Component({
  selector: 'app-list',
  templateUrl: './list.component.html',
  styles: [
  ],
  animations: [slide, todoAnimations]
})
export class ListComponent implements AfterViewInit {
  @ViewChild('todoInput') todoInput!: ElementRef;
  todos: Observable<{id: string, value: string}[]>
  constructor(private store: Store<AppState>) {
    this.todos = this.store.pipe(select(selectLists))
  }
  
  removeItem(id: string) {
    console.log('id', id);
    this.store.dispatch(deleteList({ id }))
  };
  ngAfterViewInit(): void {
    fromEvent<KeyboardEvent>(this.todoInput.nativeElement, "keyup").pipe(
      filter(event => event.key === "Enter"),
      map(event => (<HTMLInputElement>event.target).value),
      map(value => value.trim()),
      filter(value => value !== "")
    ).subscribe(value => {
      console.log('input', this.todoInput)
      this.store.dispatch(addList({ value: value }))
      this.todoInput.nativeElement.value = ''
    })
  }
}

8.模板

<div class="container">
  <h2>TODO</h2>
  <div class="form-group">
    <input #todoInput type="text" class="form-control" placeholder="add todo" />
  </div>
  <ul class="list-group">
    <li
      @slide
      *ngFor="let item of todos | async; let i = index;"
      (click)="removeItem(item.id)"
      class="list-group-item"

    >
      {{ item.value }}
    </li>
  </ul>
</div>

动画

定义

:enter | void => * 进入动画

:leave | * => void离开动画

trigger 方法用于创建动画,指定动画名称

transition 方法用于指定动画的运动状态,出场动画或者入场动画,或者自定义状态动画。

style 方法用于设置元素在不同的状态下所对应的样式

animate 方法用于设置运动参数,比如动画运动时间,延迟事件,运动形式

keyFrames 关键帧动画

transition(":leave", [
animate(
600,
keyframes([
style({ offset: 0.3, transform: "translateX(-80px)" }),
style({ offset: 1, transform: "translateX(100%)" })
])
)
])

query查询元素

group 动画并行

stagger 在多个元素同时执行同一个动画时,让每个元素动画的执行依次延迟。

transition(":enter", [
group([
query("h2", [
style({ transform: "translateY(-30px)" }),
animate(300)
]),
query("@slide", stagger(200, animateChild()))
])
])

state 定义状态

trigger("expandCollapse", [
// 使用 state 方法定义折叠状态元素对应的样式
state(
"collapsed",
style({
height: 0,
overflow: "hidden",
paddingTop: 0,
paddingBottom: 0
})
),
// 使用 state 方法定义展开状态元素对应的样式
state("expanded", style({ height: "*", overflow: "auto" })),
// 定义展开动画
transition("collapsed => expanded", animate("400ms ease-out")),
// 定义折叠动画
transition("expanded => collapsed", animate("400ms ease-in"))
])

举例

和NgRx例子一样,这里给出动画代码

// animation.ts
import {
  animate,
  animateChild,
  animation,
  group,
  keyframes,
  query,
  style,
  transition,
  trigger,
  useAnimation,
  stagger,
} from "@angular/animations";

const slideUp = animation([
  style({ opacity: 0, transform: "translateY(40px)" }),
  animate(250),
]);

const slideOut = animation([
  //   animate(
  //     600,
  //     keyframes([
  //       style({ offset: 0.3, transform: "translateX(-80px)" }),
  //       style({ offset: 1, transform: "translateX(100%)" }),
  //     ])
  //   ),
  animate(600, style({ opacity: 0, transform: "translateX(100%)" })),
]);
export const slide = trigger("slide", [
  transition(":enter", useAnimation(slideUp)),
  transition(":leave", useAnimation(slideOut)),
]);

export const todoAnimations = trigger("todoAnimations", [
  transition(":enter", [
    group([
      query("h2", [style({ transform: "translateY(-30px)" }), animate(300)]),
      query("@slide", stagger(200, animateChild()), { optional: true }),
    ]),
  ]),
]);