Angular 依赖注入简介

2019-11-05 15:47:54 浏览数 (1)

在介绍依赖注入的概念和作用前,我们先来看个例子。各位同学请睁大眼睛,我要开始 “闭门造车” 了。

一辆车内部构造很复杂,出于简单考虑,我们就只考虑三个部分:车身、车门和引擎。

现在我们来分别定义各个部分:

  1. 定义车身类
代码语言:javascript复制
export default class Body { }
  1. 定义车门类
代码语言:javascript复制
export default class Doors { }
  1. 定义车引擎类
代码语言:javascript复制
export default class Engine {
  start() {
    console.log('开动鸟~~~');
  }
}
  1. 定义汽车类
代码语言:javascript复制
import Engine from './engine';
import Doors from './doors';
import Body from './body';

export default class Car {
    engine: Engine;
    doors: Doors;
    body: Body;

    constructor() {
      this.engine = new Engine();
      this.body = new Body();
      this.doors = new Doors();
    }

    run() {
      this.engine.start();
    }
}

一切已准备就绪,我们马上来造一辆车:

代码语言:javascript复制
let car = new Car(); // 造辆新车
car.run(); // 开车上路咯

车已经可以成功上路,但却存在以下问题:

  • 问题一:在创建新车的时候,你没有选择,假设你想更换汽车引擎的话,按照目前的方案,是实现不了的。
  • 问题二:在汽车类内部,你需要在构造函数中手动去创建各个部件。

为了解决第一个问题,提供更灵活的方案,我们需要重构一下 Car 类:

代码语言:javascript复制
export default class Car {
    engine: Engine;
    doors: Doors;
    body: Body;

    constructor(engine, body, doors) {
      this.engine = engine;
      this.body = body;
      this.doors = doors;
    }

    run() {
      this.engine.start();
    }
}

重构完 Car 类,我们来重新造辆新车:

代码语言:javascript复制
let engine = new NewEngine();
let body = new Body();
let doors = new Doors();
this.car = new Car(engine, body, doors);
this.car.run();

此时我们已经解决了上面提到的第一个问题,要解决第二个问题我们要先介绍一下依赖注入的概念。

依赖注入的概念

在软件工程中,依赖注入是种实现控制反转用于解决依赖性设计模式。一个依赖关系指的是可被利用的一种对象(即服务提供端) 。依赖注入是将所依赖的传递给将使用的从属对象(即客户端)。该服务是将会变成客户端的状态的一部分。 传递服务给客户端,而非允许客户端来建立或寻找服务,是本设计模式的基本要求。 —— 维基百科 控制反转 (inversion of control,IoC) 原则的非正式称谓是“好莱坞法则”。它来自好莱坞的一句常用语“别打给我们,我们会打给你 (don’t call us, we’ll call you)”。

一般情况下,如果服务 A 需要服务 B,那就意味着服务 A 要在内部创建服务 B 的实例,也就说服务 A 依赖于服务 B:

(图片来源 —— Angular 权威指南)(图片来源 —— Angular 权威指南)

Angular 利用依赖注入机制改变了这一点,在该机制下,如果服务 A 中需要服务 B,即服务 A 依赖于服务 B,那么我们期望服务 B 能被自动注入到服务 A 中,如下图所示:

(图片来源 —— Angular 权威指南)(图片来源 —— Angular 权威指南)

在 Angular 中,依赖注入包括以下三个部分:

  • 提供者负责把一个令牌(可能是字符串也可能是类)映射到一个依赖的列表。它告诉 Angular 该如何根据指定的令牌创建对象。
  • 注入器负责持有一组绑定;当外界要求创建对象时,解析这些依赖并注入它们。
  • 依赖就是将被用于注入的对象。

三者的关系图如下:

(图片来源 —— Angular 权威指南)(图片来源 —— Angular 权威指南)

接下来我们来看一下如何利用 Angular 重写上面的示例:

car.model.ts

代码语言:javascript复制
export class Body {}
export class Doors {}
export class Engine {
  start() {
    console.log("?开动鸟~~~");
  }
}

