angular学习(六)—— 依赖注入


依赖注入(DI)是一种处理组件如何获得依赖的软件设计模式,在Angular中,injector子系统负责创建组件,解决组件的依赖,并将它们提供给其他组件。

使用依赖注入

在Angular中DI是无处不在的,你可以用它来定义一个组件,也可以提供module实现run和config代码块。

  • 像services, directives, filters 和 animations这样的组件都由一个工厂方法或者构造函数定义,在此service和value组件可以作为依赖注入到这些组件当中。
  • Controllers通过一个构造函数定义,一样service和value组件可以作为依赖注入到组件当中,但他们也有着特殊的依赖关系,我们在angular学习(三)—— Controller 中有讲到。
  • Module的run方法接受一个函数,在函数的参数中作为依赖可以注入service,value和constant组件,但是不能注入providers。
  • Module的config方法接受一个函数,在函数的参数中作为依赖可以注入provider和constant组件,但是不能注入service和value。

factory方法

directive, service, or filter都由一个工厂方法定义,该方法通过modules注册。定义这个工厂方法推荐的写法如下:

1
2
3
4
5
6
7
8
9
10
angular.module('myModule', [])
.factory('serviceId', ['depService', function(depService) {
// ...
}])
.directive('directiveName', ['depService', function(depService) {
// ...
}])
.filter('filterName', ['depService', function(depService) {
// ...
}]);

module方法

我们可以指定module在配置和运行时需要调用的config方法和run方法。这些函数就像上面的工厂方法一样可以注入依赖。

1
2
3
4
5
6
7
angular.module('myModule', [])
.config(['depProvider', function(depProvider) {
// ...
}])
.run(['depService', function(depService) {
// ...
}]);

controller

controller能够提供让application可以在模板定义声明性标记的行为(方法)。定义controller中使用依赖注入的推荐方法是用一个数组。

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

和service不同的是,在同一个应用程序中可以有许多相同类型的控制器实例。
此外,控制器可以使用一些额外的依赖关系:

  • $scope:控制器与DOM中的一个元素相关联,因此提供了以元素相对应的访问范围。其他组件(如service)只能访问$rootScope服务
  • resolves: 如果controller被实例化为route的一部分,作为route的一部分解析的任何值都可用于注入控制器.

依赖注解

Angular调用通过注入调用一些函数时(比如service的工厂方法和controller),为了让injector知道需要将哪些service注入到函数中,你需要对这些函数进行注解。这里有三种注解方式:

  • 内联数组注解,最好优先使用这种方式
  • 用 $inject属性注解
  • 以函数的参数名字隐式注解,这种方式有一些警告。

内联数组注解

这是angular程序注解组件的首选方法,本系列的文章都是这种方式。

1
2
3
omeModule.controller('MyController', ['$scope', 'greeter', function($scope, greeter) {
// ...
}]);

内联函数由所需依赖名字构成的字符串列表和函数本身构成。当使用这种注解方式时要注意,内联数组中所需依赖名字字符串顺序和函数中的参数顺序要一致。

$inject属性注解

使用$inject属性一样可以做到这一点,$inject属性是一个由所需依赖名字构成字符串数组。

1
2
3
4
5
var MyController = function($scope, greeter) {
// ...
}
MyController.$inject = ['$scope', 'greeter'];
someModule.controller('MyController', MyController);

和内联数组注解一样,函数中的依赖参数顺序要和$inject 属性数组里的顺序一致。

隐式注解

注意:如果你想最小化你的javascript代码时,如果使用了这种注解方式,service将会被重命名,程序就会报错。

获得依赖最简单的方法就是假定函数参数的名字就是所需依赖的名字。

1
2
3
someModule.controller('MyController', function($scope, greeter) {
// ...
});

给出一个函数,injector可以通过检查函数声明和提取参数名称来确定需要注入的service的名字。
这种注解方法的一个优点是,你不需要去关心数组中的元素和函数的参数是否一致,你可以自由排列参数的顺序。
但是你一旦对javascript做了最小化或者混淆操作,那么函数将无法运行,因为参数会被重命名。基于这些问题,我们建议避免这种风格的注解。

