第十三章 依赖注入(DI)

来源:互联网 发布:js文件怎么运行 编辑:程序博客网 时间:2024/06/03 17:53

注:学习使用,禁止转载

通常,随着应用程序的变大,应用程序的不同部分需要交流,当模块a需要模块b才能运行时,我们说b依赖于a。

获得依赖的最常见的一种方式就是导入一个文件,比如,在我们假设的模块中,我们可能向下面这样做:

// in A.tsimport {B} from 'B'; // a dependency!B.foo(); // using B

通常情况下,简单导入其他代码就够了,然而有时候,我们所需要的是一种更复杂的方式提供依赖项。比如:

  • 在测试期间,我们需要实现B代替MockB。
  • 我们想要分享我们B类的单例在我们的app中(比如单例模式)
  • 当每次b被使用时,需要创建一个新的实例

DI可以解决这些问题

DI是这样一个系统,它使我们程序的一部分可以访问其他部分,而且,我们可以配置这些事怎么发生的。

DI这个词是用来描述设计模式的(它使用在很多框架中),angular内建就实现了DI。

使用DI的主要好处是,客户组件不需要了解如何创建依赖,只需要知道怎么交互就可以了。4

DI的例子:PriceService

想象一下,我们有一个Product类,这个Product有一个价格,为了计算这个产品的最终价格,我们依赖于一个服务,它使用输入传递进来。

  • 产品的基本价格
  • 我们销售的状态

下面是不使用DI的样式:

class Product { constructor(basePrice: number) { this.service = new PriceService(); this.basePrice = basePrice; } price(state: string) { return this.service.calculate(this.basePrice, state); }}

现在,让我们想象一下,我们需要为这个Product编写一个测试,假设PriceService通过查询数据库返回一个折扣,我们写的测试像下面这样:

let product; beforeEach(() => { product = new Product(11); });describe('price', () => { it('is calculated based on the basePrice and the state', () => { expect(product.price('FL')).toBe(11.66); });})

即使这个测试可以工作,这种方法有一些缺点,为了测试成功,必须有几个条件得到满足:

  • 数据库必须运行
  • Florida的税收收入必须是我们期望的。

基本上,如果我们增加一个意想不到的依赖,我们的测试会更加脆弱。

如果我们做一点改变,像下面这样:

class Product { constructor(service: PriceService, basePrice: number) { this.service = service; this.basePrice = basePrice; } price(state: string) { return this.service.calculate(this.basePrice, state); }}

现在,创建一个Product,客户需要决定使用哪一种价格服务。
我们可以编写一个简单的测试,如下:

class MockPriceService { calculate(basePrice: number, state: string) { if (state === 'FL') { return basePrice * 1.06; } return basePrice; }}

通过这个小小的改变,我们可以调整我们的测试使其更清楚,而且摆脱了数据的依赖。

let product; beforeEach(() => { const service = new MockPriceService(); product = new Product(service, 11); }); describe('price', () => { it('is calculated based on the basePrice and the state', () => { expect(product.price('FL')).toBe(11.66); });})

我们还会得到好处,我们的 Product类是隔离的,也就是说,我们的Product有一个确切的依赖。

Don’t Call Us…”别叫我

DI这个技术依赖于控制反转这种原则。

多年来,这是很常见的的模式,我们为每一个组件注明完整的应用程序上下文,并且设置它的依赖,这看起来是很清晰的,Product类必须知道PriceService类。

这样做的一个缺点是,一旦一个组件需要一个依赖,组件本身变得脆弱而且难以改变。如果我们做出变化,我们的组件依赖于很多其他组件,我们不得不传播这些依赖。换句话说,它使得我们的组件紧耦合。

当我们使用DI的时候,我们朝着松耦合的方向前进,单个应用程序尽量少影响其他的组件。并且,只要这些组件的接口没有改变,我们甚至可以直接改变它们,不需要组件做任何其他的改变。

