# 初始化项目

一般使用 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 绑定属性生成一个绑定的一个树状结构(就是哪个属性绑定哪个值的一种对象结构,绑定名,绑定表达式),从根组件到子组件。对于检查是单向的数据流。

avatar

对于组件的声明周期中。也有脏值检测过程。我们一般是在更新 DOM 之前进行数值属性的绑定,以避免引起无线循环的脏值检测。

avatar

如下代码,会导致无限循环,抛出异常:

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()
  }
更新: 4/18/2021, 5:09:31 PM