有一个好工具ng-annotate可以帮你在最小化前自动把你程序里的所有的隐私注解更改为内联数组注解。
还有如果你决定采取这种方法,您可能希望使用ng-strict-di。

使用ng-strict-di指令

你可以在和ng-app同一个元素中添加一个ng-strict-di指令。

1
2
3
4
5
6
7
<!doctype html>
<html ng-app="myApp" ng-strict-di>
<body>
I can add: {{ 1 + 2 }}.
<script src="angular.js"></script>
</body>
</html>

在strict模式下,当一个服务试图使用隐式注解时就会抛出一个错误。
如下,包含了隐式注解的willBreak service。

1
2
3
4
5
6
7
angular.module('myApp', [])
.factory('willBreak', function($rootScope) {
// $rootScope is implicitly injected
})
.run(['willBreak', function(willBreak) {
// Angular will throw when this runs
}]);

当willBreak初始化时就会报错,因为已经指定了strict模式。这可以用来保证ng-annotate工具已经把你程序的所有隐式注解修改为了内联数组注解。

如果你手动修改bootstrapping,你可以指定strictDi: true来达到同样的效果。

1
2
3
angular.bootstrap(document, ['myApp'], {
strictDi: true
});

为什么使用依赖注入

一个组件(对象或者函数)获得它需要的依赖的方法只有三种

  1. 组件去创建这个依赖,通常使用new操作符
  2. 组件去查找一个依赖,引用一个全局变量
  3. 组件可以得到一个依赖传递在它需要使用的地方

前两个方法都不是最佳的,因为需要将依赖硬编码到组件中。需要依赖需要修改的时候这就非常的困难。特别是在测试阶段,为了测试的隔离性,常常需要提供一个模拟的依赖。
第三种方式是可行的,因为它把依赖的定位查找的责任从组件中剥离出来,依赖仅仅是简单地传递给组件。

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

在上面的例子中SomeClass不需要关心greeter依赖的创建和查找,仅仅需要在实例化时获得greeter依赖即可。它把获得依赖的责任丢给了代码结构设计。

为了管理依赖创建的责任,每个angular应用都有一个injector,injector是一个负责创建和查找依赖的服务定位器。

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

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

我们来看看injector如何创建greeter服务。注意greeter还依赖于$window服务,greeter服务是一个包含了greet函数的对象。

1
2
3
4
5
6
7
myModule.factory('greeter', function($window) {
return {
greet: function(text) {
$window.alert(text);
}
};
});

在myModule中创建一个可以提供组件的injector,并且从injector中获得greeter服务。(这通常由angular的bootstrap完成)

1
2
var injector = angular.injector(['ng', 'myModule']);
var greeter = injector.get('greeter');

这样虽然也解决了依赖的硬编码问题,但是意味着injector需要穿过整个应用,这样就打破了迪米特法则。为了弥补这一点,我们在HTML模板使用声明性符号把创建依赖的责任移交给injector,如下:

1
2
3
<div ng-controller="MyController">
<button ng-click="sayHello()">Hello</button>
</div>
1
2
3
4
5
function MyController($scope, greeter) {
$scope.sayHello = function() {
greeter.greet('Hello World');
};
}

当angular编译这个html时,它处理到ng-controller,它将要求injector去创建controller实例和它的依赖。

1
injector.instantiate(MyController);

这些操作都是在后台做的。通过ng-controller要求injector去实例化的方式,可以让controller不知道injector的情况下,创建了MyController所有的依赖。应用程序无需理会injector,仅仅是简单的声明它所需要的依赖,这样就不会打破迪米特法则。

整个过程图示如下:
这里写图片描述

文章目录
  1. 1. 使用依赖注入
    1. 1.1. factory方法
    2. 1.2. module方法
    3. 1.3. controller
  2. 2. 依赖注解
    1. 2.1. 内联数组注解
    2. 2.2. $inject属性注解
    3. 2.3. 隐式注解
  3. 3. 使用ng-strict-di指令
  4. 4. 为什么使用依赖注入
|