Improve this doc

Translated by @GrahamLe

依赖注入

依赖注入(译注:以下简称DI)是一种让代码处理其依赖关系的软件设计模式。

期待更多详细的有关 DI 的讨论,请看维基百科的 Dependency Injection 词条,以及 Martin Fowler 写的 Inversion of Control ,或者是在你喜欢的任何一本设计模式的书去查看。

DI简介

对象或是函数只有三种方式可以得到依赖:

  • 依赖创建,通常是通过 new 操作符

  • 依赖查找,通过引用一个全局变量

  • 依赖传递,依赖可以被注入到任何需要依赖的地方

前两种选择:创建或是查找依赖都不是那么理想,因为它们都是将依赖写死在对象或函数里了。不能说完全没可能吧,但至少,想要更改上述两种方式得到的依赖是很困难的。尤其是在测试的时候,会遇到很多问题,因为测试时常常需要我们提供模拟的依赖(译注:此句涉及测试,本人不精,已跪)。

第三种方式是最可行的,因为它去除了去组件里面定位相应的依赖这个担子,而是反过来,依赖总是能够很简单地被注入到需要它的组件中。(译注:说实话,上述三种方式,说的啥玩意呀,对于我是相当于没说一样)

  1. function SomeClass(greeter) {
  2. this.greeter = greeter;
  3. }
  4.  
  5. SomeClass.prototype.doSomething = function(name) {
  6. this.greeter.greet(name);
  7. }

上述例子中,SomeClass 不必去在意 greeter 依赖是从哪里来的,反正就是在运行的时候,greeter 依赖被传进来了,我用就是了。(译注:so easy,妈妈再也不用担心我的依赖了)

像这个例子中的情况是我们愿意看到的,但是它把获得依赖的大部分责任都放在了我们创建 SomeClass 的代码中。

依赖注入 - 图1

为了分配依赖创建的任务,每个 Angular 应用都有一个 injector。这个 injector 是一个服务定位器,负责创建和查找依赖的。(译注:当你的app的某处声明需要用到某个依赖时,Angular 会调用这个依赖注入器去查找或是创建你所需要的依赖,然后返回来给你用)

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

  1. // Provide the wiring information in a module
  2. angular.module('myModule', []).
  3.  
  4. // 下面是教 injector 如何构建一个 'greeter' 依赖
  5. // 注意 greeter 本身依赖于 '$window'
  6. factory('greeter', function($window) {
  7. // 这是一个 factory 函数,负责创建 'greeter' 服务
  8. return {
  9. greet: function(text) {
  10. $window.alert(text);
  11. }
  12. };
  13. });
  14.  
  15. // 从 module 创建的 injector
  16. // 这个常常是 Angular 启动时自动完成的
  17. var injector = angular.injector(['myModule', 'ng']);
  18.  
  19. // 通过 injector 请求任意的依赖
  20. var greeter = injector.get('greeter');

函数或对象通过请求依赖解决了硬编码的问题,但同时也就意味着 injector 需要通过应用传递,而传递 injector 破坏了 Law of Demeter。为了弥补这个,我们通过像下面例子那样声明依赖,将依赖查找的任务丢给了 injector 去做:

  1. <!-- Given this HTML -->
  2. <div ng-controller="MyController">
  3. <button ng-click="sayHello()">Hello</button>
  4. </div>
  1. // And this controller definition
  2. function MyController($scope, greeter) {
  3. $scope.sayHello = function() {
  4. greeter.greet('Hello World');
  5. };
  6. }
  7.  
  8. // The 'ng-controller' directive does this behind the scenes
  9. injector.instantiate(MyController);

注意,通过让 ng-controller 在背后调用 injector 初始化控制器类满足了 MyController 需要依赖的需求,而且可以让控制器根本不知道 injector 的存在(译注:好一招瞒天过海)。这是最好的结果了。应用中的代码简单地提交需要它需要某依赖的请求,不需要去管 injector ,而且这样也不违反迪米特法则 。

依赖注释

