依賴注入是重要的應(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ù)中用具體的Engine
和Tires
類實例化出自己的副本徒蟆。
如果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
泻轰、Engine
和Tires
。Car
類把它的快樂建立在了消費者的痛苦之上且轨。需要某種機制為我們把這三個部分裝配好浮声。
可以寫一個巨大的類來做這件事:
// 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)建Engine
和Tires
。消費者不需要知道如何創(chuàng)建Car
倘待。開發(fā)人員不需要維護巨大的工廠類疮跑。Car
和消費者只要簡單地請求想要什么,注入器就會交付它們凸舵。
這就是關(guān)于依賴注入框架的全部祸挪。
Angular 依賴注入
Angular 搭載了自己的依賴注入框架。在這篇指南中贞间,你將會通過對一個范例應(yīng)用的討論來學(xué)習 Angular 的依賴注入技術(shù)贿条。運行在線示例 (查看源碼)雹仿。
先從英雄指南中英雄 特性的一個簡化版本開始。
HeroesComponent
是位于頂級的英雄組件整以。它唯一的用途是顯示 HeroListComponent
胧辽,而HeroListComponent
用于顯示一列英雄。
這個版本的HeroListComponent
從mockHeroes
(一個定義在獨立文件中的內(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
類絮爷。OldLogger
和NewLogger
具有相同的接口趴酣,但是由于某些原因,我們不能更新這個舊組件來使用它坑夯。
當舊組件使用OldLogger
記錄消息時岖寞,你希望改用NewLogger
的單例實例來處理它。
當一個組件請求新的或是舊 logger 時柜蜈,依賴注入應(yīng)該注入那個單例實例仗谆。OldLogger
應(yīng)該是NewLogger
的別名。
你當然不希望應(yīng)用中有兩個不同的NewLogger
實例淑履。不幸的是隶垮,如果你嘗試useClass
就會導(dǎo)致這樣的后果。
const ClassProvider(NewLogger),
const ClassProvider(OldLogger, useClass: NewLogger),
使用 ExistingProvider 以確保OldLogger
和NewLogger
提供的是同一個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
火焰,但是工廠方法可以。
同時把Logger
和UserService
注入到工廠提供器中胧沫,并且讓注入器通過工廠函數(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ù)時蚌父,可能會采用這個方法。
下一步