依賴注入

版本:Angular 5.0.0-alpha

依賴注入是重要的應(yīng)用設(shè)計模式实愚。它使用得非常廣泛兼呵,以至于幾乎每個人都稱它為 DI兔辅。

Angular 有它自己的依賴注入框架,離開它萍程,你幾乎沒辦法構(gòu)建出 Angular 應(yīng)用幢妄。

本章將學(xué)習什么是 DI,它有什么用茫负,以及如何使用 Angular DI蕉鸳。

運行在線示例 (源代碼)。

為什么需要依賴注入忍法?

要理解為什么依賴注入這么重要潮尝,不妨先考慮一個不使用它的例子。想象如下代碼:

// lib/src/car/car.dart (without DI)

class Car {
  Engine engine;
  Tires tires;
  var description = 'No DI';

  Car() {
    engine = new Engine();
    tires = new Tires();
  }

  // Method using the engine and tires
  String drive() => '$description car with '
    '${engine.cylinders} cylinders and '
    '${tires.make} tires.';
}

Car類會在它的構(gòu)造函數(shù)中創(chuàng)建所需的每樣?xùn)|西饿序。有什么問題嗎勉失?問題在于,這個Car類過于脆弱原探、缺乏彈性并且難以測試乱凿。

Car需要一個引擎 (engine) 和一些輪胎 (tire),它沒有去請求現(xiàn)成的實例咽弦,而是在構(gòu)造函數(shù)中用具體的EngineTires類實例化出自己的副本徒蟆。

如果Engine類升級了,它的構(gòu)造函數(shù)要求一個參數(shù)型型,這該怎么辦段审?這個Car類就被破壞了,在把創(chuàng)建引擎的代碼重寫為engine = new Engine(theNewParameter)之前闹蒜,它都是壞的寺枉。當?shù)谝淮螌?code>Car類時,我們不關(guān)心Engine構(gòu)造函數(shù)的參數(shù)”谅洌現(xiàn)在也不想關(guān)心姥闪。但是,當Engine類的定義發(fā)生變化時砌烁,就不得不在乎了甘畅,Car類也不得不跟著改變。這就會讓Car類過于脆弱往弓。

如果想在Car上使用不同品牌的輪胎會怎樣疏唾?太糟了。我們被鎖定在Tires類創(chuàng)建時使用的那個品牌上函似。這讓Car類缺乏彈性槐脏。

現(xiàn)在,每輛新車都有它自己的引擎撇寞。它不能和其它車輛共享引擎顿天。雖然這對于汽車來說還算可以理解堂氯,但是設(shè)想一下那些應(yīng)該被共享的依賴,比如用來聯(lián)系廠家服務(wù)中心的車載無線電牌废。我們的car缺乏必要的彈性咽白,無法共享當初給其它消費者創(chuàng)建的車載無線電。

當給Car類寫測試的時候鸟缕,你就會受制于它隱藏的那些依賴晶框。能在測試環(huán)境中成功創(chuàng)建新的Engine嗎?Engine自己又依賴什么懂从?那些依賴本身又依賴什么授段?Engine的新實例會發(fā)起到服務(wù)器的異步調(diào)用嗎?我們當然不想在測試期間這么一層層追下去番甩。

如果Car應(yīng)該在輪胎氣壓低的時候閃動警示燈該怎么辦侵贵?如果沒法在測試期間換上一個低氣壓的輪胎,那該如何確認它能正確的閃警示燈缘薛?

我們沒法控制這輛車背后隱藏的依賴窍育。當不能控制依賴時,類就會變得難以測試宴胧。

該如何讓Car更健壯蔫骂、有彈性以及易于測試?

答案超級簡單牺汤。把Car的構(gòu)造函數(shù)改造成使用 DI 的版本:

final Engine engine;
final Tires tires;
String description = 'DI';
Car(this.engine, this.tires);

發(fā)生了什么?我們把依賴的定義移到了構(gòu)造函數(shù)中浩嫌。Car類不再創(chuàng)建引擎或者輪胎檐迟。它僅僅“消費”它們。

這個例子充分利用了 Dart 的構(gòu)造函數(shù)語法來同時聲明參數(shù)和初始化屬性码耐。

現(xiàn)在追迟,通過往構(gòu)造函數(shù)中傳入引擎和輪胎來創(chuàng)建一輛車。

// Simple car with 4 cylinders and Flintstone tires.

new Car(new Engine(), new Tires())