(译注:此处注释非代码注释,应理解为依赖声明的方法)

那么,injector 是如何知道哪些服务需要被注入呢?

应用开发者需要提供 injector 需要使用的注释信息来解析依赖。Angular 之中,按照API文档说明,某些 API 方法需要通过 injector 调用。这样,injector 要知道得往这个方法中注入什么服务。下面是用服务名信息来进行注释的三种等价的方式,它们可以互用,按照你觉得适合的情况选用相应的方式。

推断依赖

最简单的获取依赖的方法是让你的函数的参数名直接使用依赖名。

  1. function MyController($scope, greeter) {
  2. ...
  3. }

给 injector 一个函数,它可以通过检查函数声明并抽取参数名可以推断需要注入的服务名。在上面的例子中,$scopegreeter 是两个需要被注入到函数中的服务。

虽然这种方式很直观明了,但是它对于压缩的 JavaScript 代码来说是不起作用的,因为压缩过后的 JavaScript 代码重命名了函数的参数名。这就让这种注释方式只对 pretotyping 和 demo级应用有用。

$inject 注释

为了让重命名了参数名的压缩版的 JavaScript 代码能够正确地注入相关的依赖服务。函数需要通过 $inject 属性进行标注,这个属性是一个存放需要注入的服务的数组。

  1. var MyController = function(renamed$scope, renamedGreeter) {
  2. ...
  3. }
  4. MyController['$inject'] = ['$scope', 'greeter'];

在这种场合下,$inject 数组中的服务名顺序必须和函数参数名顺序一致。以上述代码段为例,$scope 将会被注入到 'renamed$scope',而 greeter 则是注入到 'renamedGreeter'。需要注意 $inject 注释是和真实的函数声明中的参数保持同步的。

这种注释方法对于控制器声明很有用处,因为它是把注释信息赋给了函数。

行内注释

有时候用 $inject 注释的方式不方便,比如标注指令的时候(译注:这里标注指令可以理解为告诉指令需要加载哪些服务依赖的说明)。

看下面的例子:

  1. someModule.factory('greeter', function($window) {
  2. ...
  3. });

由于需要一个临时的变量导致代码膨胀:

  1. var greeterFactory = function(renamed$window) {
  2. ...
  3. };
  4.  
  5. greeterFactory.$inject = ['$window'];
  6.  
  7. someModule.factory('greeter', greeterFactory);

所以,第三种注释风格被引入,如下:

  1. someModule.factory('greeter', ['$window', function(renamed$window) {
  2. ...
  3. }]);

记住,所有上面的三种依赖注释风格(译注:声明依赖的风格)是等价的,在 Angular 中任何支持依赖注入的地方都可以使用。

哪里使用DI

在 Angular 中,DI 无处不在。通常在控制器和工厂方法(译注:所谓的工厂方法个人理解为 Angular 中的API)中使用较多。

控制器中使用DI

控制器是负责应用操作逻辑的 JavaScript 类,推荐的在控制器中使用DI的方式是用数组标记风格,如下:

  1. someModule.controller('MyController', ['$scope', 'dep1', 'dep2', function($scope, dep1, dep2) {
  2. ...
  3. $scope.aMethod = function() {
  4. ...
  5. }
  6. ...
  7. }]);

如上那样,避免了在全局作用域内创建控制器,同时也避免了代码压缩时引起的注入问题。

工厂方法中使用DI

工厂方法负责创建 Angular 中的绝大多数对象。例如指令,服务,和过滤器等。工厂方法是注册在模块之下的,推荐的声明方式如下:

  1. angular.module('myModule', []).
  2. config(['depProvider', function(depProvider){
  3. ...
  4. }]).
  5. factory('serviceId', ['depService', function(depService) {
  6. ...
  7. }]).
  8. directive('directiveName', ['depService', function(depService) {
  9. ...
  10. }]).
  11. filter('filterName', ['depService', function(depService) {
  12. ...
  13. }]).
  14. run(['depService', function(depService) {
  15. ...
  16. }]);