依赖注入是重要的应用设计模式。它使用得非常广泛,以至于几乎每个人都称它为 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 找不到服务。
这种方法是服务定位器模式的一个范例。
要避免使用此技术,除非确实需要它。它倡导了一个粗糙的存取方法,就像在这里看到的。它难以解释、理解和测试。你不能通过检查构造函数,来知道这个类需要什么或者它要做什么。它可以从任何祖先组件中获得服务,而不仅仅是它自己。你会被迫研究它的实现,才可能明白它都做了什么。
当框架开发人员必须通用地、动态地获取服务时,可能会采用这个方法。
下一步