依賴注入是重要的應用設計模式。它使用得非常廣泛,以至于幾乎每個人都稱它為 DI。
Angular 有它自己的依賴注入框架,離開它,你幾乎沒辦法構建出 Angular 應用。
本章將學習什么是 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
類會在它的構造函數中創建所需的每樣東西。有什么問題嗎?問題在于,這個Car
類過于脆弱、缺乏彈性并且難以測試。
Car
需要一個引擎 (engine) 和一些輪胎 (tire),它沒有去請求現成的實例,而是在構造函數中用具體的Engine
和Tires
類實例化出自己的副本。
如果Engine
類升級了,它的構造函數要求一個參數,這該怎么辦?這個Car
類就被破壞了,在把創建引擎的代碼重寫為engine = new Engine(theNewParameter)
之前,它都是壞的。當第一次寫Car
類時,我們不關心Engine
構造函數的參數。現在也不想關心。但是,當Engine
類的定義發生變化時,就不得不在乎了,Car
類也不得不跟著改變。這就會讓Car
類過于脆弱。
如果想在Car
上使用不同品牌的輪胎會怎樣?太糟了。我們被鎖定在Tires
類創建時使用的那個品牌上。這讓Car
類缺乏彈性。
現在,每輛新車都有它自己的引擎。它不能和其它車輛共享引擎。雖然這對于汽車來說還算可以理解,但是設想一下那些應該被共享的依賴,比如用來聯系廠家服務中心的車載無線電。我們的car
缺乏必要的彈性,無法共享當初給其它消費者創建的車載無線電。
當給Car
類寫測試的時候,你就會受制于它隱藏的那些依賴。能在測試環境中成功創建新的Engine
嗎?Engine
自己又依賴什么?那些依賴本身又依賴什么?Engine
的新實例會發起到服務器的異步調用嗎?我們當然不想在測試期間這么一層層追下去。
如果Car
應該在輪胎氣壓低的時候閃動警示燈該怎么辦?如果沒法在測試期間換上一個低氣壓的輪胎,那該如何確認它能正確的閃警示燈?
我們沒法控制這輛車背后隱藏的依賴。當不能控制依賴時,類就會變得難以測試。
該如何讓Car
更健壯、有彈性以及易于測試?
答案超級簡單。把Car
的構造函數改造成使用 DI 的版本:
final Engine engine;
final Tires tires;
String description = 'DI';
Car(this.engine, this.tires);
發生了什么?我們把依賴的定義移到了構造函數中。Car
類不再創建引擎或者輪胎。它僅僅“消費”它們。
這個例子充分利用了 Dart 的構造函數語法來同時聲明參數和初始化屬性。
現在,通過往構造函數中傳入引擎和輪胎來創建一輛車。
// Simple car with 4 cylinders and Flintstone tires.
new Car(new Engine(), new Tires())
這太酷了,不是嗎?引擎和輪胎這兩個依賴的定義與Car
類本身解耦了。只要喜歡,可以傳入任何類型的引擎或輪胎,只要它們能滿足引擎或輪胎的通用 API 需求。
如果有人擴展了Engine
類,那就不再是Car
類的問題了。
Car
的消費者(即 car 類的實例)才有這個問題。消費者必須更新創建這輛車的代碼,就像這樣: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
類非常容易測試,因為現在我們對它的依賴有了完全的控制權。在每個測試期間,我們可以往構造函數中傳入模擬對象,做想讓它們做的事:
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';
剛剛學習了什么是依賴注入。
它是一種編程模式,可以讓類從外部源中獲得它的依賴,而不必親自創建它們。
酷!但是,可憐的消費者怎么辦?那些希望得到一個Car
的人們現在必須創建所有這三部分了: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();
}
現在只需要三個創建方法,這還不算太壞。但是當應用規模變大之后,維護它將變得驚險重重。這個工廠類將變成由相互依賴的工廠方法構成的巨型蜘蛛網。
如果能簡單的列出想建造的東西,而不用定義該把哪些依賴注入到哪些對象中,那該多好!
到了依賴注入框架一展身手的時候了。想象框架中有一個叫做注入器(injector)的東西。用這個注入器注冊一些類,它會解決如何創建它們。
當需要一個Car
時,就簡單的請求注入器獲取它就可以了。
var car = injector.get(Car);
皆大歡喜。Car
不需要知道如何創建Engine
和Tires
。消費者不需要知道如何創建Car
。開發人員不需要維護巨大的工廠類。Car
和消費者只要簡單地請求想要什么,注入器就會交付它們。
這就是關于依賴注入框架的全部。
Angular 依賴注入
Angular 搭載了自己的依賴注入框架。在這篇指南中,你將會通過對一個范例應用的討論來學習 Angular 的依賴注入技術。運行在線示例 (查看源碼)。
先從英雄指南中英雄 特性的一個簡化版本開始。
HeroesComponent
是位于頂級的英雄組件。它唯一的用途是顯示 HeroListComponent
,而HeroListComponent
用于顯示一列英雄。
這個版本的HeroListComponent
從mockHeroes
(一個定義在獨立文件中的內存集合)中獲取 heroes。
// lib/src/heroes/hero_list_component.dart (class)
class HeroListComponent {
final List<Hero> heroes = mockHeroes;
}
在開發的早期階段,這就夠用了,不過還不是很理想。一旦你試圖測試這個組件或者要從遠端服務器獲取英雄數據時,你就不得不去修改 HeroesListComponent
的實現,并要替換所有使用了mockHeroes
數據的地方。
創建一個可注入的 HeroService
最好在服務類的內部隱藏涉及英雄數據訪問的細節,把它定義在自己的文件中。
// 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 服務的必備要素。這個服務類暴露一個返回和以前一樣的模擬數據的getAll()
方法。
當然,這還不是真正的數據服務。如果該應用真的從遠端服務器獲取數據,那么getAll()
的方法簽名就應該是異步的。這樣的英雄服務是在教程的 HTTP 章節介紹的。這里的重點是服務注入,因此同步服務就足夠了。
注冊服務提供器
服務僅僅是一個類,直到你使用 Angular 的依賴注入器注冊它。
Angular 的依賴注入器負責創建服務的實例,并把它們注入到像HeroListComponent
這樣的類中。
Angular 當它執行應用時會為你創建大多數注入器,從(可選的)根注入器開始,即你提供給 runApp() 函數的參數。
在注入器能夠創建服務之前,你必須使用注入器注冊提供器。
提供器會告訴注入器如何創建該服務。如果沒有提供器,注入器既不知道它該負責創建該服務,也不知道如何創建該服務。
你會在稍后的部分學到更多關于提供器的知識。現在,只要知道它們用于創建服務,以及它們必須用注入器進行注冊就行了。
注冊提供器最常用的方式是使用任意一個有providers
列表參數的 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
的實例現在可以在HeroesComponent
及其所有的子組件中注入。
由組件提供的服務,生命周期是有限的。組件的每個新實例都會有它自己的服務實例,當組件實例被銷毀時,服務的實例也同樣會被銷毀。
在這個示例應用中,HeroesComponent
會在應用啟動時創建,并且它從未銷毀,因此,為HeroesComponent
創建的HeroService
也同樣存活在應用的整個生命周期中。
根注入器 providers
你也可以在應用的根注入器注冊提供器,即傳遞給 runApp() 函數的參數。
應用在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
的實例可以在整個應用程序中注入。
使用根注入器是供外部應用程序包聲明全應用范圍的服務的。這就是為什么注冊應用程序特定的服務是不推薦的。
首選的方法是在應用組件中注冊應用服務。因為HeroService
被用在Heroes 專題里,沒有其它地方了,所以它理想的注冊地方是在HeroesComponent
內。
下面是一個更真實的根注入器的例子,它來自教程的第五部分:
// ../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);
}
注入服務
HeroListComponent
應該從HeroService
中獲取英雄。
該組件不應該使用new
來創建HeroService
。而應該要求注入HeroService
。
你可以通過指定帶有依賴類型的構造函數參數來告訴Angular 把這個依賴注入到組件的構造函數中。下面是HeroListComponent
的構造函數,它要求注入HeroService
。
HeroListComponent(HeroService heroService)
當然,HeroListComponent
還應該使用注入的HeroService
做點什么。 下面是利用注入的服務修改后的組件。
// 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
。它唯一需要關心的事情是HeroService
是由某個父注入器提供的。
單例服務
服務在每個注入器的范圍內是單例的。在給定的注入器中,最多只會有同一個服務的一個實例。
然而,Angular DI 是一個 多級注入系統,這意味著嵌套的注入器可以創建它們自己的服務實例。Angular 始終創建嵌套的注入器。
組件的子注入器
例如,當 Angular 創建一個帶有@Component.providers
的組件新實例時,也會同時為這個實例創建一個新的子注入器。
組件注入器是彼此獨立的,每一個都會為這些組件提供的服務創建單獨的實例。
當 Angular 銷毀任何一個組件實例時,也會同時銷毀組件的注入器以及該注入器中的那些服務實例。
在注入器繼承機制的幫助下,你仍然可以把全應用級的服務注入到這些組件中。組件的注入器也是其父組件的注入器的子注入器,這同樣適用于其父組件的父組件的注入器,以此類推,最終會回到應用的根注入器。Angular 可以注入由這個注入器繼承鏈提供的任何一個注入器。
測試組件
前面強調過,設計一個適合依賴注入的類,可以讓這個類更容易測試。要有效的測試應用中的一部分,只需要在構造函數的參數中列出依賴。
例如,新建的HeroListComponent
實例使用一個模擬服務,以便可以在測試中操縱它:
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);
});
要學習更多知識,參見測試。
當服務需要另一個服務
這個HeroService
非常簡單。它本身不需要任何依賴。
如果它也有依賴,該怎么辦呢?例如,它需要通過日志服務來匯報自己的活動。我們同樣用構造函數注入模式,來添加一個帶有Logger
參數的構造函數。
下面是修改后的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;
}
}
這個構造函數要求注入一個Logger
的實例,并把它存到名為_logger
的私有屬性中。當請求英雄數據時,getAll()
方法就會記錄一個消息。
被依賴的 Logger 服務
這個示例應用的Logger
服務非常簡單:
// 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';
}
一個真實的實現可能需要使用 logging 包
如果該應用沒有提供這個Logger
服務,當 Angular 試圖把Logger
注入到HeroService
中時,就會拋出一個異常。
EXCEPTION: No provider for Logger! (HeroListComponent -> HeroService -> Logger)
由于單例的 logger 服務在應用中隨處可用,所以要在AppComponent
中注冊它。
// lib/app_component.dart (excerpt)
providers: [
const ClassProvider(Logger),
],
@Injectable()
@Injectable()
注解標識一個服務類可以被注入器實例化。通常來講,當試圖實例化一個沒有被標識為@Injectable()
的類時,注入器會報錯。
注入器同時負責實例化像HerosComponent
這樣的組件。為什么HerosComponent
沒有被標記為@Injectable()
呢?
如果你真的想的話,可以添加它。但是沒有必要,因為HeroesComponent
已經用@Component
標記了,這個注解類(和隨后將會學到的@Directive
和@Pipe
一樣)是 Injectable 的子類型。實際上,正是這些Injectable
注解把一個類標識為注入器實例化的目標。
總是帶著括號()
總是寫
@Injectable()
,而不僅僅是@Injectable
。一個元數據注解必須是一個編譯時常量的引用或一個常量構造函數的調用比如Injectable()
。如果忘了括號,分析器會顯示:"Annotation creation must have arguments"。如果你不管如何都要試圖運行這個應用,它不會工作,并且會在控制臺顯示:"expression must be a compile-time constant"。
提供器
一個服務提供器提供一個具體的、與依賴令牌(token)相關聯的運行時實例。注入器根據 提供器 創建可以注入到組件、管道和其它服務的服務的實例。
你必須使用注入器注冊一個服務的提供器,否則它不知道如何創建服務。
接下來的幾個部分會說明注冊一個提供器的多種方法。
類提供器
實現Logger
類的方法有很多。最常見的方法是使用 ClassProvider。
providers: [
const ClassProvider(Logger),
],
但這不是唯一的方法。
你可以使用另類可以實現一個Logger
的提供器來配置注入器。你可以提供一個替代的類。你可以給它一個調用一個 logger 工廠函數的提供器。在適當的情況下,這些方法中的任何一個都可能是個不錯的選擇。
重要的是,當注入器需要一個Logger
時,它得先有一個提供器。
useClass 提供器
偶爾你會請求一個不同的類來提供服務。下面的代碼告訴注入器,當有人請求Logger
時,返回一個BetterLogger
。
const ClassProvider(Logger, useClass: BetterLogger),
帶依賴的類提供器
假設EvenBetterLogger
可以在日志消息中顯示用戶名。
@Injectable()
class EvenBetterLogger extends Logger {
final UserService _userService;
EvenBetterLogger(this._userService);
String toString() => super.toString() + ' (user:${_userService.user.name})';
}
這個日志服務從注入的UserService
中取得用戶,它也在 app component 的providers
的列表中列出。
const ClassProvider(UserService),
const ClassProvider(Logger, useClass: EvenBetterLogger),
現有的提供器
假設一個舊組件依賴一個OldLogger
類。OldLogger
和NewLogger
具有相同的接口,但是由于某些原因,我們不能更新這個舊組件來使用它。
當舊組件使用OldLogger
記錄消息時,你希望改用NewLogger
的單例實例來處理它。
當一個組件請求新的或是舊 logger 時,依賴注入應該注入那個單例實例。OldLogger
應該是NewLogger
的別名。
你當然不希望應用中有兩個不同的NewLogger
實例。不幸的是,如果你嘗試useClass
就會導致這樣的后果。
const ClassProvider(NewLogger),
const ClassProvider(OldLogger, useClass: NewLogger),
使用 ExistingProvider 以確保OldLogger
和NewLogger
提供的是同一個NewLogger
實例。
const ClassProvider(NewLogger),
const ExistingProvider(OldLogger, NewLogger),
值提供器
有時,提供一個已準備好的對象比請求注入器從一個類中創建它更容易。
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。
工廠提供器
有時,我們需要動態創建這個依賴值,因為它所需要的信息直到最后一刻才能確定。也許這個信息會在瀏覽器的會話中不停地變化。
假設這個可注入的服務不能獨立訪問信息源。
這種情況下需要一個工廠提供器。
為了說明這一點,添加一個新的業務需求:HeroService
必須對普通用戶隱藏秘密 英雄。只有授權用戶才能看到秘密英雄。
就像EvenBetterLogger
那樣,HeroService
需要了解此用戶的身份。它需要知道,這個用戶是否有權看到隱藏英雄。這個授權可能在單一的應用會話中被改變,例如,用不同的用戶登錄時。
與EvenBetterLogger
不同,不能把UserService
注入到HeroService
中。HeroService
不能直接訪問用戶信息,來決定誰有授權誰沒有授權。
讓HeroService
的構造函數帶上一個布爾型的標記,來控制秘密英雄的顯示。
// 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
。你不得不通過工廠提供器創建這個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
注入到工廠提供器中,并且讓注入器通過工廠函數傳遞它們:
// lib/src/heroes/hero_service_provider.dart (provider)
const heroServiceProvider =
const FactoryProvider(HeroService, heroServiceFactory);
注意,你把這個工廠提供器賦值給了一個常量——heroServiceProvider
。這個額外的步驟讓工廠提供器可被復用。你可以在任何需要它的地方使用這個常量注冊HeroService
。
在這個例子中,你只在HeroesComponent
中需要它,這里,它代替了之前在元數據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)
當你使用一個注入器注冊提供器時,實際上是使用一個依賴注入令牌把它們關聯起來了。注入器維護一個從令牌到提供器的內部映射,當請求一個依賴時它作為參考。
類類型
在前面的所有例子中,令牌是一個類類型,提供的值是這個類型的實例。例如,通過提供HeroService
類型作為令牌,從注入器直接獲取一個HeroService
實例:
heroService = _injector.get(HeroService);
同樣的,當你定義一個HeroService
類型的構造函數參數時,Angular 就會知道注入一個HeroService
實例:
HeroListComponent(HeroService heroService)
OpaqueToken
有時候想要注入的東西是一個字符串、列表、映射乃至一個函數。例如,假使你想要注入這個應用標題呢?
const appTitle = 'Dependency Injection';
你知道值提供器適合于本例,但是你使用什么來作為令牌呢?你可以使用String
,但是如果你的應用依賴幾個這樣的注入的字符串,它并不起作用。
一個解決方案是定義并使用一個 OpaqueToken:
import 'package:angular/angular.dart';
const appTitleToken = const OpaqueToken<String>('app.title');
泛型類型參數,雖然是可選的,向開發人員和工具傳達了依賴的類型(不要搞混了OpaqueToken
構造函數參數的類型,它一直是String
)。OpaqueToken
參數令牌描述是一個開發人員的幫助。
使用OpaqueToken
對象注冊依賴提供器:
const ValueProvider.forToken(appTitleToken, appTitle)
現在,在 @Inject() 注解的幫助下,你可以把標題注入到任何需要它的構造函數中:
AppComponent(@Inject(appTitleToken) this.title);
另外,你也可以直接使用OpaqueToken
常量作為一個注解:
AppComponent(@appTitleToken this.title);
除了字符串,你還可以注入值。例如,應用有時有一個包含大量簡單屬性的 Map 類型的配置對象:
const appConfigMap = const {
'apiEndpoint': 'api.heroes.com',
'title': 'Dependency Injection',
// ...
};
const appConfigMapToken = const OpaqueToken<Map>('app.config');
自定義配置類
除了使用一個 Map 作為應用的配置對象,考慮定義一個自定義的應用配置類:
// lib/src/app_config.dart (AppConfig)
class AppConfig {
String apiEndpoint;
String title;
}
AppConfig appConfigFactory() => new AppConfig()
..apiEndpoint = 'api.heroes.com'
..title = 'Dependency Injection';
定義一個配置類有幾個好處。一個關鍵的好處是強大的靜態檢查:如果你拼錯了屬性名或分配了一個錯誤類型的值,你會提早被警告。Dart 的級聯符號(..)提供了一個初始化配置對象的方便方法。
如果使用級聯符號,配置對象不能被聲明為const
,所有你不能使用值提供器,但你可以使用工廠提供器。
// lib/app_component.dart (FactoryProvider)
const FactoryProvider(AppConfig, appConfigFactory),
你可以像下面這樣使用這個應用配置:
// lib/app_component.dart (AppComponent)
AppComponent(AppConfig config, this._userService) : title = config.title;
可選依賴
HeroService
需要一個Logger
,但是怎么在沒有 logger 的情況下也能獲取它呢?你可以在構造函數的參數中使用 @Optional() 注解,來告訴 Angular 這個依賴是可選的。
HeroService(@Optional() Logger logger) {
logger?.fine('Hello');
}
當使用@Optional()
時,你的代碼必須準備好處理一個空值。如果在其它的代碼中沒有注冊一個 logger,注入器會設置該logger
的值為null。
總結
本章,學習了 Angular 依賴注入的基礎知識。你可以注冊各種類型的提供器,并且你知道如何通過添加構造函數的參數來請求一個注入的對象(例如一個服務)。
Angular 的依賴注入比本章描述的更能干。學習關于它的更多高級特性,從對嵌套注入器的支持開始,見 多級依賴注入。
附錄:直接使用注入器
開發者很少直接使用注入器,但下面的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
本身是一個可注入的服務。
在這個例子中,Angular 把組件自身的Injector
注入到了組件的構造函數中。 然后,組件在ngOnInit()
中向注入的注入器請求它所需的服務。
注意,這些服務本身沒有注入到組件,它們是通過調用injector.get()
獲得的。
get()
方法如果不能解析所請求的服務,會拋出錯誤。調用 get() 時,還可以使用第二個參數,它是當服務不存在時該返回的值。如果沒有在當前或任何祖先注入器中注冊過,Angular 找不到服務。
這種方法是服務定位器模式的一個范例。
要避免使用此技術,除非確實需要它。它倡導了一個粗糙的存取方法,就像在這里看到的。它難以解釋、理解和測試。你不能通過檢查構造函數,來知道這個類需要什么或者它要做什么。它可以從任何祖先組件中獲得服務,而不僅僅是它自己。你會被迫研究它的實現,才可能明白它都做了什么。
當框架開發人員必須通用地、動態地獲取服務時,可能會采用這個方法。
下一步