依赖注入
依赖项注入(DI)是一种设计模式,在这种设计模式中,类会从外部资源请求依赖项而不是自己创建它们。依赖项是指某个类执行其功能所需的服务或对象。这种模式可以帮助解决一些重要的问题/目标,例如:
- 应用程序或类如何独立于其对象的构造方式
- 可以在单独的配置文件中指定对象创建
- 我们的应用程序如何支持不同的配置
- 依赖注入使测试更容易—因为现在我们可以将服务更改为真实服务的模拟程序,以便我们可以控制结果。
创建你自己的依赖注入器
TypeScript对类型的支持非常好,接下来将会用typescript一步一步的实现一个简单的依赖注入系统。
构造函数接口
首先,定义一个构造函数的接口:
export interface Constructable<T=any> {new(...params:any): T;}
接下来我们需要有一种方式来声明能被注入的对象,这里使用typescript提供的装饰器(Decorator),这个装饰器会被应用到类的声明上:
export function Injectable(constructor: Constructable): Constructable{const original = constructor;return original;}@Injectableexport class ServiceClass{constructor(){}sayHello(){ return "Hello from ServiceClass";}}
类装饰器的表达式会在运行时被当作函数来调用,这个函数的唯一参数就是被装饰的类的构造函数。以上是一个简单的用于装饰类的装饰器,它接受一个构造函数作为参数,并返回这个构造函数。这看起来好像没有什么用,而且好像多此一举。但是当这个装饰器应用到类上时,typescript会向类中加入元数据信息,这样我们可以在运行时利用反射查看这些元数据。
Typescript Decorators
Decorators提供一种方式以支持注解和元数据编程语法,装饰器可以应用到类以及类的方法上。在typescript中启用装饰器需要在tsconfig.json中做如下配置:
{"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */}
可以把装饰器当作一种高阶函数来看待,也就是一个函数,接受其他函数作为参数,并且用一个函数作为其返回值。
Reflection
反射被用来描述一些代码,这些代码具有系统中审查其他代码的能力,或者反射是一种语言级别的能力,它能够在运行时审查和动态调用类,方法,属性等。Typescript包含这项实验性功能,它可以为具有装饰器的声明emit某些类型的元数据。我们将用 “reflect-metadata”库来查看它们。
const metaData: Constructable[] = Reflect.getMetadata('design:paramtypes', constructor);
design:paramtypes - 会告诉我们构造函数在被调用时,其所接受的参数类型是什么。
Injector实现
export class Injector{public diMap = new Map();getInstance<T>(contr: Constructable<T>): T {const instance = this.constructObject(contr);return instance;}private constructObject(constructor: Constructable) {let currentInstance = this.diMap.get(constructor);if (currentInstance) return currentInstance;const metaData: Constructable[] = Reflect.getMetadata("design:paramtypes", constructor);// 实例化每一个构造函数const argumentsInstances = metaData.map((params) => this.constructObject(params));currentInstance = new constructor(...argumentsInstances);this.diMap.set(constructor, currentInstance);return currentInstance;}}
Angular中的依赖注入
Angular 的 DI 框架会在实例化某个类时为其提供所需依赖项。使用 Angular DI 可以提高应用程序的灵活性和模块化程度。
创建可注入服务
@Injectable() 装饰器会指定 Angular 可以在 DI 体系中使用此类。元数据 providedIn: ‘root’ 表示 HeroService 在整个应用程序中都是可见的。
import { Injectable } from '@angular/core';@Injectable({providedIn: 'root',})export class HeroService {constructor() { }}
注入服务
注入某些服务会使它们对组件可见。
要将依赖项注入组件的 constructor() 中,请提供具有此依赖项类型的构造函数参数。下面的示例在 组件的构造函数中指定了 HeroService。heroService 的类型是 HeroService。
constructor(heroService: HeroService)
在其他服务中使用这些服务
当某个服务依赖于另一个服务时,请遵循与注入组件相同的模式。在这里,HeroService 要依靠 Logger 服务来报告其活动。
当创建一个其 constructor() 带有参数的类时,请指定其类型和关于这些参数的元数据,以便 Angular 可以注入正确的服务。
在这里,constructor() 指定了 Logger 的类型,并把 Logger 的实例存储在名叫 logger 的私有字段中。
import { Injectable } from '@angular/core';// Logger.ts@Injectable({providedIn: 'root'})export class Logger {logs: string[] = []; // capture logs for testinglog(message: string) {this.logs.push(message);console.log(message);}}// hero.service.ts@Injectable({providedIn: 'root',})export class HeroService {constructor(private logger: Logger) { }getHeroes() {this.logger.log('Getting heroes ...');return [];}}
HttpClient
Reactive Extensions Library for JavaScript - RxJS
RxJS是一个响应式编程库,它利用Observable,使得构建异步和基于event方式的编程更加容易。它提供一个核心类型Observable,一些其他类型(Observer, Schedulers, Subjects)和众多的操作符,操作符使得处理异步发生的事件就像处理集合中的元素一样。
第一个例子
通常,我们以如下方式注册事件监听:
document.addEventListener('click', () => console.log('Clicked!'));
使用RxJS创建一个可观察对象:
import { fromEvent } from 'rxjs';fromEvent(document, 'click').subscribe(() => console.log('Clicked!'));
纯度
RxJS 的强大之处在于它能够使用纯函数生成值。 这意味着你的代码不太容易出错。
通常你会创建一个不纯的函数,你的其他代码片段可能会弄乱你的状态。
let count = 0;document.addEventListener('click', () => console.log(`Clicked ${++count} times`));
使用 RxJS 可以隔离状态。
import { fromEvent } from 'rxjs';import { scan } from 'rxjs/operators';fromEvent(document, 'click').pipe(scan(count => count + 1, 0)).subscribe(count => console.log(`Clicked ${count} times`));
scan 操作符的工作方式与数组的 reduce 类似。 它接受一个值并传递给回调函数。 回调的返回值将成为下次运行回调时传递的参数值。
流
RxJS 有一系列的操作符,可以帮助你控制事件如何流经你的 observables。
以下是使用纯 JavaScript 实现每秒最多允许一次点击的方式:
let count = 0;let rate = 1000;let lastClick = Date.now() - rate;document.addEventListener('click', () => {if (Date.now() - lastClick >= rate) {console.log(`Clicked ${++count} times`);lastClick = Date.now();}});
使用RxJS
import { fromEvent } from 'rxjs';import { throttleTime, scan } from 'rxjs/operators';fromEvent(document, 'click').pipe(throttleTime(1000),scan(count => count + 1, 0)).subscribe(count => console.log(`Clicked ${count} times`));
其他流控制操作符有 filter、delay、debounceTime、take、takeUntil、distinct、distinctUntilChanged 等。
值
你可以转换通过 observable 传递的值。
以下是使用纯 JavaScript 为每次点击添加当前鼠标 x 位置的方法:
let count = 0;const rate = 1000;let lastClick = Date.now() - rate;document.addEventListener('click', event => {if (Date.now() - lastClick >= rate) {count += event.clientX;console.log(count);lastClick = Date.now();}});
使用RxJS
import { fromEvent } from 'rxjs';import { throttleTime, map, scan } from 'rxjs/operators';fromEvent(document, 'click').pipe(throttleTime(1000),map(event => event.clientX),scan((count, clientX) => count + clientX, 0)).subscribe(count => console.log(count));
使用HTTP与服务端通信
大多数前端应用程序需要通过 HTTP 协议与服务器通信,以便下载或上传数据以及访问其他后端服务。 Angular 提供了一个客户端 HTTP API,@angular/common/http 中的 HttpClient 服务类。
HTTP 客户端服务提供以下主要功能。
- 类型化响应对象(HttpResponse)的能力
- 简化的错误处理
- 可测试性特征
- 请求和响应拦截
准备工作
要想使用 HttpClient,就要先导入 Angular 的 HttpClientModule。大多数应用都会在根模块 AppModule 中导入它。
import { NgModule } from '@angular/core';import { BrowserModule } from '@angular/platform-browser';import { HttpClientModule } from '@angular/common/http';@NgModule({imports: [BrowserModule,// import HttpClientModule after BrowserModule.HttpClientModule,],declarations: [AppComponent,],bootstrap: [ AppComponent ]})export class AppModule {}
然后,你可以把 HttpClient 服务注入成一个应用类的依赖项,如下面的 ConfigService 例子所示。
import { Injectable } from '@angular/core';import { HttpClient } from '@angular/common/http';@Injectable()export class ConfigService {constructor(private http: HttpClient) { }}
HttpClient 服务为所有工作都使用了可观察对象。你必须导入范例代码片段中出现的 RxJS 可观察对象和操作符。比如 ConfigService 中的这些导入就很典型。
import { Observable, throwError } from 'rxjs';import { catchError, retry } from 'rxjs/operators';
从服务器请求数据
使用 HttpClient.get() 方法从服务器获取数据。该异步方法会发送一个 HTTP 请求,并返回一个 Observable,它会在收到响应时发出所请求到的数据。返回的类型取决于你调用时传入的 observe 和 responseType 参数。
get() 方法有两个参数。要获取资源的 URL,以及一个可以用来配置请求的选项对象。
options: {headers?: HttpHeaders | {[header: string]: string | string[]},observe?: 'body' | 'events' | 'response',params?: HttpParams|{[param: string]: string | number | boolean | ReadonlyArray<string | number | boolean>},reportProgress?: boolean,responseType?: 'arraybuffer'|'blob'|'json'|'text',withCredentials?: boolean,}
这些重要的选项包括 observe 和 responseType 属性。
- observe 选项用于指定要返回的响应内容。
- responseType 选项指定返回数据的格式。
拦截请求和响应
借助拦截机制,你可以声明一些拦截器,它们可以检查并转换从应用中发给服务器的 HTTP 请求。这些拦截器也可以检查和转换来自服务器的响应。多个拦截器构成了请求/响应处理器的双向链表。
拦截器可以用一种常规的、标准的方式对每一次 HTTP 的请求/响应任务执行从认证到记录日志等很多种隐式任务。
如果没有拦截机制,那么开发人员将不得不对每次 HttpClient 调用显式实现这些任务。
编写拦截器
要实现拦截器,就要实现一个实现了 HttpInterceptor 接口中的 intercept() 方法的类。
这里是一个什么也不做的空白拦截器,它只会不做任何修改的传递这个请求。
import { Injectable } from '@angular/core';import {HttpEvent, HttpInterceptor, HttpHandler, HttpRequest} from '@angular/common/http';import { Observable } from 'rxjs';/** Pass untouched request through to the next request handler. */@Injectable()export class NoopInterceptor implements HttpInterceptor {intercept(req: HttpRequest<any>, next: HttpHandler):Observable<HttpEvent<any>> {return next.handle(req);}}
intercept 方法会把请求转换成一个最终返回 HTTP 响应体的 Observable。 在这个场景中,每个拦截器都完全能自己处理这个请求。
大多数拦截器拦截都会在传入时检查请求,然后把(可能被修改过的)请求转发给 next 对象的 handle() 方法,而 next 对象实现了 HttpHandler 接口。
export abstract class HttpHandler {abstract handle(req: HttpRequest<any>): Observable<HttpEvent<any>>;}
像 intercept() 一样,handle() 方法也会把 HTTP 请求转换成 HttpEvents 组成的 Observable,它最终包含的是来自服务器的响应。 intercept() 函数可以检查这个可观察对象,并在把它返回给调用者之前修改它。
这个无操作的拦截器,会使用原始的请求调用 next.handle(),并返回它返回的可观察对象,而不做任何后续处理。
next 对象
next 对象表示拦截器链表中的下一个拦截器。 这个链表中的最后一个 next 对象就是 HttpClient 的后端处理器(backend handler),它会把请求发给服务器,并接收服务器的响应。
大多数的拦截器都会调用 next.handle(),以便这个请求流能走到下一个拦截器,并最终传给后端处理器。 拦截器也可以不调用 next.handle(),使这个链路短路,并返回一个带有人工构造出来的服务器响应的 自己的Observable。
这是一种常见的中间件模式,在像 Express.js 这样的框架中也会找到它。
提供这个拦截器
这个 NoopInterceptor 就是一个由 Angular 依赖注入 (DI)系统管理的服务。 像其它服务一样,你也必须先提供这个拦截器类,应用才能使用它。
由于拦截器是 HttpClient 服务的(可选)依赖,所以你必须在提供 HttpClient 的同一个(或其各级父注入器)注入器中提供这些拦截器。 那些在 DI 创建完 HttpClient 之后再提供的拦截器将会被忽略。
由于在 AppModule 中导入了 HttpClientModule,导致本应用在其根注入器中提供了 HttpClient。所以你也同样要在 AppModule 中提供这些拦截器。
在从 @angular/common/http 中导入了 HTTP_INTERCEPTORS 注入令牌之后,编写如下的 NoopInterceptor 提供者注册语句:
{ provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },
注意 multi: true 选项。 这个必须的选项会告诉 Angular HTTP_INTERCEPTORS 是一个多重提供者的令牌,表示它会注入一个多值的数组,而不是单一的值。
你也可以直接把这个提供者添加到 AppModule 中的提供者数组中,不过那样会非常啰嗦。况且,你将来还会用这种方式创建更多的拦截器并提供它们。 你还要特别注意提供这些拦截器的顺序。
可以考虑创建一个单独文件,用于把所有拦截器都收集起来,一起提供给 httpInterceptorProviders 数组,可以先从这个 NoopInterceptor 开始。
/* "Barrel" of Http Interceptors */import { HTTP_INTERCEPTORS } from '@angular/common/http';import { NoopInterceptor } from './noop-interceptor';/** Http interceptor providers in outside-in order */export const httpInterceptorProviders = [{ provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },];
然后导入它,并把它加到 AppModule 的 providers** 数组**中,就像这样:
providers: [httpInterceptorProviders],
当你再创建新的拦截器时,就同样把它们添加到 httpInterceptorProviders 数组中,而不用再修改 AppModule。
拦截器的顺序
Angular 会按你提供拦截器的顺序应用它们。例如,考虑一个场景:你想处理 HTTP 请求的身份验证并记录它们,然后再将它们发送到服务器。要完成此任务,你可以提供 AuthInterceptor 服务,然后提供 LoggingInterceptor 服务。发出的请求将从 AuthInterceptor 到 LoggingInterceptor。这些请求的响应则沿相反的方向流动,从 LoggingInterceptor 回到 AuthInterceptor。以下是该过程的直观表示:
