前篇教程请参考 Angular进阶教程1 - (路由 + 表单)
依赖注入与HTTP的介绍
为什么使用服务?
不应该直接获取或保存数据,它们应该聚焦于展示数据,而把数据访问和处理的职责委托给某个。那面对组件和服务之间的关系,该如何处理他们之间的依赖关系呢?Angular就引入了去解决这件事情。
依赖注入(DI)
依赖项( 服务/对象 )注入是一种设计模式,在这种设计模式中,类会从外部源而不是创建它们。Angular 的 DI 框架会在某个类时为其提供依赖,从而提高模块性和灵活性。在学习依赖注入之前我们先来了解一下关于依赖注入中比较核心的三个概念:
注入器(Injector):提供了一系列的接口用于
创建
依赖对象的实例
。 (可以想象成是一个厨师做菜)Provider:用于
配置注入器
,注入器通过它来创建被依赖对象的实例。Provider把标识(Token)
映射到列表对象,同时还提供了一个运行时所需的依赖
,被依赖的对象就是通过该方法来创建的。(可以想象成厨师手中的菜谱,其中Token就是菜名)依赖(Dependence):指定了被依赖对象的类型,注入器会根据此类型创建对应的对象。
[图片上传失败...(image-7f08d6-1639566693325)]
依赖注入的使用
- 创建可注入服务:
import { Injectable } from '@angular/core';
// @Injectable()装饰器,是告诉Angular这是一个可供注入的服务,该注入器主要负责创建服务实例,并把他注入到类中, 元数据providedIn: 'root' 表示 HeroService在整个应用程序中都是可见的。
@Injectable({
providedIn: 'root',
})
export class GoodsListService {
constructor() { }
}
如果所创建的服务
不依赖于其他服务
,是可以不用使用 Injectable 类装饰器。但当该服务需要在构造函数中注入依赖对象,就需要使用Injectable 装饰器。不过我们在开发过程中一般都会加上这个装饰器。
- 注入服务
将依赖项(服务)注入到组件的constructor()中
constructor(goodsListService: GoodsListService)
注入服务的常见方式
在组件中注入服务
如果你在的上定义了providers,那么angular会根据providers为这个组件创建一个注入器,这个也会这个注入器,如果没有定义,那么组件会根据组件树查找合适的注入器来创建组件的依赖。
// 这种方式注册,会注册到每个组件实例自己的注入器上。(多个组件会有多个注入器)
@Component({
selector: 'app-goods-list',
providers: [ GoodsListService ]
})
其实这种引入方式只是一种简写,不过也是一种常用的写法,真正的完整版本是:
@Component({
selector: 'app-goods-list',
providers: [{ provide: GoodsListService, useClass: GoodsListService } ]
// 其中provide属性可以理解为这个Provider的唯一标识,用于定位依赖值,也就是应用中使用的服务名
// 而useClass属性则代表使用哪个服务类来创建实例
})
在模块中注入服务
在中注入的服务,在所有的中都能这个服务,当然在中也可以达到相同的结果,需要我们通过导入了外来模块,那么外来模块的服务就都注入到了你所在模块的
补充上述原因: 因为Angular在启动程序时会启动一个
根模块
,并加载它所依赖的其他模块,此时会生成一个全局的根注入器
,由该注入器创建的依赖注入对象在整个应用程序级别可见,并共享一个实例。所以说在Angular中并没有模块级别的区域,只有组件级别
和应用级别
的区域。模块级别的注入
就相当于是应用级别
。
// 这种方式注册,可以对服务进行一些额外的配置(服务类中也需要写@Injectable()装饰器)。
// 在未使用路由懒加载的情况下,这种注入的方式和在服务类中注入的方式是一样的。
@NgModule({
providers: [ GoodsListService ],
})
注意的点: 虽然在模块中注入的依赖相当于是应用级别的,但是当遇到
路由懒加载
的时候,会出现一种特殊情况,Angular会对延迟加载模块初始化一个新的执行上下文
,并创建一个新的注入器
,在该注入器中注入的依赖只在该模块内部
可见,这算是一个特殊的模块级作用域
。
在服务类中注入服务
// 这种注入方式,会告诉Angular在根注入器中注册这个服务,这也是使用CLI生成服务时默认的方式.
// 这种方式注册,不需要再@NgModule装饰器中写providers,而且在代码编译打包时,可以执行tree shaking优化,会移除所有没在应用中使用过的服务。推荐使用此种方式注册服务.
@Injectable({
providedIn: 'root'
})
在
根组件
还是在子组件
中进行服务注入,该怎么选择呢?这取决于想让注入的依赖服务具有
全局性
还是局部性
依赖对象的创建方式有四种(仅了解):
-
useClass: 基于标识来指定依赖项
-
useValue: 依赖对象不一定是类,也可以是常量、字符串、对象等其他数据类型
-
useExisting: 就可以在一个Provider中配置多个标识,他们对应的对象指向同一个实例,从而实现多个依赖、一个对象实例的作用
-
useFactory: 动态生成依赖对象
Http的介绍
大多数前端应用都要通过 HTTP 协议与服务器,才能下载或上传数据并访问其它后端服务。Angular 给应用提供了一个 HTTP 客户端 API,也就是 中的 服务类。
使用HttpClient
- 一般会在根模块下导入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 { }
- 在服务类中依赖注入 (需要在服务类中通过HttpClient去进行通讯)
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable()
export class GoodsListService {
constructor(private http: HttpClient) { }
}
- 使用 返回的都是可观察对象(observable)类型的服务。因此我们还需要在服务类中导入RxJS 和可能会使用到的。
import { Observable } from 'rxjs';
import { pluck } from 'rxjs/operators'; // 此操作符是用来获取某个字段内容
常用的请求方式
- 从服务器请求数据 HttpClient.get()
// 在服务类中去封装和服务端通讯的方法
public getHttpResult(code: string, name: string): Observable<any> {
const url: string = ''; // 这是请求的地址
return this._http.get(url, { params: { code, name } });
}
- 发送数据到服务器 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是一个库,是针对编程工具,当然Angular引入RxJS就是让异步更加简单,更加可控,在开始RxJS之前,我们先来了解一下Reactive Programming,其本质就是使用的一种编程方式。
什么是流呢?
所谓,就是数据基于事件(event)变化的整体。stream = data + event,不过我们可以通过河流来更直观的理解一下流,首先河流是有的,所以流也是有流向的,一条河流可以分成很多支流,很多小的支流也可以汇总成一条河流,所以在RxJS中,流也可以使用实现流的和。
RxJS中的核心概念(Observable 、Observer 、Subscription、Subject)
在Angular项目中我们在调用接口的时候,常用的调用方式是:
this._goodsListService.getHttpResult
.subscribe((res) => {
console.log(res)
})
//this._goodsListService.getHttpResult就是返回observable,他可以是api的调用,可以是事件的调用等等
我们可以把上述的调用方式抽象一下为在这里我们认识到了两个新的事物分别是Observable和Observer,以及这个方法调用的返回对象,返回的是一个Subscription对象的实例化,接下来我们逐一介绍这些核心概念。
Observable
Observable是RxJS中最核心的一个概念,它的本质就是“Observable is a function to generate values”,首先它是一个,也就是说它是,一般我们会在变量末尾加$表示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的一些特性,如下所示:
- 必须被调用(订阅)才会被执行
- observable 被调用后,必须能被关闭,否则会一直运行下去
- 对于同一个observable,在不同的地方subscribe,是无关的。这和function执行多次,互相没有关联是一致的。
Observer(了解)
它是。它是一个有三个回调函数的,每个回调函数对应三种Observable发送的通知类型(next, error, complete),observer表示的是对序列结果的。在实际开发中,如果我们提供了作为参数,subscribe会将我们提供的函数参数作为的回调处理函数。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是:我们可以像订阅任何observable一样去订阅subject。
Subject是: 它有next(v),error(e),和complete()方法,如果我们需要给subject提供新值,只要调用<font color="#13c078">next(v)</font>,它会将值多播给已注册监听该subject的观察者。
所以: Subject既是Observable,也是观察者(可以多个)
Subject与Observable的区别:
- Subject是【他可以将值多播给多个观察者】
- 普通的Observble是【每个已经订阅的观察者(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是个,它的输入为observable,返回也observable。operators的本质是,描述从一个数据流到另一个数据流之间的关系,也就是observer到observable中间发生的转换,很类似于Lodash。
在RxJS中操作符有接近100个,不过在开发过程常用的也就十多个。
常见的运算符包含 map, filter, concat, flatmap, switchmap, forkjoin
在这里我们只调挑出forkJoin和switchMap来讲解一下,其他的操作符可以自己去查阅。
// 当用户不关心接口的返回顺序
// 使用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 被调用后,必须能被关闭,否则会一直运行下去”,所以我们在组件中订阅的observabled,都需要在组件的阶段被,从而来优化项目的性能和效率。具体取消订阅的有如下三个步骤:
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());
}
}
网友评论