這太酷了骚腥,不是嗎敦间?引擎和輪胎這兩個依賴的定義與Car類本身解耦了。只要喜歡束铭,可以傳入任何類型的引擎或輪胎廓块,只要它們能滿足引擎或輪胎的通用 API 需求。

如果有人擴展了Engine類契沫,那就不再是Car類的問題了带猴。

Car的消費者(即 car 類的實例)才有這個問題。消費者必須更新創(chuàng)建這輛車的代碼懈万,就像這樣:

class Engine2 extends Engine {
  Engine2(cylinders) : super.withCylinders(cylinders);
}

Car superCar() =>
  // Super car with 12 cylinders and Flintstone tires.
  new Car(new Engine2(12), new Tires())
  ..description = 'Super';

這里的要點是:Car類本身不必變化拴清。稍后就來解決消費者的問題靶病。

Car類非常容易測試,因為現(xiàn)在我們對它的依賴有了完全的控制權(quán)口予。在每個測試期間娄周,我們可以往構(gòu)造函數(shù)中傳入模擬對象,做想讓它們做的事:

class MockEngine extends Engine {
  MockEngine() : super.withCylinders(8);
}

class MockTires extends Tires {
  MockTires() { make = 'YokoGoodStone'; }
}

Car testCar() =>
  // Test car with 8 cylinders and YokoGoodStone tires.
  new Car(new MockEngine(), new MockTires())
  ..description = 'Test';

剛剛學(xué)習了什么是依賴注入沪停。

它是一種編程模式煤辨,可以讓類從外部源中獲得它的依賴,而不必親自創(chuàng)建它們牙甫。

酷掷酗!但是,可憐的消費者怎么辦窟哺?那些希望得到一個Car的人們現(xiàn)在必須創(chuàng)建所有這三部分了:Car泻轰、EngineTiresCar類把它的快樂建立在了消費者的痛苦之上且轨。需要某種機制為我們把這三個部分裝配好浮声。

可以寫一個巨大的類來做這件事:

// lib/src/car/car_factory.dart

import 'car.dart';

// BAD pattern!
class CarFactory {
  Car createCar() =>
      new Car(createEngine(), createTires())
        ..description = 'Factory';

  Engine createEngine() => new Engine();
  Tires createTires() => new Tires();
}

現(xiàn)在只需要三個創(chuàng)建方法,這還不算太壞旋奢。但是當應(yīng)用規(guī)模變大之后泳挥,維護它將變得驚險重重。這個工廠類將變成由相互依賴的工廠方法構(gòu)成的巨型蜘蛛網(wǎng)至朗。

如果能簡單的列出想建造的東西屉符,而不用定義該把哪些依賴注入到哪些對象中,那該多好锹引!

到了依賴注入框架一展身手的時候了矗钟。想象框架中有一個叫做注入器(injector)的東西。用這個注入器注冊一些類嫌变,它會解決如何創(chuàng)建它們吨艇。

當需要一個Car時,就簡單的請求注入器獲取它就可以了腾啥。

var car = injector.get(Car);

皆大歡喜东涡。Car不需要知道如何創(chuàng)建EngineTires。消費者不需要知道如何創(chuàng)建Car倘待。開發(fā)人員不需要維護巨大的工廠類疮跑。Car和消費者只要簡單地請求想要什么,注入器就會交付它們凸舵。

這就是關(guān)于依賴注入框架的全部祸挪。

Angular 依賴注入

Angular 搭載了自己的依賴注入框架。在這篇指南中贞间,你將會通過對一個范例應(yīng)用的討論來學(xué)習 Angular 的依賴注入技術(shù)贿条。運行在線示例 (查看源碼)雹仿。

先從英雄指南英雄 特性的一個簡化版本開始。

HeroesComponent是位于頂級的英雄組件整以。它唯一的用途是顯示 HeroListComponent胧辽,而HeroListComponent用于顯示一列英雄。

這個版本的HeroListComponentmockHeroes(一個定義在獨立文件中的內(nèi)存集合)中獲取 heroes公黑。

// lib/src/heroes/hero_list_component.dart (class)

class HeroListComponent {
  final List<Hero> heroes = mockHeroes;
}

在開發(fā)的早期階段邑商,這就夠用了,不過還不是很理想凡蚜。一旦你試圖測試這個組件或者要從遠端服務(wù)器獲取英雄數(shù)據(jù)時人断,你就不得不去修改 HeroesListComponent的實現(xiàn),并要替換所有使用了mockHeroes數(shù)據(jù)的地方朝蜘。

創(chuàng)建一個可注入的 HeroService

最好在服務(wù)類的內(nèi)部隱藏涉及英雄數(shù)據(jù)訪問的細節(jié)恶迈,把它定義在自己的文件中。

// lib/src/heroes/hero_service.dart

import 'package:angular/angular.dart';
import 'hero.dart';
import 'mock_heroes.dart';

@Injectable()
class HeroService {
  List<Hero> getAll() => mockHeroes;
}

目前先把 @Injectable() 注解當成定義每個 Angular 服務(wù)的必備要素谱醇。這個服務(wù)類暴露一個返回和以前一樣的模擬數(shù)據(jù)的getAll()方法暇仲。

當然,這還不是真正的數(shù)據(jù)服務(wù)副渴。如果該應(yīng)用真的從遠端服務(wù)器獲取數(shù)據(jù)奈附,那么getAll()的方法簽名就應(yīng)該是異步的。這樣的英雄服務(wù)是在教程的 HTTP 章節(jié)介紹的煮剧。這里的重點是服務(wù)注入斥滤,因此同步服務(wù)就足夠了。

注冊服務(wù)提供器

服務(wù)僅僅是一個類勉盅,直到你使用 Angular 的依賴注入器注冊它佑颇。

Angular 的依賴注入器負責創(chuàng)建服務(wù)的實例,并把它們注入到像HeroListComponent這樣的類中菇篡。

Angular 當它執(zhí)行應(yīng)用時會為你創(chuàng)建大多數(shù)注入器,從(可選的)根注入器開始一喘,即你提供給 runApp() 函數(shù)的參數(shù)驱还。

在注入器能夠創(chuàng)建服務(wù)之前,你必須使用注入器注冊提供器凸克。

提供器會告訴注入器如何創(chuàng)建該服務(wù)议蟆。如果沒有提供器,注入器既不知道它該負責創(chuàng)建該服務(wù)萎战,也不知道如何創(chuàng)建該服務(wù)咐容。

你會在稍后的部分學(xué)到更多關(guān)于提供器的知識。現(xiàn)在蚂维,只要知道它們用于創(chuàng)建服務(wù)戳粒,以及它們必須用注入器進行注冊就行了路狮。

注冊提供器最常用的方式是使用任意一個有providers列表參數(shù)的 Angular 注解。最常見的注解就是 @Component()蔚约。

@Component providers

下面是修改過的HeroesComponent奄妨,把HeroService注冊到了它的providers列表中。

// lib/src/heroes/heroes_component.dart (revised)

import 'package:angular/angular.dart';

import 'hero_list_component.dart';
import 'hero_service.dart';

@Component(
    selector: 'my-heroes',
    template: '''
      <h2>Heroes</h2>
      <hero-list></hero-list>''',
    providers: [const ClassProvider(HeroService)],
    directives: [HeroListComponent])
class HeroesComponent {}

HeroService的實例現(xiàn)在可以在HeroesComponent及其所有的子組件中注入苹祟。

由組件提供的服務(wù)砸抛,生命周期是有限的。組件的每個新實例都會有它自己的服務(wù)實例树枫,當組件實例被銷毀時直焙,服務(wù)的實例也同樣會被銷毀。

在這個示例應(yīng)用中砂轻,HeroesComponent會在應(yīng)用啟動時創(chuàng)建奔誓,并且它從未銷毀,因此舔清,為HeroesComponent創(chuàng)建的HeroService也同樣存活在應(yīng)用的整個生命周期中丝里。

根注入器 providers

你也可以在應(yīng)用的根注入器注冊提供器,即傳遞給 runApp() 函數(shù)的參數(shù)体谒。

應(yīng)用在web/main.dart啟動:

// web/main.dart

@GenerateInjector([
  // For illustration purposes only (don't register app-local services here).
  const ClassProvider(HeroService),
])
final InjectorFactory rootInjector = self.rootInjector$Injector;

void main() {
  runApp(ng.AppComponentNgFactory, createInjector: rootInjector);
}

HeroService的實例可以在整個應(yīng)用程序中注入杯聚。

使用根注入器是供外部應(yīng)用程序包聲明全應(yīng)用范圍的服務(wù)的。這就是為什么注冊應(yīng)用程序特定的服務(wù)是不推薦的抒痒。

首選的方法是在應(yīng)用組件中注冊應(yīng)用服務(wù)幌绍。因為HeroService被用在Heroes 專題里,沒有其它地方了故响,所以它理想的注冊地方是在HeroesComponent內(nèi)傀广。

下面是一個更真實的根注入器的例子,它來自教程的第五部分