ng2继承了ng1的一大特点就是控制反转。angular使用DI去解决来自于外部的依赖。

传统上,如果组件a依赖组件b,那么在组件a的内部会创建一个b的对象,这意味着a依赖于b,

这里写图片描述

angular使用DI改变这个事情,A像依赖注入系统请求B,依赖注入系统会像我们期望一样传递一个B给A。

这里写图片描述

相对传统方式来说,这个有很多好处。一个好处就是当我们测试A的时候,我们可以mock一个B出来注入到A。

在这本书中,我们使用了很多次DI。比如我们在路由那张创建music应用的时候,为了去和Spotify API进行交互,我们创建了SpotifyService,它被注入到了几个组件中:

code/routes/music/app/ts/components/AlbumComponent.ts

constructor(public routeParams: RouteParams, public spotify: SpotifyService, public locationStrategy: LocationStrategy) { this.id = routeParams.get('id');

现在,让我们学习怎么创建自己的service和我们注入它们的不同形式。

依赖注入部分

注册一个依赖,我们必须将其绑定到能标识依赖的地方,这个标识被称为依赖令牌(token),比如,我们想要注册一个API的URL,我们可以使用API_URL作为令牌,同理,如果我们注册一个类,我们可以使用类本身作为它的令牌。

在angular中的依赖注入,分为三步部分:

  • 提供者(Provider-通常也称为绑定)映射一个令牌(通常是一个string或者一个类)到一个列表中,它告诉angular,怎么去创建一个对象,并且给定一个令牌。
  • 注入器(Injector),它存储绑定的集合,并在创建对象的时候去解析依赖并且注入它们
  • 依赖(Dependency),将被注入的东西

我们可以通过下面这张图描述它们的职责:

这里写图片描述

当处理DI的时候,有很多不同的选择,让我们逐一学习它们。

使用DI最通用的场景就是在整个应用程序中提供服务或者值,这个场景占了99%。

这个就是我们想要去做的,接下来,我们会讲解怎样编写一个基本的服务,它会是我们在用户程序中花费时间最多的。

注入器

像上面提到的一样,angular在后台为我们安装一个DI,在我们使用注解注入依赖到我们的组件之前,让我们首先了解一下注入器。

我们创建一个仅仅返回一个string的简单服务。

code/dependency_injection/simple/app/ts/app.ts

class MyService {    getValue():string{        return 'a value';    }}

接下来,我们创建我们的APP组件:

code/dependency_injection/simple/app/ts/app.ts

@Component({    selector: 'di-sample-app',    template: `  <button (click)="invokeApi()">Get a value</button>  `})class DiSampleApp {    myService: MyService;    constructor() {       let injector: any = ReflectiveInjector.resolveAndCreate([MyService]);       this.myService = injector.get(MyService);       console.log('Same instance?', this.myService === injector.get(MyService));    }    invokeApi():void {        console.log('MyService returned', this.myService.getValue());    }}

这个程序很简单,我们定义一个DiSampleApp的组件,它会显示一个button,当button被点击的时候,调用invokeApi。

现在,我们关注一下构造函数,我们使用一个来自于ReflectiveInjector的静态方法resolveAndCreate.这个方法的职责是创建一个新的注入器,参数是一个我们想要这个注入器知道的可注入的(injectable)东西的数组,在这个例子中,我只想要它知道MyService。

ReflectiveInjector是Injector的具体实现,它使用反射去寻找合适的参数类型,相对于其他的注入器,ReflectiveInjector是一个通用的注入器。

一个重要的事情是,它将会注入这个类的单实例(single)

这个可以通过构造函数的最后两行得以确定,

console.log('Same instance?', this.myService === injector.get(MyService));

这句话证明,我们使用injector.get获取的两次实例是相同的。

注意,如果我们使用自己的注入器,我们就不需要告诉应用程序的可注入列表。

既然我们学会了注入器的工作原理,然后我们学习angular的注入框架,接下来学习我们可以注入的其他东西,并且学会怎么注入它们。

Providers(提供者)

接下来需要知道的,在angular的DI系统中,我们可以使用几种注入方式:

  • 注入一个单例
  • 调用任何函数,然后注入这个函数的返回值
  • 注入一个值
  • 创建一个别名

让我们分开看看

使用一个类

注入一个类的单实例可能是最通常做的事情。下面是我们配置它的代码:

provide(MyComponent, {useClass: MyComponent})

值得注意的是,provide方法接收两个参数,第一个就是令牌,第二个是怎样注入或者注入什么。所以这里,我们将我们的MyComponent类映射到MyComponent令牌,在这种情况下,类的名字与令牌匹配,这是通常的情形,但是要注意,被注入的东西和令牌不一定要有相同的名字。

就像我们在上面看到的一样,这个方法会创建一个单例对象,并且我们每次注入的时候它都返回这个相同的单例,当然,在第一次注入之前,不会创建这个类的实例,所以第一次注入的时候,会调用该类的构造函数。

如果一个服务的构造函数需要参数怎么办呢?

code/dependency_injection/misc/app/ts/app.ts

class ParamService {  constructor(private phrase: string) {    console.log('ParamService is being created with phrase', phrase);  }  getValue(): string {    return this.phrase;  }}

注意,这个构造器怎么获取参数呢?如果我们使用正常的注入机制:

bootstrap(MyApp, [ParameterService]);

我们会得到一个错误:

这里写图片描述

这个发生的原因是我们没有给注入器关于构建一个类的足够的信息,为了解决这个问题,我们需要告诉注入器,当创建这个服务的实例时,我们想要它去使用哪些参数。

如果我们希望创建服务的时候传递参数,我们可能需要使用工厂来代替(factory)

工厂(Using a Factory)

当我们使用一个工厂注入器的时候,我们可以编写一个函数,这个函数可以返回任何类型。

 provide(MyComponent, useFactory: () => {     if (loggedIn) {         return new MyLoggedComponent();     }     return new MyComponent();});

在上面的代码中,我们注入MyComponent令牌,但是这个会检测一个外部变量loggedIn,如果这个参数为真,我们会接受一个MyLoggedComponent的实例,否则返回MyComponent。

工厂也可以有依赖,比如:

provide(MyComponent, useFactory: (user) => {     if (user.loggedIn()) {         return new MyLoggedComponent(user);     }     return new MyComponent();}, deps: [User])

如果我们希望像上面一样使用我们的ParamService,我们可以将他包装在useFactory中,像下面这样:

bootstrap(DiSampleApp, [  SimpleService,  provide(ParamService, {    useFactory: (): ParamService => new ParamService('YOLO')  })]).catch((err: any) => console.error(err));

对于创建注入来说,工厂是功能最强大的方式,因为我们可以在函数中做任何事情。

使用一个值

当我们想要一个值,在应用程序的另外一个地方会被重定义的时候,注入一个值是非常有用的。比如:

provide('API_URL', {useValue: 'http://my.api.com/v1'})

用于定义别名(alias)

我们也可以用于定义一个引用的别名,像下面这样:

provide(NewComponent, {useClass: MyComponent})

在应用程序中的依赖注入

在编写应用程序时,为了注入服务,需要执行下面三步:

  1. 创建一个服务类
  2. 在接收组件里面声明一个依赖
  3. 配置注入(使用angular注册一个注入)

我们要做的第一件事情就是创建一个服务类,这个类可以暴露一些我们希望的行为,这个被称为注射,它包含一些我们组件类将要从诸如服务那里获取到的行为。

下面是我们创建的一个服务:

code/dependency_injection/simple/app/ts/services/ApiService.ts

export class ApiService {  get(): void {    console.log('Getting resource...');  }}

现在,我们有了要注入的类,接下来就是在我们的组件中声明一个依赖,前面我们直接使用Injector类,但是angular为我们提供了两种简便的方式,最经典的方式就是在我们的构造器中声明。

为了做这个事情,首先需要引入服务:

code/dependency_injection/simple/app/ts/app.ts

/* * Services */import {ApiService} from 'services/ApiService';

然后,我们在构造器中声明它:

code/dependency_injection/simple/app/ts/app.ts

class DiSampleApp {    constructor(private apiService:ApiService) {    }

当我们在构造器中声明之后,angular会通过反射查找要注入的服务,并注入进来。也就是说,angular会通过我们在构造器中声明的对象类型寻找ApiService,然后向DI系统接收一个合适的注入。

有时,我们需要给angular更多的提示,这个时候,我们使用@Inject注解。

class DiSampleApp { private apiService: ApiService; constructor(@Inject(ApiService) apiService) { this.apiService = apiService;}

使用依赖注入的最后一件事情就是使用injectable链接我们组件想要的注入的东西。换句话说,我们要告诉angular当一个组件声明它的依赖的时候,哪些东西需要注入。

provide(ApiService, {useClass: ApiService})

在上面的代码中,我们使用ApiService这个token去导出ApiService的单实例。

与Injectors工作

我们已经可以使用injectors做一些事情,现在我们讨论,我们什么时候需要显示使用他们

一种情况就是当我们希望控制依赖注入的单实例创建的时候,为了描述这个情况,我们使用ApiService创建另外一个应用程序。

这个服务会使用到其他的两个服务,它是基于浏览器视框。当小于800像素的时候,返回一个叫着SmallService,否则,返回一个LargeService.。

SmallService像下面这样:

code/dependency_injection/complex/app/ts/services/SmallService.ts

export class SmallService {  run(): void {    console.log('Small service...');  }}

LargeService:像这样:
code/dependency_injection/complex/app/ts/services/LargeService.ts

export class LargeService {  run(): void {    console.log('Large service...');  }}

然后,我们编写ViewPortService,它选择两者之一:

code/dependency_injection/complex/app/ts/services/ViewPortService.ts

import {LargeService} from './LargeService';import {SmallService} from './SmallService';export class ViewPortService {  determineService(): any {    let w: number = Math.max(document.documentElement.clientWidth,                             window.innerWidth || 0);    if (w < 800) {      return new SmallService();    }    return new LargeService();  }}

现在,让我们创建一个访问我们服务的APP,

code/dependency_injection/complex/app/ts/app.ts

class DiSampleApp {  constructor(private apiService: ApiService,              @Inject('ApiServiceAlias') private aliasService: ApiService,              @Inject('SizeService') private sizeService: any) {  }

这里,我们获取了一个ApiService的实例,但是我们又获取了一个ApiService的服务,并给它取一个别名ApiServiceAlias,然后,我们获取一个SizeService,但是它还没有定义。

为了理解每一个service代表什么,让我们看看bootstrap的代码:

bootstrap(DiSampleApp, [  ApiService,  ViewPortService,  provide('ApiServiceAlias', {useExisting: ApiService}),  provide('SizeService', {useFactory: (viewport: any) => {    return viewport.determineService();  }, deps: [ViewPortService]})]).catch((err: any) => console.error(err));

这里,我们希望应用程序注入器知道ApiService和ViewPortService。然后,我们定义了ApiService的另外一个别名ApiServiceAlias。然后,我们定义了另外一个注入叫着SizeService.这个工厂接收一个ViewPortService的实例,将它作为依赖放入列表,然后调用它的determineService,它会根据浏览器的宽度返回SmallService或者LargeService。

当我们在app上点击button的时候,我们希望去做三个调用: 一个是ApiService,一个是ApiServiceAlias,一个是SizeService:。

code/dependency_injection/complex/app/ts/app.ts

invokeApi(): void {    this.apiService.get();    this.aliasService.get();    this.sizeService.run();  }

现在,让我们运行我们的app,如果在小的浏览器上:

这里写图片描述

如果在大的浏览器上:

这里写图片描述

可以看到,两次打印的信息不一样,说明获取到了不同的服务。

但是,现在,如果我们缩放浏览器,并点击button,它始终会打印Large。

这里写图片描述

这是因为,factory只会被调用一次,为了解决这个问题,我们创建我们自己的注入器,它获取适当的服务,像下面这样:

code/dependency_injection/complex/app/ts/app.ts

let injector:any = ReflectiveInjector.resolveAndCreate([            ViewPortService,            provide('OtherSizeService', {                useFactory: (viewport:any) => {                    return viewport.determineService();                }, deps: [ViewPortService]            })        ]);        let sizeService:any = injector.get('OtherSizeService');        sizeService.run();

这里,我们创建一个注入器,它知道ViewPortService,和另外的OtherSizeService作为令牌的注入,最后我们使用获取OtherSizeService的实例。现在如果我们缩放窗口,点击button,我们会得到合适的日志,因为每次点击button的时候,都会调用useInjectors一次。

替换值

使用DI的另一个原因就是在运行时替换值。当我们有一个APIService,它执行HTPP请求的时候发生。在我们的单元测试或者继承测试中,我们不希望我们的代码访问数据库,在这种情况下,我们希望编写一个mock service来代替我们的真正实现。

比如,如果我们希望在开发环境和生产环境,我们的APP访问不同的api。

或者是,当我们发布一个开源或者可重用的API服务时,我们希望客户端应用程序定义或者重载我们API URL。

让我们编写一个应用程序,它根据我们是在开发环境还是在生产环境返回不同的值。

code/dependency_injection/value/app/ts/services/ApiService.ts

import { Inject } from '@angular/core';export const API_URL: string = 'API_URL';export class ApiService {  constructor(@Inject(API_URL) private apiUrl: string) {  }  get(): void {    console.log(`Calling ${this.apiUrl}/endpoint...`);  }}

我们定义一个常量,它会被作为令牌为API URL的依赖使用。换句话说,angular会使用API URL来存储调用的URL信息,这样,当我们使用@Inject(API_URL)的时候,就会返回合适的值,注意,我们导出了API_URL常量,所以客户端应用程序可以使用。

现在,已经有了service,让我们编写一个使用这个服务的组件,它根据不同的运行环境提供不同的值。

code/dependency_injection/value/app/ts/app.ts

@Component({  selector: 'di-value-app',  template: `  <button (click)="invokeApi()">Invoke API</button>  `})class DiValueApp {  constructor(private apiService: ApiService) {  }  invokeApi(): void {    this.apiService.get();  }}

这是组件的代码,在构造器中,我们使用了注入apiService服务,如果想要显示声明,可以使用下面这种方式:

constructor(@Inject(ApiService) private apiService: ApiService) { }

在这个组件中有一个button,当我们点击button的时候,调用apiService的get方法,并打印返回值,也就是API_URL的值。

接下来是使用providers配置应用程序。

code/dependency_injection/value/app/ts/app.ts

bootstrap(DiValueApp, [  provide(ApiService, { useClass: ApiService }),  provide(API_URL, {    useValue: isProduction ?      'https://production-api.sample.com' :      'http://dev-api.sample.com'  })]).catch((err: any) => console.error(err));

首先,我们声明一个常量isProduction,并且设置它为false,我们可以假装我们做一些事情来决定我们是不是在生产环境。这个设置可以是像我们一样硬编码,也可以使用像webpack一样的设置。

最后,我们启动应用程序,并且按照两个提供程序,一个是ApiService,另外一个是API_URL,如果我们是在生产环境中,我们会使用一个值,否则使用另外一个值。

为了测试,可以将isProduction设置为true,并且点击button,会得到下面两种输出。

这里写图片描述

这里写图片描述

总结

就像你所看到的,管理应用程序依赖方面,DI是一个功能强大的方式。

1 0