Angular 2中的依賴注入

來源 http://kittencup.com/javascript/2015/07/23/Angular%202%E4%B8%AD%E7%9A%84%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5.html


原文地址:http://blog.thoughtram.io/angular/2015/05/18/dependency-injection-in-angular-2.html

依賴注入一直是Angular的一個最大特點和賣點。它允許在我們應用不同組件中注入依賴,而不需要知道這些依賴是如何創建的,或者它們需要的依賴關係是什麼,可是,已證明了目前的Angular 1的依賴注入系統有一些問題,所以建立了下一代框架Angular 2來解決這些問題,在這篇文章中,我們將探索新的依賴注入系統。

在我們進入新的依賴注入系統之前,先讓我們先了解什麼是依賴注入,而Angular 1的問題又是什麼問題。

依賴注入是一種模式

在ng-conf 2014中Vojta Jina有一個關於依賴注入的演講,在這次演講中,它討論了在開發Angular 2中關於新的DI系統的想法和故事,他很清楚,我們可以把DI看成兩件事:作爲一種設計模式和作爲一個框架。而前者用來解釋DI模式,後者可以幫助我們的系統,維護和組裝依賴關係。這篇文章中我想做同樣的事來幫助我們理解這一概念。

我們首先考慮看看下面的代碼。

class Car {
  constructor() {
    this.engine = new Engine();
    this.tires = Tires.getInstance();
    this.doors = app.get('doors');
  }}

在這裏沒什麼特別的東西,我們有一個Car類,我們在構造函數中建立了構建一輛汽車需要的對象,此代碼有什麼問題嗎?你可以看到,構造函數不僅分配需要的依賴關係到內部屬性,也知道如何創建這些對象。 例如engine屬性是使用Engine構造函數創建的,Tires似乎是一個單例的對象,doors是從一個全局對象中獲取的,全局對象類似於服務定位(service locator)。

這導致代碼難以維護,甚至更難測試。想象一下你想測試這個類,在代碼中你將如何替換依賴Engine爲MockEngine? 當編寫測試時,我們想要使用代碼在不同的場景中測試,因此每個場景需要它自己的配置。 如果我們想寫測試代碼,我們需要編寫可重用的代碼。只要所有依賴關係都滿足,我們的代碼應該在任何環境中工作。這給我們帶來的結論是可測試的代碼是可重用的代碼,反之亦然。

那麼如何才能更好的寫出這樣的代碼,使它更容易測試?這是超級容易,你可能已經知道該怎麼做。我們把代碼改成這樣:

class Car {
  constructor(engine, tires, doors) {
    this.engine = engine;
    this.tires = tires;
    this.doors = doors;
  }}

所有我們做的是,我們將期望所有需要依賴的對象從構造函數創建移動到構造函數的參數創建,在這個代碼中沒有具體的實現,我們從字面上把創建這些依賴關係的責任轉移到一個更高的層次上。如果我們想創建一個Car對象,我們所要做的就是把所有需要的依賴關係傳遞給構造函數:

var car = new Car(
  new Engine(),
  new Tires(),
  new Doors());

這太酷了,不是嗎?依賴從我們的類脫離出來,在我們編寫測試中,我們可以通過mock依賴實現測試

var car = new Car(
  new MockEngine(),
  new MockTires(),
  new MockDoors());

這就是依賴注入。更具體一點,這個特定的模式也被稱爲構造函數注入。還有兩個注入模式,setter注入和interface注入,但我們將不會在本文中介紹這些內容。

好酷,現在我們正在使用DI,但是什麼時候來一個DI系統呢?如前所述,我們從字面上把依賴性創造的責任轉移到一個更高的水平。這正是我們新的問題。誰來負責組裝所有這些依賴關係?是我們。

function main() {
  var engine = new Engine();
  var tires = new Tires();
  var doors = new Doors();
  var car = new Car(engine, tires, doors);
  car.drive();}

我們必須保留一個main函數,這樣做是相當危險的,尤其是當應用程序變得越來越大,如果我們能做點像這樣的事情,那是不是更好?

function main() {
  var injector = new Injector(...)
  var car = injector.get(Car);

  car.drive();}

依賴注入作爲一個框架

這是依賴注入作爲框架的地方。衆所周知,Angular 1有它自己的DI系統,允許我們註釋服務和其他組件,讓injector發現他們知道什麼依賴需要實例化,例如,下面的代碼演示瞭如何在Angular 1中註釋我們的Car類:

class Car {
  ...}Car.$inject = ['Engine', 'Tires', 'Doors'];

然後,我們註冊我們的Car作爲一個服務,每當我們使用它時,我們得到一個單例,它不需要關心Car需要創建的依賴。