// ../toh-5/web/main.dart

import 'package:angular/angular.dart';
import 'package:angular_router/angular_router.dart';
import 'package:angular_tour_of_heroes/app_component.template.dart' as ng;

import 'main.template.dart' as self;

@GenerateInjector(
  routerProvidersHash, // You can use routerProviders in production
)
final InjectorFactory injector = self.injector$Injector;

void main() {
  runApp(ng.AppComponentNgFactory, createInjector: injector);
}

注入服務(wù)

HeroListComponent應(yīng)該從HeroService中獲取英雄彩届。

該組件不應(yīng)該使用new來創(chuàng)建HeroService伪冰。而應(yīng)該要求注入HeroService

你可以通過指定帶有依賴類型的構(gòu)造函數(shù)參數(shù)來告訴Angular 把這個依賴注入到組件的構(gòu)造函數(shù)中樟蠕。下面是HeroListComponent的構(gòu)造函數(shù)贮聂,它要求注入HeroService

HeroListComponent(HeroService heroService)

當然寨辩,HeroListComponent還應(yīng)該使用注入的HeroService做點什么吓懈。 下面是利用注入的服務(wù)修改后的組件。

// lib/src/heroes/hero_list_component.dart (with DI)

import 'package:angular/angular.dart';
import 'hero.dart';
import 'hero_service.dart';
@Component(
  selector: 'hero-list',
  template: '''
    <div *ngFor="let hero of heroes">
      {{hero.id}} - {{hero.name}}
    </div>''',
  directives: [coreDirectives],
)
class HeroListComponent {
  final List<Hero> heroes;
  HeroListComponent(HeroService heroService) : heroes = heroService.getAll();
}

注意靡狞,HeroListComponent并不知道HeroService來自哪里耻警。你自己知道它來自父組件HeroesComponent。它唯一需要關(guān)心的事情HeroService是由某個父注入器提供的。

單例服務(wù)

服務(wù)在每個注入器的范圍內(nèi)是單例的甘穿。在給定的注入器中腮恩,最多只會有同一個服務(wù)的一個實例。

然而扒磁,Angular DI 是一個 多級注入系統(tǒng)庆揪,這意味著嵌套的注入器可以創(chuàng)建它們自己的服務(wù)實例。Angular 始終創(chuàng)建嵌套的注入器妨托。

組件的子注入器

例如缸榛,當 Angular 創(chuàng)建一個帶有@Component.providers的組件新實例時,也會同時為這個實例創(chuàng)建一個新的子注入器兰伤。

組件注入器是彼此獨立的内颗,每一個都會為這些組件提供的服務(wù)創(chuàng)建單獨的實例。

當 Angular 銷毀任何一個組件實例時敦腔,也會同時銷毀組件的注入器以及該注入器中的那些服務(wù)實例均澳。

注入器繼承機制的幫助下,你仍然可以把全應(yīng)用級的服務(wù)注入到這些組件中符衔。組件的注入器也是其父組件的注入器的子注入器找前,這同樣適用于其父組件的父組件的注入器,以此類推判族,最終會回到應(yīng)用的注入器躺盛。Angular 可以注入由這個注入器繼承鏈提供的任何一個注入器。

測試組件

前面強調(diào)過形帮,設(shè)計一個適合依賴注入的類槽惫,可以讓這個類更容易測試。要有效的測試應(yīng)用中的一部分辩撑,只需要在構(gòu)造函數(shù)的參數(shù)中列出依賴界斜。

例如,新建的HeroListComponent實例使用一個模擬服務(wù)合冀,以便可以在測試中操縱它:

var expectedHeroes = [new Hero(0, 'A'), new Hero(1, 'B')];
var mockService = new MockHeroService(expectedHeroes);
it('should have heroes when HeroListComponent created', () {
  var hlc = new HeroListComponent(mockService);
  expect(hlc.heroes.length).toEqual(expectedHeroes.length);
});

要學(xué)習更多知識各薇,參見測試

當服務(wù)需要另一個服務(wù)

這個HeroService非常簡單君躺。它本身不需要任何依賴峭判。

如果它也有依賴,該怎么辦呢晰洒?例如朝抖,它需要通過日志服務(wù)來匯報自己的活動啥箭。我們同樣用構(gòu)造函數(shù)注入模式谍珊,來添加一個帶有Logger參數(shù)的構(gòu)造函數(shù)。

下面是修改后的HeroService,它注入了Logger

// lib/src/heroes/hero_service.dart (v2)

import 'package:angular/angular.dart';
import '../logger_service.dart';
import 'hero.dart';
import 'mock_heroes.dart';
@Injectable()
class HeroService {
  final Logger _logger;
  HeroService(this._logger);
  List<Hero> getAll() {
    _logger.fine('Getting heroes ...');
    return mockHeroes;
  }
}

這個構(gòu)造函數(shù)要求注入一個Logger的實例砌滞,并把它存到名為_logger的私有屬性中侮邀。當請求英雄數(shù)據(jù)時,getAll()方法就會記錄一個消息贝润。

被依賴的 Logger 服務(wù)

這個示例應(yīng)用的Logger服務(wù)非常簡單:

// lib/src/logger_service.dart

import 'package:angular/angular.dart';

@Injectable()
/// Logger that keeps only the last log entry.
class Logger {
  String _log = '';

  void fine(String msg) => _log = msg;

  @override
  String toString() => '[$runtimeType] $_log';
}

一個真實的實現(xiàn)可能需要使用 logging 包

如果該應(yīng)用沒有提供這個Logger服務(wù)绊茧,當 Angular 試圖把Logger注入到HeroService中時,就會拋出一個異常打掘。

  EXCEPTION: No provider for Logger! (HeroListComponent -> HeroService -> Logger)

由于單例的 logger 服務(wù)在應(yīng)用中隨處可用华畏,所以要在AppComponent中注冊它。

// lib/app_component.dart (excerpt)

providers: [
  const ClassProvider(Logger),
],

@Injectable()

@Injectable()注解標識一個服務(wù)類可以被注入器實例化尊蚁。通常來講亡笑,當試圖實例化一個沒有被標識為@Injectable()的類時,注入器會報錯横朋。

注入器同時負責實例化像HerosComponent這樣的組件仑乌。為什么HerosComponent沒有被標記為@Injectable()呢?

如果你真的想的話琴锭,可以添加它晰甚。但是沒有必要,因為HeroesComponent已經(jīng)用@Component標記了决帖,這個注解類(和隨后將會學(xué)到的@Directive@Pipe一樣)是 Injectable 的子類型厕九。實際上,正是這些Injectable注解把一個類標識為注入器實例化的目標古瓤。

總是帶著括號()

總是寫@Injectable()止剖,而不僅僅是@Injectable。一個元數(shù)據(jù)注解必須是一個編譯時常量的引用或一個常量構(gòu)造函數(shù)的調(diào)用比如Injectable()落君。

如果忘了括號穿香,分析器會顯示:"Annotation creation must have arguments"。如果你不管如何都要試圖運行這個應(yīng)用绎速,它不會工作皮获,并且會在控制臺顯示:"expression must be a compile-time constant"。

提供器

一個服務(wù)提供器提供一個具體的纹冤、與依賴令牌(token)相關(guān)聯(lián)的運行時實例洒宝。注入器根據(jù) 提供器 創(chuàng)建可以注入到組件、管道和其它服務(wù)的服務(wù)的實例萌京。

你必須使用注入器注冊一個服務(wù)的提供器雁歌,否則它不知道如何創(chuàng)建服務(wù)。

接下來的幾個部分會說明注冊一個提供器的多種方法知残。

類提供器

實現(xiàn)Logger類的方法有很多靠瞎。最常見的方法是使用 ClassProvider

providers: [
  const ClassProvider(Logger),
],

但這不是唯一的方法。

你可以使用另類可以實現(xiàn)一個Logger的提供器來配置注入器乏盐。你可以提供一個替代的類佳窑。你可以給它一個調(diào)用一個 logger 工廠函數(shù)的提供器。在適當?shù)那闆r下父能,這些方法中的任何一個都可能是個不錯的選擇神凑。

重要的是,當注入器需要一個Logger時何吝,它得先有一個提供器溉委。

useClass 提供器

偶爾你會請求一個不同的類來提供服務(wù)。下面的代碼告訴注入器爱榕,當有人請求Logger時薛躬,返回一個BetterLogger

const ClassProvider(Logger, useClass: BetterLogger),

帶依賴的類提供器

假設(shè)EvenBetterLogger可以在日志消息中顯示用戶名呆细。

@Injectable()
class EvenBetterLogger extends Logger {
  final UserService _userService;

  EvenBetterLogger(this._userService);

  String toString() => super.toString() + ' (user:${_userService.user.name})';
}

這個日志服務(wù)從注入的UserService中取得用戶型宝,它也在 app component 的providers的列表中列出。

