本文译自https://docs.angularjs.org/guide/di

依赖注入

依赖注入(DI)是一种解决组件如何获取其依赖这一问题的设计模式。
Angular injector子系统负责创建组件,解决它们的依赖,并按要求将它们提供给其他组件。
如果想深入了解DI,可以查看依赖注入的Wikipedia以及Martin Fowler的这篇Inversion of Control


DI in a Nutshell

一个组件要想获取它的依赖可以通过以下三种途径:

前两种获取依赖的方式并不理想,因为这会导致依赖关系被硬编码进代码中。这会导致更新依赖关系变得很复杂。在测试时尤其如此,我们通常会提供mock过的依赖关系以隔离不同组件间的测试,硬编码会导致每次测试都需要修改相关的源码。

相比之下,第三种方式是最可行的,因为它将解决依赖关系的责任从组件中移除。依赖只是简单的作为参数传递给组件:

function SomeClass(greeter) {
  this.greeter = greeter;
}

SomeClass.prototype.doSomething = function(name) {
  this.greeter.greet(name);
}

在上边的例子中,SomeClass并不关心如何创建或找到greeter,它只是在初始化时从参数列表里取出需要的依赖即可。
但这样做的问题是,寻找/创建依赖的责任被转交给了调用SomeClass的代码。

为了统一管理依赖关系,每个Angular应用都有一个injectorinjector是一个服务定位器,它负责查找或创建依赖。

下面是一个使用injector服务的例子:

// Provide the wiring information in a module
var myModule = angular.module('myModule', []);

Alt text

下面的代码告诉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中,有三种方式可以声明一个组件所依赖的服务:

隐式依赖

最简单获取依赖的方式就是假定函数的参数名就是依赖的名字:

function MyController($scope, greeter) {
  // ...
}

给定一个函数,injector可以通过检查它的参数名来推断出那些服务需要被注入。在上边的例子中,$scopegreeter就是需要被注入给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作为依赖注入。
- runconfig方法接收一个函数作为参数,这个函数也可以把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注入以下依赖: