本文译自https://docs.angularjs.org/guide/di
依赖注入
依赖注入(DI)是一种解决组件如何获取其依赖这一问题的设计模式。
Angular injector子系统负责创建组件,解决它们的依赖,并按要求将它们提供给其他组件。
如果想深入了解DI,可以查看依赖注入的Wikipedia以及Martin Fowler的这篇Inversion of Control。
DI in a Nutshell
一个组件要想获取它的依赖可以通过以下三种途径:
- 直接创建这个依赖的一个实例,通常使用
new
操作符 - 通过全局变量查找已经被创建的依赖
- 依赖可以作为参数传递给需要它的组件
前两种获取依赖的方式并不理想,因为这会导致依赖关系被硬编码进代码中。这会导致更新依赖关系变得很复杂。在测试时尤其如此,我们通常会提供mock过的依赖关系以隔离不同组件间的测试,硬编码会导致每次测试都需要修改相关的源码。
相比之下,第三种方式是最可行的,因为它将解决依赖关系的责任从组件中移除。依赖只是简单的作为参数传递给组件:
function SomeClass(greeter) {
this.greeter = greeter;
}
SomeClass.prototype.doSomething = function(name) {
this.greeter.greet(name);
}
在上边的例子中,SomeClass
并不关心如何创建或找到greeter
,它只是在初始化时从参数列表里取出需要的依赖即可。
但这样做的问题是,寻找/创建依赖的责任被转交给了调用SomeClass
的代码。
为了统一管理依赖关系,每个Angular应用都有一个injector。injector
是一个服务定位器,它负责查找或创建依赖。
下面是一个使用injector
服务的例子:
// Provide the wiring information in a module
var myModule = angular.module('myModule', []);
下面的代码告诉injector如何创建greeter
服务。需要注意的是greeter
又依赖于$window
服务。greeter
实际上是一个包含greet
方法的对象。
myModule.factory('greeter', function($window) {
return {
greet: function(text) {
$window.alert(text);
}
};
});
下面的代码演示了如何创建一个injector
并通过它来请求greeter
服务:
var injector = angular.injector(['myModule', 'ng']);
var greeter = injector.get('greeter');
向外部索要依赖服务解决了硬编码的问题,但是这也意味着injector
需要在应用内部到处传递(这违背了Law of Demeter)。为解决这个问题,我们在HTML模板中使用一种声明,以此将创建组件的责任交给injector
。示例如下:
<div ng-controller="MyController">
<button ng-click="sayHello()">Hello</button>
</div>
function MyController($scope, greeter) {
$scope.sayHello = function() {
greeter.greet('Hello World');
};
}
当Angular处理HTML到ng-controller
时,它会调用injector
生成一个controler的实例:
injector.instantiate(MyController); // 同时会处理MyController的依赖
所有这些都是自动完成的。通过使用ng-controller
来调用injector创建controller实例可以达到既解决了controller的依赖,又不需要将injector暴露给controller的目的。
这就是最终的解决方案。应用程序只需要声明它依赖的服务而不需要与injector打交道,这种组织代码的方法并不违背Law of Demeter
。
依赖声明
injector如何知道需要给组件提供那些依赖?
程序的开发人员需要明确的声明依赖以便injector
决定如何解决依赖关系。在Angular中,有三种方式可以声明一个组件所依赖的服务:
- 隐式的从函数参数名中推断
- 使用
$inject
属性声明 - 使用数组声明
隐式依赖
最简单获取依赖的方式就是假定函数的参数名就是依赖的名字:
function MyController($scope, greeter) {
// ...
}
给定一个函数,injector
可以通过检查它的参数名来推断出那些服务需要被注入。在上边的例子中,$scope
和greeter
就是需要被注入给MyController的两个服务的名字。
虽然这种方式简单易懂,但它并不适用于使用了JavaScript压缩工具/混淆工具的场景,因为那些工具会将函数的参数名重命名。这使得这种方式只适用于原型开发或是演示程序。
$inject属性声明
为了在允许压缩工具重命名函数参数的前提下还能注入正确的服务,函数需要使用$inject
属性来声明依赖。$inject
是一个包含被注入服务名字的数组。
var MyController = function(renamed$scope, renamedGreeter) {
...
}
MyController['$inject'] = ['$scope', 'greeter'];
在这方式中,$inject
数组中服务的顺序需要与函数参数名中服务的顺序保持一致。以上边的示例代码为例,$scope
服务会被注入到renamed$scope
中,greeter
会被注入到renamedGreeter
中。这里的顺序一定要保持对应。
由于这种方式将声明信息作为属性赋给一个函数,它比较适合定义Controller。
数组声明
使用$inject
属性的方式声明依赖在定义directives或services的时候并不是太方便,因为这些组件是通过factory方法定义的。
比如:
someModule.factory('greeter', function($window) {
// ...
});
会导致代码膨胀为以下的形式(需要额外定义一个临时变量):
var greeterFactory = function(renamed$window) {
// ...
};
greeterFactory.$inject = ['$window'];
someModule.factory('greeter', greeterFactory);
为了避免这种情况,第三种声明方式允许你以如下方式声明:
someModule.factory('greeter', ['$window', function(renamed$window) {
// ...
}]);
在这里,我们没有简单的把一个factory作为第二个参数,取而代之的是一个包含一系列服务名的字符串以及一个函数的数组。
以上三种方式可以互换,且可以在Angular系统中任何支持依赖注入的地方使用。
哪些地方可以使用依赖注入?
依赖注入的使用在Angular相当常见。你可以在定义组件或是为一个模块提供run
/config
块时使用:
- 组件,比如service,directive,filter和animation,这些由可注入的factory方法定义的组件可以将service作为依赖注入。
- run
或config
方法接收一个函数作为参数,这个函数也可以把services作为依赖注入。
- Controller由构造函数定义,构造函数可以注入service作为依赖,同时还有一些特殊依赖也可以被注入到Controller的构造函数中。
Factory方法
Factory方法用来创造Angular中的绝大部分对象,比如directive,service以及filter。factory方法会向module注册,声明factory方法的推荐做法是:
angular.module('myModule', [])
.factory('serviceId', ['depService', function(depService) {
...
}])
.directive('directiveName', ['depService', function(depService) {
...
}])
.filter('filterName', ['depService', function(depService) {
...
}]);
Module方法
我们可以通过run
方法和config
方法来指定一个模块在配置阶段和运行阶段要执行的代码。这些方法就像factory方法一样可以注入依赖:
angular.module('myModule', [])
.config(['depProvider', function(depProvider){
...
}])
.run(['depService', function(depService) {
...
}]);
Controller
Controller是负责为页面元素提供应用逻辑的构造函数。定义Controller的推荐方式是使用数组声明:
someModule.controller('MyController', ['$scope', 'dep1', 'dep2', function($scope, dep1, dep2) {
...
$scope.aMethod = function() {
...
}
...
}]);
这样既避免了创建全局函数又可以不受代码压缩的影响。
与service不同的是,一个应用中可以有同一个Controller的多个实例。每一个ng-controller
语句都会创建一个新的实例。
此外,还可以向Controller注入以下依赖:
$scope
:Controller总是与DOM中某个元素相关联,因此都有一个对应的scope用来隔离访问。其他组件,比如service只能访问$rootScope
服务。$route
:如果一个Controller作为路由的一部分被实例化,那么所有被解析为路由一部分的值都会通过$route
注入到Controller中。