Angular的进阶教程2 - (依赖注入 + RxJS)

前篇教程请参考 Angular进阶教程1 - (路由 + 表单)

依赖注入与HTTP的介绍

为什么使用服务?

\color{#0abb3c}{组件}不应该直接获取或保存数据,它们应该聚焦于展示数据,而把数据访问和处理的职责委托给某个\color{#0abb3c}{服务}。那面对组件和服务之间的关系,该如何处理他们之间的依赖关系呢?Angular就引入了\color{#0abb3c}{依赖注入框架}去解决这件事情。

依赖注入(DI)

依赖项( 服务/对象 )注入是一种设计模式,在这种设计模式中,类会从外部源\color{#0abb3c}{请求依赖项}而不是创建它们。Angular 的 DI 框架会在\color{#0abb3c}{实例化}某个类时为其提供依赖,从而提高模块性和灵活性。在学习依赖注入之前我们先来了解一下关于依赖注入中比较核心的三个概念:

  • 注入器(Injector):提供了一系列的接口用于创建依赖对象的实例。 (可以想象成是一个厨师做菜)

  • Provider:用于配置注入器,注入器通过它来创建被依赖对象的实例。Provider把标识(Token)映射到列表对象,同时还提供了一个运行时所需的依赖,被依赖的对象就是通过该方法来创建的。(可以想象成厨师手中的菜谱,其中Token就是菜名)

  • 依赖(Dependence):指定了被依赖对象的类型,注入器会根据此类型创建对应的对象。

[图片上传失败...(image-7f08d6-1639566693325)]

依赖注入的使用

  1. 创建可注入服务:
import { Injectable } from '@angular/core';

// @Injectable()装饰器,是告诉Angular这是一个可供注入的服务,该注入器主要负责创建服务实例,并把他注入到类中, 元数据providedIn: 'root' 表示 HeroService在整个应用程序中都是可见的。
@Injectable({
  providedIn: 'root',
})
export class GoodsListService {
  constructor() { }
}

如果所创建的服务不依赖于其他服务,是可以不用使用 Injectable 类装饰器。但当该服务需要在构造函数中注入依赖对象,就需要使用Injectable 装饰器。不过我们在开发过程中一般都会加上这个装饰器。

  1. 注入服务
    将依赖项(服务)注入到组件的constructor()中
constructor(goodsListService: GoodsListService)

注入服务的常见方式

在组件中注入服务

如果你在\color{#0abb3c}{组件中}\color{#0abb3c}{元数据}上定义了providers,那么angular会根据providers为这个组件创建一个注入器,这个\color{#0abb3c}{组件的子组件}也会\color{#0abb3c}{共享}这个注入器,如果没有定义,那么组件会根据组件树\color{#0abb3c}{逐级向上}查找合适的注入器来创建组件的依赖。

// 这种方式注册,会注册到每个组件实例自己的注入器上。(多个组件会有多个注入器)
@Component({ 
 selector: 'app-goods-list',
 providers: [ GoodsListService ]
})

其实这种引入方式只是一种简写,不过也是一种常用的写法,真正的完整版本是:
@Component({ 
 selector: 'app-goods-list',
 providers: [{ provide: GoodsListService, useClass: GoodsListService } ]
 // 其中provide属性可以理解为这个Provider的唯一标识,用于定位依赖值,也就是应用中使用的服务名
 // 而useClass属性则代表使用哪个服务类来创建实例
})

在模块中注入服务

\color{#0abb3c}{根组件}中注入的服务,在所有的\color{#0abb3c}{子组件}中都能\color{#0abb3c}{共享}这个服务,当然在\color{#0abb3c}{模块}\color{#0abb3c}{注入服务}也可以达到相同的结果,需要我们通过\color{#0abb3c}{imports}导入了外来模块,那么外来模块的服务就都注入到了你所在模块的\color{#0abb3c}{injectors}

补充上述原因: 因为Angular在启动程序时会启动一个根模块,并加载它所依赖的其他模块,此时会生成一个全局的根注入器,由该注入器创建的依赖注入对象在整个应用程序级别可见,并共享一个实例。所以说在Angular中并没有模块级别的区域,只有组件级别应用级别的区域。模块级别的注入就相当于是应用级别

// 这种方式注册,可以对服务进行一些额外的配置(服务类中也需要写@Injectable()装饰器)。
// 在未使用路由懒加载的情况下,这种注入的方式和在服务类中注入的方式是一样的。

@NgModule({ 
 providers: [ GoodsListService ],
})

注意的点: 虽然在模块中注入的依赖相当于是应用级别的,但是当遇到路由懒加载的时候,会出现一种特殊情况,Angular会对延迟加载模块初始化一个新的执行上下文,并创建一个新的注入器,在该注入器中注入的依赖只在该模块内部可见,这算是一个特殊的模块级作用域

在服务类中注入服务

// 这种注入方式,会告诉Angular在根注入器中注册这个服务,这也是使用CLI生成服务时默认的方式.
// 这种方式注册,不需要再@NgModule装饰器中写providers,而且在代码编译打包时,可以执行tree shaking优化,会移除所有没在应用中使用过的服务。推荐使用此种方式注册服务.

@Injectable({
    providedIn: 'root'
})

根组件还是在子组件中进行服务注入,该怎么选择呢?

这取决于想让注入的依赖服务具有全局性还是局部性

依赖对象的创建方式有四种(仅了解):

  • useClass: 基于标识来指定依赖项

  • useValue: 依赖对象不一定是类,也可以是常量、字符串、对象等其他数据类型

  • useExisting: 就可以在一个Provider中配置多个标识,他们对应的对象指向同一个实例,从而实现多个依赖、一个对象实例的作用

  • useFactory: 动态生成依赖对象

Http的介绍

大多数前端应用都要通过 HTTP 协议与服务器\color{#0abb3c}{通讯},才能下载或上传数据并访问其它后端服务。Angular 给应用提供了一个 HTTP 客户端 API,也就是 \color{#0abb3c}{@angular/common/http} 中的 \color{#0abb3c}{HttpClient} 服务类。

使用HttpClient

  1. 一般会在根模块下导入HttpClient
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
// 导入HttpClientModule
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
  ],
  exports: [],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
  1. 在服务类中依赖注入 (需要在服务类中通过HttpClient去进行通讯)
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable()

export class GoodsListService {
  constructor(private http: HttpClient) { }
}
  1. 使用\color{#0abb3c}{HttpClient} 返回的都是可观察对象(observable)类型的服务。因此我们还需要在服务类中导入RxJS \color{#0abb3c}{可观察对象}和可能会使用到的\color{#0abb3c}{操作符}
import { Observable } from 'rxjs';
import { pluck } from 'rxjs/operators'; // 此操作符是用来获取某个字段内容

常用的请求方式

  1. 从服务器请求数据 HttpClient.get()
  // 在服务类中去封装和服务端通讯的方法
  
  public getHttpResult(code: string, name: string): Observable<any> {
    const url: string = '';  // 这是请求的地址
    return this._http.get(url, { params: { code, name } });
  }
  1. 发送数据到服务器 HttpClient.post()
  public postHttpResult(body: any): Observable<any> {
    const url: string = ''; // 这是请求的地址
    return this._http.post(url, body);
  }

错误处理

在调用接口的时候,当遇到接口请求失败或者报错的时候,前端需要做一些错误的提示信息展示,具体操作如下:

    this._goodsListService.getHttpResult('12', 'zs')
    .subscribe((res) => { // 由于httpClient返回的是observable,他必须被订阅之后才可以执行并返回结果
      console.log(res);
    }, (error) => { // 这里是接口报错的处理错误的地方
      console.log(error);
    });

RxJS的实战介绍

什么是RxJS

首先RxJS是一个库,是针对\color{#0abb3c}{异步数据流}编程工具,当然Angular引入RxJS就是让异步更加简单,更加可控,在开始RxJS之前,我们先来了解一下Reactive Programming,其本质就是使用\color{#0abb3c}{流(stream)}的一种编程方式。

什么是流呢?

所谓\color{#0abb3c}{流/stream},就是数据基于事件(event)变化的整体。stream = data + event,不过我们可以通过河流来更直观的理解一下流,首先河流是有\color{#0abb3c}{流向}的,所以流也是有流向的,一条河流可以分成很多支流,很多小的支流也可以汇总成一条河流,所以在RxJS中,流也可以使用\color{#0abb3c}{操作符}实现流的\color{#0abb3c}{汇总}\color{#0abb3c}{分流}

RxJS中的核心概念(Observable 、Observer 、Subscription、Subject)

在Angular项目中我们在调用接口的时候,常用的调用方式是:

this._goodsListService.getHttpResult
       .subscribe((res) => {
         console.log(res)
       })
 //this._goodsListService.getHttpResult就是返回observable,他可以是api的调用,可以是事件的调用等等

我们可以把上述的调用方式抽象一下为\color{#0abb3c}{observable.subscribe(observer)}在这里我们认识到了两个新的事物分别是Observable和Observer,以及这个方法调用的返回对象,返回的是一个Subscription对象的实例化,接下来我们逐一介绍这些核心概念。

Observable

Observable是RxJS中最核心的一个概念,它的本质就是“Observable is a function to generate values”,首先它是一个\color{#0abb3c}{函数},也就是说它是\color{#0abb3c}{数据源头,是数据生产者},一般我们会在变量末尾加$表示Observable类型的对象。

// 此函数定义了setInterval 每两秒产生一个 value的功能
const observable$ = (observer) => {
    let counter = 0;
    const id = setInterval(() => observer.next(counter++), 2000);
}
// 因为Observable是个对象,所以需要调用才可以执行
observable$({ next: (val) => console.log(val) });

函数中会定义 value 的生成方式,函数调用时,observer.next 来执行在observer 中定义的行为,比如上述示例中的counter++。从中我们可以发现observable的一些特性,如下所示:

  1. 必须被调用(订阅)才会被执行
  2. observable 被调用后,必须能被关闭,否则会一直运行下去
  3. 对于同一个observable,在不同的地方subscribe,是无关的。这和function执行多次,互相没有关联是一致的。

Observer(了解)

它是\color{#0abb3c}{观察者,数据使用者,数据消费者}。它是一个有三个回调函数的\color{#0abb3c}{对象},每个回调函数对应三种Observable发送的通知类型(next, error, complete),observer表示的是对序列结果的\color{#0abb3c}{处理方式}。在实际开发中,如果我们提供了\color{#0abb3c}{一个回调函数}作为参数,subscribe会将我们提供的函数参数作为\color{#0abb3c}{next}的回调处理函数。next决定传递一个什么样的数据给观察者。

let observer = {
 next: data => console.log('data'); // next表示数据正常流动,
 error: err=> console.log('err'); // error表示流中出错
 complete: () => console.log('complete') // complete表示流结束
}
// error和complete只会触发一个,但是可以有多个next

Subject

Subject是\color{#0abb3c}{特殊的observable}:我们可以像订阅任何observable一样去订阅subject。
Subject是\color{#0abb3c}{观察者}: 它有next(v),error(e),和complete()方法,如果我们需要给subject提供新值,只要调用<font color="#13c078">next(v)</font>,它会将值多播给已注册监听该subject的观察者。

所以: Subject既是Observable,也是观察者(可以多个)

Subject与Observable的区别:
  • Subject是\color{#0abb3c}{多播的}【他可以将值多播给多个观察者】
  • 普通的Observble是\color{#0abb3c}{单播的}【每个已经订阅的观察者(observer)都拥有observable的独立执行,上述Observble的介绍也有提及】
Subject的在Angular中的常见的作用:

可以在Angular通过service来实现不同组件,或者不同模块之间的传值

// 定义公共的用于数据存储的service,文件名是(eg:xampleStore.service.ts)
    @Injectable()
     export class ExampleStoreService {
      private currentTabNumber$ = new Subject<number>();
    }
        
// 此数据更改的逻辑,可以在任何需要更改的地方进行next相对应的值,文件名是 (eg:a.component.ts)
   this.ExampleStoreService.currentTabNumber$.next(1);
   
// 订阅接收到数据更改,并做下一步逻辑处理,文件名是(eg:b.component.ts)
  this.ExampleStoreService.currentTabNumber$
        .subscribe((res: number) => {
          this.currentIndex = res;
        })

RxJS的操作符(Operator)简介

operators是个\color{#0abb3c}{纯函数},它的输入为observable,返回也observable。operators的本质是,描述从一个数据流到另一个数据流之间的关系,也就是observer到observable中间发生的转换,很类似于Lodash。
在RxJS中操作符有接近100个,不过在开发过程常用的也就十多个。

常见的运算符包含 map, filter, concat, flatmap, switchmap, forkjoin
在这里我们只调挑出forkJoinswitchMap来讲解一下,其他的操作符可以自己去查阅。

// 当用户不关心接口的返回顺序
// 使用forkjoin主要是用于多个接口同时返回的时候,才会返回结果
    forkJoin([
      this._goodsListService.getHttpResultOne('12', 'zs'),
      this._goodsListService.getHttpResultTwo('12', 'zs')])
      .subscribe(resArr => {
      // 此时的返回结果会被按顺序放在一个数组中
      const oneData = resArr[0];
      const TwoData = resArr[1];
   });
   // 当用户关心接口的返回顺序时
   // 使用switchMap可以保证先返回getHttpResultOne的接口数据,然后在返回getHttpResultTwo的结果
   this._goodsListService.getHttpResultOne('12', 'zs')
   .pipe(
    switchMap((resultOne: any) => {
      console.log(resultOne);
      return this._goodsListService.getHttpResultTwo('12', 'zs');
    })
   )
   .subscribe((resultTwo: any) => {
     console.log(resultTwo);
   });

如何在项目中取消订阅

根据observabled的特性 “observable 被调用后,必须能被关闭,否则会一直运行下去”,所以我们在组件中\color{#0abb3c}{使用subscribe}订阅的observabled,都需要在组件的\color{#0abb3c}{销毁}阶段被\color{#0abb3c}{取消订阅},从而来优化项目的性能和效率。具体取消订阅的有如下三个步骤:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';

export class GoodsListComponent implements OnInit, OnDestroy {
    // ① 首先定义一个Subscription类型的数组,用来存放这个组件中所有订阅的observabled
    public subscriptions: Subscription[] = [];
     constructor( private _goodsListService: GoodsListService ) {}
    
    public ngOnInit(): void {
       // ② 将订阅的observabled,push到定义好的Subscription类型的数组的数组中,等待统一取消订阅
        this.subscriptions.push(
            this._goodsListService.getHttpResultOne('12', 'zs')
            .subscribe((res) => {
              // console.log(res);
            }, (error) => {
              // console.log(error);
            });
        );
    }
    
   public ngOnDestroy(): void {
   // ③在组件的销毁阶段,将数组中的订阅的observabled通过调用unsubscribe()方法进行取消订阅
    this.subscriptions.forEach((subscription) => subscription.unsubscribe());
  }
    
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容