var app = angular.module('myApp', []);app.service('Car', Car);app.service('OtherService', function (Car) { 
  // instance of Car available});

這一切都很酷,但事實證明,現有的DI有一些問題:

  • 內部緩存 - 依賴是單例的,當我們請求一個服務時,它在每個應用的生命週期中只會創建一次,創建工廠來解決這個問題也是相當危險的。

  • 默認是同步的 - 在Angular 1 中沒有方便的方式來解決加載異步依賴

  • 命名空間碰撞 - 在應用程序中只能有一個“type”的標記。會有一個問題,如果我們有car服務,然而第3方擴展也引入了一個相同名字的服務。

  • 內置框架 - Angular 1 Di被嵌入在整個框架中,我們無法使用它作爲一個獨立的系統用來解耦。

這些問題需要得到解決,以便使Angular的DI達到下一個水平。

Angular 2中得依賴注入

在看實際代碼之前,讓我們首先了解新的DI系統背後的概念。下面的圖片說明了新的DI系統所需的組件:

在Angular 2中DI基本上是由三個東西組成的:

  • Injector - injector對象是用來給我們創建依賴實例的API。

  • Provider - provider類似食譜,告訴Injector如何創建一個依賴實例。綁定需要一個token,映射到一個工廠的函數,創建一個對象。

  • Dependency - dependency是應該要創建的對象的type。

好吧,現在我們有一個概念,讓我們看看翻譯成代碼是什麼樣子的。我們繼續保持Car類的依賴關係。下面是我們如何能夠利用Angular 2的DI拿到Car的一個實例:

import { Injector } from 'angular2/di';var injector = Injector.resolveAndCreate([
  Car,
  Engine,
  Tires,
  Doors]);var car = injector.get(Car)

我們從Angular 2中導入Injector模塊,這暴露一些靜態api,Injector.resolveAndCreate()基本上是一個工廠函數用來創建一個injector,然後提供一個provider列表。我們稍後將探討如何將class提供給provider,但現在我們將焦點關注在injector.get()方法上。在代碼最後行告訴我們怎麼樣去獲取一個Car實例,我們的inject怎麼知道需要創建依賴關係來實例化一個Car,看看我們的Car類來解釋下爲什麼...

import { Inject } from 'angular2/di';class Car {
  constructor(
    @Inject(Engine) engine,
    @Inject(Tires) tires,
    @Inject(Doors) doors
  ) {
    ...
  }}

我們從框架中導入了Inject的模塊,用它來裝飾(decorator)我們的構造函數參數。如果你不知道decorator是什麼,你可能會想讀我們的文章 decorators 和 annotations兩者的不同使用ES5編寫Angular2代碼

Inject decorator會在我們的類上附加些元信息,之後會被DI系統給讀取,所以基本上我們在這裏所做的是告訴DI第一個參數是Engine類型的實例,第二個參數是Tires類型,第3個參數是Doors類型。我們可以使用TypeScript重寫這些代碼,感覺有點更自然:

class Car {
  constructor(engine: Engine, tires: Tires, doors: Doors) {
    ...
  }}

好的,我們的類聲明它自己的依賴與DI可以讀的實例信息,但Injector如何知道要如何創建這樣一個對象?這就是provider的作用。記得resolveAndCreate()方法中,我們傳遞一個數組形式的類列表嗎?

var injector = Injector.resolveAndCreate([
  Car,
  Engine,
  Tires,
  Doors]);

同樣,你可能會想知道這個類的列表應該是一個provider列表。,如果我把寫得這些轉換爲更詳細的語法,那麼可能會變得有點更加清晰。

import {provide} from 'angular2/angular2';var injector = Injector.resolveAndCreate([
  provide(Car, {useClass: Car}),
  provide(Engine, {useClass: Engine}),
  provide(Tires, {useClass: Tires}),
  provide(Doors {useClass: Doors})]);

我們有一個provide函數,他會映射一個token到配置的object上,token可以是一種type或者字符串,如果你現在閱讀那些providers,你很容易理解發生了什麼,我們綁定Car類型到Car類上,Engine類型到Engine類上等。這是我們之前所談到的食譜機制。 因此,我們不僅僅讓injector知道哪種dependencies是被用到應用程序中的,我們還配置如何創建這些依賴關係的對象。

現在,下一個問題來了,我們要使用更長的寫法而不是簡寫語法嗎?我們如果可以寫成Foo,那就沒有理由寫成provide(Foo, {useClass: Foo}),對嗎。這就是爲什麼我們開始首先使用簡潔的語法。然而,較長的語法使我們能夠做一些非常非常強大的事。看看下一個代碼片段

provide(Engine, {useClass: OtherEngine})

對,我們可以綁定一個token到任何想要綁定的東西上,在這裏綁定Engine token到OtherEngine類上,這意味着,當我們申請獲取Engine類型時,我們會獲取類OtherEngine的一個實例。

這是超級強大的,因爲這不僅爲了讓我們防止名稱衝突,我們也可以創建一個接口的類型並將它綁定到一個具體的實現。除此之外,我們可以在一個單一的地方不接觸任何其他代碼使用一個token換出它實際的依賴,

Angular 2中DI的幾個其他綁定方法將在下一節中探索。

其他provider配置

有時候,我們不希望得到一個類的一個實例,通過更多的配置我們可以得到一個單一的值或者工廠方法,異步依賴關係也可以是我們的應用的一部分,這就是爲什麼Angular 2的DI的provider機制帶有不止一個方法。讓我們快速瀏覽一下它們。

我們想要簡單的綁定到值可以使用 {useValue: value}

provide(String, {useValue: 'Hello World'})

當我們要綁定到簡單的配置值時,這就很方便了。

別名

我們可以給一個token綁定一個別名token

provide(Engine, {useClass: Engine})provide(V8, {useExisting: Engine})

工廠

是的,我們最喜愛的工廠。

provide(Engine, {useFactory: () => {
  return function () {
    if (IS_V8) {
      return new V8Engine();
    } else {
      return new V6Engine();
    }
  }}})

當然,工廠可能有它自己的依賴關係。通過對工廠的依賴性很容易給工廠添加一個tokens列表:

provide(Engine, {
  useFactory: (car, engine) => {

  },
  deps: [Car, Engine]})

可選依賴

該@Optional decorator讓我們聲明依賴可選。這遲早會有用,例如,我們的應用程序需要一個第三方庫,如果不存在的話,可以fallback

class Car {
  constructor(@Optional(jQuery) $) {
    if (!$) {
    // set up fallback
    }
  }}

正如你所看到的,Angular 2 DI解決了Angular 1 DI的前3個問題。但還有一件事,我們還沒有談到呢。新的DI是否還是創建單例對象

短暫(Transient)的依賴和child injectors

如果我們想要一個短暫的依賴,每一次獲取依賴都創建一個新的實例,我們有2個選擇:

工廠會返回一個類的實例,這將不會是單例.

provide(Engine, {useFactory: () => {
  return () => {
    return new Engine();
  }}})

我們也可以使用 Injector.resolveAndCreateChild() 來創建一個child injector,一個child injector在綁定一個對象實例時,將不同於老的injector的實例。

var injector = Injector.resolveAndCreateChild([Engine]);var childInjector = Injector.resolveAndCreateChild([Engine]);injector.get(Engine) !== childInjector.get(Engine);

child injectors 也更加有趣。如果原來child injector上沒有給定binding,他會查找綁定在parent injector上的token來進行綁定 ,

圖片顯示了3個injector,其中有2個是child injector,每一個injector都有自己的配置,現在我要從第2個child injector獲取Car類型的實例,Car對象會被該child injector創建, 然而,engine將會被第一個child injector創建,tires 和doors會被parent injector創建,這個有點像原型鏈。

我們甚至可以配置可見性的依賴關係,這將在另一篇文章中講到Host and Visibility in Angular 2's Dependency Injection,

在Angular 2中如何使用?

現在我們已經學習了DI如果在Angular 2運作,你可能會想知道它是如何使用在框架本身,我們在建立Angular 2組件時,是否需要手動創建injector? 幸運的是,Angular花了大量的精力和時間去設計出一個很好的API使Angular組件中隱藏了所有的injector。

讓我們來來下面這個簡單的Angular 2組件

@Component({
  selector: 'app'})@View({
  template: '<h1>Hello !</h1>'})class App {
  constructor() {
    this.name = 'World';
  }}bootstrap(App);
class NameService {
  constructor() {
    this.name = 'Pascal';
  }
  getName() {
    return this.name;
  }}

現在爲了在我們的應用中使用NameService,我們需要通過在應用injector中提供provider配置,但是我們該如何做?我們還沒有創建一個injector。

bootstrap(),在引導時,會我們的應用程序創建一個的根injector,在其第2個參數創建一個provider列表,將直接傳遞到Injector,換句話說,我們需要:

bootstrap(App, [NameService]);

就是這樣。現在我們在應用中使用@Inject decorator就可以使用NameService

class App {
  constructor(@Inject(NameService) NameService) {
    this.name = NameService.getName();
  }}

或者 使用typescript,我們可以使用參數類型來注入

class App {
  constructor(NameService: NameService) {
    this.name = NameService.getName();
  }}

真棒,一下子,我們再也沒有任何Injector了,但是還有一件事是:如果我們想在特定的組件中有不同的依賴配置,我們需要怎麼做?

比方說,我們有一個NameService實例,在應用程序中NameService實例類型將被廣泛注入,但有一個特定組件應該得到另一個不同的NameService實例,可使用@component註解的providers屬性,它允許我們添加providers到一個特定的組件(和它的子組件)。

@Component({
  selector: 'app',
  providers: [NameService]})@View({
  template: '<h1>Hello !</h1>'})class App {
  ...}

爲了把事情說清楚:providers不配置將要注入的實例,而是爲當前組件創建一個child injector並配置,如前所述, 我們也可以配置我們綁定的可見性,以更加具體的哪一個組件可以注入什麼。例如。該viewProviders屬性只允許依賴被當前組件使用。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章