const ClassProvider(UserService),
const ClassProvider(Logger, useClass: EvenBetterLogger),

現(xiàn)有的提供器

假設(shè)一個舊組件依賴一個OldLogger類絮爷。OldLoggerNewLogger具有相同的接口趴酣,但是由于某些原因,我們不能更新這個舊組件來使用它坑夯。

當舊組件使用OldLogger記錄消息時岖寞,你希望改用NewLogger的單例實例來處理它。

當一個組件請求新的或是舊 logger 時柜蜈,依賴注入應(yīng)該注入那個單例實例仗谆。OldLogger應(yīng)該是NewLogger的別名。

你當然不希望應(yīng)用中有兩個不同的NewLogger實例淑履。不幸的是隶垮,如果你嘗試useClass就會導(dǎo)致這樣的后果。

const ClassProvider(NewLogger),
const ClassProvider(OldLogger, useClass: NewLogger),

使用 ExistingProvider 以確保OldLoggerNewLogger提供的是同一個NewLogger實例秘噪。

const ClassProvider(NewLogger),
const ExistingProvider(OldLogger, NewLogger),

值提供器

有時狸吞,提供一個已準備好的對象比請求注入器從一個類中創(chuàng)建它更容易。

class SilentLogger implements Logger {
  const SilentLogger();
  @override
  void fine(String msg) {}
  @override
  String toString() => '';
}

const silentLogger = const SilentLogger();

然后使用 ValueProvider 來注冊這個對象指煎。

const ValueProvider(Logger, silentLogger),

更多ValueProvider的例子蹋偏,請看:OpaqueToken

工廠提供器

有時至壤,我們需要動態(tài)創(chuàng)建這個依賴值威始,因為它所需要的信息直到最后一刻才能確定。也許這個信息會在瀏覽器的會話中不停地變化像街。

假設(shè)這個可注入的服務(wù)不能獨立訪問信息源黎棠。

這種情況下需要一個工廠提供器京郑。

為了說明這一點,添加一個新的業(yè)務(wù)需求:HeroService必須對普通用戶隱藏秘密 英雄葫掉。只有授權(quán)用戶才能看到秘密英雄。

就像EvenBetterLogger那樣跟狱,HeroService需要了解此用戶的身份俭厚。它需要知道,這個用戶是否有權(quán)看到隱藏英雄驶臊。這個授權(quán)可能在單一的應(yīng)用會話中被改變挪挤,例如,用不同的用戶登錄時关翎。

EvenBetterLogger不同扛门,不能把UserService注入到HeroService中。HeroService不能直接訪問用戶信息纵寝,來決定誰有授權(quán)誰沒有授權(quán)论寨。

HeroService的構(gòu)造函數(shù)帶上一個布爾型的標記,來控制秘密英雄的顯示爽茴。

// lib/src/heroes/hero_service.dart (excerpt)

final Logger _logger;
final bool _isAuthorized;

HeroService(this._logger, this._isAuthorized);

List<Hero> getAll() {
  var auth = _isAuthorized ? 'authorized' : 'unauthorized';
  _logger.fine('Getting heroes for $auth user.');
  return mockHeroes
      .where((hero) => _isAuthorized || !hero.isSecret)
      .toList();
}

你可以注入Logger葬凳,但是不能注入布爾型的isAuthorized。你不得不通過工廠提供器創(chuàng)建這個HeroService的新實例室奏。

工廠提供器需要一個工廠方法:

// lib/src/heroes/hero_service_provider.dart (factory)

HeroService heroServiceFactory(Logger logger, UserService userService) =>
    new HeroService(logger, userService.user.isAuthorized);

雖然HeroService不能訪問UserService火焰,但是工廠方法可以。

同時把LoggerUserService注入到工廠提供器中胧沫,并且讓注入器通過工廠函數(shù)傳遞它們:

// lib/src/heroes/hero_service_provider.dart (provider)

const heroServiceProvider =
    const FactoryProvider(HeroService, heroServiceFactory);

注意昌简,你把這個工廠提供器賦值給了一個常量——heroServiceProvider。這個額外的步驟讓工廠提供器可被復(fù)用绒怨。你可以在任何需要它的地方使用這個常量注冊HeroService纯赎。

在這個例子中,你只在HeroesComponent中需要它南蹂,這里址否,它代替了之前在元數(shù)據(jù)providers列表中注冊的HeroService

// lib/src/heroes/heroes_component.dart (v3)