export class Car {
  constructor(
    private engine: Engine,
    private doors: Doors,
    private body: Body
  ) {}

  run() {
    this.engine.start();
  }
}

app.component.ts

代码语言:javascript复制
@Component({
  selector: "app-root",
  template: `
    <div>
     <h3>Angular DI</h3>
    </div>
  `
})
export class AppComponent {
  carInjector: Injector;
  constructor() {
    this.carInjector = Injector.create([
      { provide: Body, useClass: Body, deps: [] },
      { provide: Doors, useClass: Doors, deps: [] },
      { provide: Engine, useClass: Engine, deps: [] },
      { provide: Car, useClass: Car, deps: [Engine, Doors, Body] }
    ]);
    const newCar = this.createCar();
    newCar.run();
  }

  createCar(): Car {
    return this.carInjector.get(Car);
  }
}

Provider

Provider 的作用

在 Angular 中我们通过 Provider 来描述与 Token 相关联的依赖对象的创建方式。在 Angular 中依赖对象的创建方式分为以下四种:

  • useClass
  • useValue
  • useExisting
  • useFactory
Provider 的分类

在 Angular 中 Provider 主要分为:

  • ClassProvider
  • ValueProvider
  • ExistingProvider
  • FactoryProvider
Provider 的使用
  1. ClassProvider
代码语言:javascript复制
@Component({
  selector: 'drink-viewer',
  providers: [
    { provide: FoodService, useClass: FoodService }
  ],
})
  1. ValueProvider
代码语言:javascript复制
@NgModule({
  declarations: [
    AppComponent,
  ],
  providers: [
    { provide: 'api', useValue: '/api/pizzas' }
  ]
})
export class AppModule {}
  1. ExistingProvider
代码语言:javascript复制
@Component({
  selector: 'drink-viewer',
  providers: [
    FoodService,
    { provide: DrinkService, useExisting: FoodService }
  ]
})
  1. FactoryProvider
代码语言:javascript复制
export function SideFactory(http) {
  return new FoodService(http, '/api/sides');
}

@Component({
  selector: 'side-viewer',
  providers: [
    {
      provide: FoodService,
      useFactory: SideFactory,
      deps: [Http]
    }
  ],
})

在 ValueProvider 的示例中,我们使用字符串作为 token,在大多数情况下,是不会存在问题的。

代码语言:javascript复制
{ provide: 'api', useValue: '/api/pizzas' }

但假设某一天我们引入了一个第三方库,该库内部也是使用 'api' 作为 token,这时候就会导致系统出现异常。

为了解决 token 冲突问题,Angular 引入了 InjectionToken 来避免出现 token 冲突。对于上面的示例,我们可以使用 InjectionToken 来创建一个唯一的 token:

代码语言:javascript复制
export const API_TOKEN = new InjectionToken<string>('api');

使用的时候也很简单,只需要导入 API_TOKEN,然后更新 Provider 对象的 provide 属性,具体如下:

代码语言:javascript复制
providers: [
  { provide: API_TOKEN, useValue: '/api/pizzas' }
]

最后我们来介绍一下 StaticProvider,Angular 为了提高应用的性能,引入了静态注入器和 StaticProvider。在引入 StaticProvider 之前,Angular 内部通过 Reflect API 自动解析依赖对象:

代码语言:javascript复制
function _dependenciesFor(typeOrFunc: any): ReflectiveDependency[] {
  const params = reflector.parameters(typeOrFunc);
  //...
}

这个工作需要在运行时完成,而在 Angular 引入了静态注入器和 StaticProvider 之后,可以直接通过访问 Provider 对象的 provide 属性直接获取相应的依赖列表:

代码语言:javascript复制
function computeDeps(provider: StaticProvider): DependencyRecord[] {
  let deps: DependencyRecord[] = EMPTY;
  const providerDeps: any[] =
      (provider as ExistingProvider & StaticClassProvider & ConstructorProvider).deps;
}

这样在一定程度上,提高了应用程序的效率。最后感兴趣的同学,可以参考一下下图。

(图片来源于网络)(图片来源于网络)

0 人点赞