Angular进阶教程2-<依赖注入+RxJS>

2021-12-08 12:49:02 浏览数 (2)

依赖注入与HTTP的介绍

为什么使用服务?

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

依赖注入(DI)

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

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

依赖注入的使用

  1. 创建可注入服务:
代码语言:javascript复制
import { Injectable } from '@angular/core';

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

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

  1. 注入服务

将依赖项(服务)注入到组件的constructor()中

代码语言:javascript复制
constructor(goodsListService: GoodsListService)
复制代码

注入服务的常见方式

在组件中注入服务

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

代码语言:javascript复制
// 这种方式注册,会注册到每个组件实例自己的注入器上。(多个组件会有多个注入器)
@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}{注入服务}注入服务也可以达到相同的结果,需要我们通过importscolor{#0abb3c}{imports}imports导入了外来模块,那么外来模块的服务就都注入到了你所在模块的injectorscolor{#0abb3c}{injectors}injectors

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

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

@NgModule({ 
 providers: [ GoodsListService ],
})
复制代码

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

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

@Injectable({
    providedIn: 'root'
})
复制代码

根组件还是在子组件中进行服务注入,该怎么选择呢? 这取决于想让注入的依赖服务具有全局性还是局部性

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

  • useClass: 基于标识来指定依赖项
  • useValue: 依赖对象不一定是类,也可以是常量、字符串、对象等其他数据类型
  • useExisting: 就可以在一个Provider中配置多个标识,他们对应的对象指向同一个实例,从而实现多个依赖、一个对象实例的作用
  • useFactory: 动态生成依赖对象

Http的介绍

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

使用HttpClient
  1. 一般会在根模块下导入HttpClient
代码语言:javascript复制
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去进行通讯)
代码语言:javascript复制
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable()

export class GoodsListService {
  constructor(private http: HttpClient) { }
}
复制代码
  1. 使用HttpClientcolor{#0abb3c}{HttpClient}HttpClient 返回的都是可观察对象(observable)类型的服务。因此我们还需要在服务类中导入RxJS 可观察对象color{#0abb3c}{可观察对象}可观察对象和可能会使用到的操作符color{#0abb3c}{操作符}操作符。
代码语言:javascript复制
import { Observable } from 'rxjs';
import { pluck } from 'rxjs/operators'; // 此操作符是用来获取某个字段内容
复制代码
常用的请求方式
  1. 从服务器请求数据 HttpClient.get()
代码语言:javascript复制
  // 在服务类中去封装和服务端通讯的方法
  
  public getHttpResult(code: string, name: string): Observable<any> {
    const url: string = '';  // 这是请求的地址
    return this._http.get(url, { params: { code, name } });
  }
复制代码
  1. 发送数据到服务器 HttpClient.post()
代码语言:javascript复制
  public postHttpResult(body: any): Observable<any> {
    const url: string = ''; // 这是请求的地址
    return this._http.post(url, body);
  }
复制代码
错误处理

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

代码语言:javascript复制
    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,其本质就是使用流(stream)color{#0abb3c}{流(stream)}流(stream)的一种编程方式。

什么是流呢?

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

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

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

代码语言:javascript复制
this._goodsListService.getHttpResult
       .subscribe((res) => {
         console.log(res)
       })
 //this._goodsListService.getHttpResult就是返回observable,他可以是api的调用,可以是事件的调用等等
复制代码

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

Observable

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

代码语言:javascript复制
// 此函数定义了setInterval 每两秒产生一个 value的功能
const observable$ = (observer) => {
    let counter = 0;
    const id = setInterval(() => observer.next(counter  ), 2000);
}
复制代码
代码语言:javascript复制
// 因为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会将我们提供的函数参数作为nextcolor{#0abb3c}{next}next的回调处理函数。next决定传递一个什么样的数据给观察者。

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

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

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

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

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

代码语言:javascript复制
// 定义公共的用于数据存储的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来讲解一下,其他的操作符可以自己去查阅。

代码语言:javascript复制
// 当用户不关心接口的返回顺序
// 使用forkjoin主要是用于多个接口同时返回的时候,才会返回结果
    forkJoin([
      this._goodsListService.getHttpResultOne('12', 'zs'),
      this._goodsListService.getHttpResultTwo('12', 'zs')])
      .subscribe(resArr => {
      // 此时的返回结果会被按顺序放在一个数组中
      const oneData = resArr[0];
      const TwoData = resArr[1];
   });
复制代码
代码语言:javascript复制
   // 当用户关心接口的返回顺序时
   // 使用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);
   });

0 人点赞