import 'package:angular/angular.dart';
import 'hero_list_component.dart';
import 'hero_service_provider.dart';
@Component(
    selector: 'my-heroes',
    template: '''
      <h2>Heroes</h2>
      <hero-list></hero-list>''',
    providers: [heroServiceProvider],
    directives: [HeroListComponent])
class HeroesComponent {}

令牌(Tokens)

當你使用一個注入器注冊提供器時碎紊,實際上是使用一個依賴注入令牌把它們關(guān)聯(lián)起來了佑附。注入器維護一個從令牌到提供器的內(nèi)部映射,當請求一個依賴時它作為參考仗考。

類類型

在前面的所有例子中音同,令牌是一個類類型,提供的值是這個類型的實例秃嗜。例如权均,通過提供HeroService類型作為令牌顿膨,從注入器直接獲取一個HeroService實例:

heroService = _injector.get(HeroService);

同樣的,當你定義一個HeroService類型的構(gòu)造函數(shù)參數(shù)時叽赊,Angular 就會知道注入一個HeroService實例:

HeroListComponent(HeroService heroService)

OpaqueToken

有時候想要注入的東西是一個字符串恋沃、列表、映射乃至一個函數(shù)必指。例如囊咏,假使你想要注入這個應(yīng)用標題呢?

const appTitle = 'Dependency Injection';

你知道值提供器適合于本例塔橡,但是你使用什么來作為令牌呢梅割?你可以使用String,但是如果你的應(yīng)用依賴幾個這樣的注入的字符串葛家,它并不起作用户辞。

一個解決方案是定義并使用一個 OpaqueToken

import 'package:angular/angular.dart';

const appTitleToken = const OpaqueToken<String>('app.title');

泛型類型參數(shù),雖然是可選的癞谒,向開發(fā)人員和工具傳達了依賴的類型(不要搞混了OpaqueToken構(gòu)造函數(shù)參數(shù)的類型底燎,它一直是String)。OpaqueToken參數(shù)令牌描述是一個開發(fā)人員的幫助弹砚。

使用OpaqueToken對象注冊依賴提供器:

const ValueProvider.forToken(appTitleToken, appTitle)

現(xiàn)在书蚪,在 @Inject() 注解的幫助下,你可以把標題注入到任何需要它的構(gòu)造函數(shù)中:

AppComponent(@Inject(appTitleToken) this.title);

另外迅栅,你也可以直接使用OpaqueToken常量作為一個注解:

AppComponent(@appTitleToken this.title);

除了字符串殊校,你還可以注入值。例如读存,應(yīng)用有時有一個包含大量簡單屬性的 Map 類型的配置對象:

const appConfigMap = const {
  'apiEndpoint': 'api.heroes.com',
  'title': 'Dependency Injection',
  // ...
};

const appConfigMapToken = const OpaqueToken<Map>('app.config');

自定義配置類

除了使用一個 Map 作為應(yīng)用的配置對象为流,考慮定義一個自定義的應(yīng)用配置類:

// lib/src/app_config.dart (AppConfig)

class AppConfig {
  String apiEndpoint;
  String title;
}

AppConfig appConfigFactory() => new AppConfig()
  ..apiEndpoint = 'api.heroes.com'
  ..title = 'Dependency Injection';

定義一個配置類有幾個好處。一個關(guān)鍵的好處是強大的靜態(tài)檢查:如果你拼錯了屬性名或分配了一個錯誤類型的值让簿,你會提早被警告敬察。Dart 的級聯(lián)符號(..)提供了一個初始化配置對象的方便方法。

如果使用級聯(lián)符號尔当,配置對象不能被聲明為const莲祸,所有你不能使用值提供器,但你可以使用工廠提供器椭迎。

// lib/app_component.dart (FactoryProvider)

const FactoryProvider(AppConfig, appConfigFactory),

你可以像下面這樣使用這個應(yīng)用配置:

// lib/app_component.dart (AppComponent)

AppComponent(AppConfig config, this._userService) : title = config.title;

可選依賴

HeroService需要一個Logger锐帜,但是怎么在沒有 logger 的情況下也能獲取它呢?你可以在構(gòu)造函數(shù)的參數(shù)中使用 @Optional() 注解畜号,來告訴 Angular 這個依賴是可選的缴阎。

HeroService(@Optional() Logger logger) {
  logger?.fine('Hello');
}

當使用@Optional()時,你的代碼必須準備好處理一個空值简软。如果在其它的代碼中沒有注冊一個 logger蛮拔,注入器會設(shè)置該logger的值為null。

總結(jié)

