依赖注入(Dependency Injection)是一种软件设计模式,用来处理代码如何获取它的依赖。
如果想对依赖注入进入更深入的讨论,参看Wikipedia上的文章,以及由Martin Fowler写的Inversion of Controller,或者自行找书。
一个对象或函数,只有这三种方式可取得依赖:
new
操作符创建一个依赖前两种方式都不是最佳的,因为它们都需要在代码中“硬编码”依赖,以致于难以甚至无法修改依赖。在测试时这会是一个大问题,因为测试时通常都会使用一些mock来代替原有的依赖,以方便测试。
第三种方式最可行,因为它把“定位依赖”的责任从组件中移除了。依赖可以简单的传递给组件。
function SomeClass(greeter) {
this.greeter = greeter
}
SomeClass.prototype.doSomething = function(name) {
this.greeter.greet(name);
}
在上面的例子中,SomeClass
不用关心它的依赖greeter
从何而来,直接用就行了。greeter
会在运行时直接传入。
这么做虽然可行,但它却把取得依赖的责任踢给了SomeClass
的构造函数。
为了管理依赖的创建,每个angular程序都有一个injector
。该injector是一个定位服务,专门用于创建和寻找依赖。
下面是使用injector的例子:
// Provide the wiring information in a module
angular.module('myModule', []).
// Teach the injector how to build a 'greeter'
// Notice that greeter itself is dependent on '$window'
factory('greeter', function($window) {
// This is a factory function, and is responsible for
// creating the 'greet' service.
return {
greet: function(text) {
$window.alert(text);
}
};
}).
// New injector is created from the module.
// (This is usually done automatically by angular bootstrap)
var injector = angular.injector('myModule');
// Request any dependency from the injector
var greeter = injector.get('greeter');
虽然用“请求依赖”解决了“硬编码”的问题,但它也意味着有一个injector需要传到程序中。传入injector破坏了迪米特法则,即“一个软件实体应当与尽可能少的其他实体发生相互作用”。为了解决这个问题,我们把寻找依赖的责任通过声明依赖的方式,转移给injector。
<!-- Given this HTML -->
<div ng-controller="MyController">
<button ng-click="sayHello()">Hello</button>
</div>
// And this controller definition
function MyController($scope, greeter) {
$scope.sayHello = function() {
greeter('Hello World');
};
}
// The 'ng-controller' directive does this behind the scenes
injector.instantiate(MyController);
注意,当我们使用ng-controller
这个directive来实例化这个类时,它会自动向MyController
传入它需要的依赖,而controller根本不需要知道injector的存在。这是最好的结果。程序代码不需要与injector交互,就可以请求它所需要的依赖。这也没有违反迪米特法则。
依赖注解
injector如何知道哪个service应该被注入进去呢?
程序开发者需要提供注解信息,让injector用来寻找依赖。根据Angular的API文档,Angular的API函数都是由injector调用的。Injector需要知道哪些services应该注入到函数中。下面是将service名称注解到代码中的三种等价方式。你可以根据需要使用:
推断依赖
最简单的方式,就是直接用函数参数名来表示依赖名。
function MyController($scope, greeter) {
...
}
Injector可以检查函数定义,找到参数名,并以此去寻找依赖。在上例中,$scope
和greeter
就是需要注入的两个依赖。
然而,如果你使用一些工具,对JavaScript代码进行了压缩或混淆处理,则这种方式就行不通了,因为参数名会被修改。所以这种方式常用于原型或演示程序。
$inject
注解为了配合压缩工具,可以使用$inject
属性。它是一个由service名称组成的数组,用于寻找依赖。如果设置了它,则参数名将不再用来寻找依赖。
var MyController = function(renamed$scope, renamedGreeter) { ... } MyController.$inject = ['$scope', 'greeter']; ```
需要注意的是,$inject
中的数据要跟Controller函数中的参数保持一致。
对于controller的声明,这种方法比较有用,因为它把注解信息赋给了函数。
使用$inject
向directives添加注解信息不太方便。
比如这个例子:
someModule.factory('greeter', function($window) { ...; });
如果要使用$inject
,需要用到临时变量:
var greeterFactory = function(renamed$window) { ...; }; greeterFactory.$inject = ['$window']; someModule.factory('greeter', greeterFactory);
所以Angular提供了第三种注解方式:
someModule.factory('greeter', ['$window', function(renamed$window) { ...; }]);
注意上面三种方式是完全等价的,在Angular中任何地方都可以使用它们中任意一种。
DI在Angular中使用得非常普遍。最常用在controller和factory方法中。
Controllers是用来描述程序行为的类。推荐使用下面的方式来声明controller:
var MyController = function(dep1, dep2) {
...
}
MyController.$inject = ['dep1', 'dep2'];
MyController.prototype.aMethod = function() {
...
}
工厂方法用来创建Angular中大多数的对象。例如directives, services,和filters。工厂方法注册到模块上,推荐的声明方式为:
angualar.module('myModule', []).
config(['depProvider', function(depProvider){
...
}]).
factory('serviceId', ['depService', function(depService) {
...
}]).
directive('directiveName', ['depService', function(depService) {
...
}]).
filter('filterName', ['depService', function(depService) {
...
}]).
run(['depService', function(depService) {
...
}]
);