本章肛跌,學(xué)習了 Angular 依賴注入的基礎(chǔ)知識。你可以注冊各種類型的提供器,并且你知道如何通過添加構(gòu)造函數(shù)的參數(shù)來請求一個注入的對象(例如一個服務(wù))。

Angular 的依賴注入比本章描述的更能干。學(xué)習關(guān)于它的更多高級特性,從對嵌套注入器的支持開始,見 多級依賴注入

附錄:直接使用注入器

開發(fā)者很少直接使用注入器,但下面的InjectorComponent使用了帕胆。

// lib/src/injector_component.dart (injector)

@Component(
  selector: 'my-injectors',
  template: '''
      <h2>Other Injections</h2>
      <div id="car">{{car.drive()}}</div>
      <div id="hero">{{hero.name}}</div>
      <div id="rodent">{{rodent}}</div>''',
  providers: [
    const ClassProvider(Car),
    const ClassProvider(Engine),
    const ClassProvider(Tires),
    heroServiceProvider,
    const ClassProvider(Logger),
  ],
)
class InjectorComponent implements OnInit {
  final Injector _injector;
  Car car;
  HeroService heroService;
  Hero hero;

  InjectorComponent(this._injector);

  @override
  void ngOnInit() {
    car = _injector.get(Car);
    heroService = _injector.get(HeroService);
    hero = heroService.getAll()[0];
  }

  String get rodent =>
      _injector.get(ROUS, "R.O.U.S.'s? I don't think they exist!");
}

Injector本身是一個可注入的服務(wù)何陆。

在這個例子中巩剖,Angular 把組件自身的Injector注入到了組件的構(gòu)造函數(shù)中鞠鲜。 然后坐漏,組件在ngOnInit()中向注入的注入器請求它所需的服務(wù)慨畸。

注意,這些服務(wù)本身沒有注入到組件荧飞,它們是通過調(diào)用injector.get()獲得的岸晦。

get()方法如果不能解析所請求的服務(wù)欧啤,會拋出錯誤邢隧。調(diào)用 get() 時倒慧,還可以使用第二個參數(shù),它是當服務(wù)不存在時該返回的值口柳。如果沒有在當前或任何祖先注入器中注冊過苹粟,Angular 找不到服務(wù)。

這種方法是服務(wù)定位器模式的一個范例望艺。

避免使用此技術(shù)苛秕,除非確實需要它。它倡導(dǎo)了一個粗糙的存取方法找默,就像在這里看到的艇劫。它難以解釋、理解和測試惩激。你不能通過檢查構(gòu)造函數(shù)店煞,來知道這個類需要什么或者它要做什么蟹演。它可以從任何祖先組件中獲得服務(wù),而不僅僅是它自己浅缸。你會被迫研究它的實現(xiàn)轨帜,才可能明白它都做了什么。

當框架開發(fā)人員必須通用地衩椒、動態(tài)地獲取服務(wù)時蚌父,可能會采用這個方法。

下一步

模板語法

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末毛萌,一起剝皮案震驚了整個濱河市苟弛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌阁将,老刑警劉巖膏秫,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異做盅,居然都是意外死亡缤削,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門吹榴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來亭敢,“玉大人,你說我怎么就攤上這事图筹∷У叮” “怎么了?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵远剩,是天一觀的道長扣溺。 經(jīng)常有香客問我,道長瓜晤,這世上最難降的妖魔是什么锥余? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮痢掠,結(jié)果婚禮上驱犹,老公的妹妹穿的比我還像新娘。我一直安慰自己志群,他們只是感情好着绷,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著锌云,像睡著了一般荠医。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天彬向,我揣著相機與錄音兼贡,去河邊找鬼。 笑死娃胆,一個胖子當著我的面吹牛遍希,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播里烦,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼凿蒜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了胁黑?” 一聲冷哼從身側(cè)響起废封,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎丧蘸,沒想到半個月后漂洋,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡力喷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年刽漂,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弟孟。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡贝咙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出披蕉,到底是詐尸還是另有隱情颈畸,我是刑警寧澤乌奇,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布没讲,位于F島的核電站,受9級特大地震影響礁苗,放射性物質(zhì)發(fā)生泄漏爬凑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一试伙、第九天 我趴在偏房一處隱蔽的房頂上張望嘁信。 院中可真熱鬧,春花似錦疏叨、人聲如沸潘靖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽卦溢。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間单寂,已是汗流浹背贬芥。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留宣决,地道東北人蘸劈。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像尊沸,于是被迫代替她去往敵國和親威沫。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內(nèi)容