详解AngularJS中的依赖注入
发布在用Angular开发web应用2014年3月12日view:18751
在文章任何区域双击击即可给文章添加【评注】!浮到评注点上可以查看详情。

依赖注入

一般来说,一个对象只能通过三种方法来得到它的依赖项目:

  1. 我们可以在对象内部创建依赖项目
  2. 我们可以将依赖作为一个全局变量来进行查找或引用
  3. 我们可以将依赖传递到需要它的地方

在使用依赖注入时,我们采用的是第三种方式(另外两种方式都会引起其他困难的挑战,例如污染全局作用域以及使隔离变得几乎不可能)。依赖注入是一种设计模式,它移除了硬编码依赖,因此使得我们可以在运行中随时移除并改变依赖项目。

在运行过程中能够修改依赖项目的能力允许我们创建隔离环境,这对于测试来说是非常理想的。我们可以用测试环境中的一个冒牌对象来替换生产环境中的一个真实对象。

从功能上来说,这种模式通过自动提前查找依赖以及为依赖提供目标,以此将依赖资源注入到需要它们的地方。

在我们编写一个依赖于其他对象或库的组件时,我们会描述它的依赖项目。再运行过程中,一个注入器将会创建依赖的实例并将它们传递给一个依赖消费者。

 // 一个来自于AngularJS的好例子
 function SomeClass(greeter) {
     this.greeter = greeter;
 }
 SomeClass.prototype.greetName = function(name) {  
    this.greeter.greet(name) 
 }

注意:像上面的实例代码一样,在全局作用域中创建一个控制器永远不是一个好主意。上面的代码只是为了简化说明依赖注入的原理。

在运行中,SomeClass并不关心它是如何获得greeter依赖的,只要得到它就行。为了将greeter实例传递到SomeClass中,SomeClass的创造者还需要负责在创建函数时为它传递依赖。

基于以上原因,Angular使用$injector来管理依赖查询以及实例化依赖项目。事实上,$injector负责处理我们的Angular组件中所有的实例,包括我们应用的模块,指令,控制器等等。

在运行过程中,当我们的任何模块被引导启动时,注入器实际上负责实例化这个对象的实例并为它传递它所需要的依赖项目。

例如,下面的这段简单的代码声明了一个单独的模块和一个单独的控制器,如下所示:

 angular.module('myApp', []) 
 .factory('greeter', function() {
 return {
 greet: function(msg) { alert(msg); }
 } 
 })
 .controller('MyController', 
 function($scope, greeter) {
 $scope.sayHello = function() { 
 greeter.greet("Hello!");
 }; 
 });

在运行期间,当AngularJS初始化我们模型的实例时,它会查找greeter并简单地将它传递给我们的模块:

<div ng-app="myApp">
<div ng-controller="MyController">
<button ng-click="sayHello()">Hello</button> </div>
</div>

在幕后,Angular运行的过程如下所示:

// 使用注入器载入应用   
var injector = angular.injector(['ng', 'myApp']); 

//和注入器一起载入$controller

var $controller = injector.get('$controller'); 
var scope = injector.get('$rootScope').$new(); 

//载入控制器,将它传递给一个作用域
//这就是angular在运行过程中做的事
var MyController = $controller('MyController', {$scope: scope})  

上面的例子中并没有描述我们怎样去寻找greeter;它运行非常简单,在此期间注入器会帮助我们找到并载入依赖项目。

在实例化期间,AngularJS使用一个注释函数来从传递过去的数组中提取属性。你可以在Chrome浏览器的开发者工具中输入以下代码来查看这个函数:

 > injector.annotate(function($q,greeter){})
 ["$q","greeter"]  

在每个Angular应用中,$injector都一直在运行,不管我们有没有意识到这一点。当我们在编写一个控制器但是没有加上[]括号标示符时,或者显式的设置了它们的时,$injector将会根据变量的名称来推测依赖项目。

通过推测来注释

Angular假设函数的参数名称就是依赖项目的名称,如果没有特别指明的话。因此,它会在函数上调用toString()方法,解析并从函数中提取变量,然后使用$injector将这个变量注入到对象的实例中。注入的过程是这样的:

injector.invoke(function($http,greeter){});
注意到这个过程只能在没有经过压缩,没有歧义的代码下面完成,因为Angular需要完整的解析变量。

通过JavaScript的推测,参数的顺序并不重要:Angular会为我们找出这些参数并将正确属性注入到“正确”的位置。

JavaScript精简器一般来说会将函数的参数变为个数最少的字母(同时也会改变空格,移除新行和注释等等),以此来减少JavaScript文件最终的体积。如果我们没有显式的描述变量,Angular将不能够推测变量,从而无法注入需要的依赖项。

显式注释

Angular为我们提供了一种方法来显式的定义一个函数所需要的依赖项目。这中方法允许精简器将函数的参数重命名,同时也能保证将合适的服务注入到函数中。

注入过程使用$inject属性来注释函数。一个函数的$inject属性是一个包含服务名称的数组,这些服务用作依赖项会被注入到函数中。

为了使用$inject属性方法,我们将它在一个函数或者函数名上进行设置。

var aControllerFactory =
function aController($scope, greeter) {
    console.log("LOADED controller", greeter);
    // ... 控制器
};
aControllerFactory.$inject = ['$scope', 'greeter'];
// Greeter 服务
var greeterService = function() {
  console.log("greeter service");
}
// 我们的应用控制器
angular.module('myApp', [])
  .controller('MyController', aControllerFactory)
  .factory('greeter', greeterService);
// 获取注入器并创建一个新作用域
var injector = angular.injector(['ng', 'myApp']), 
controller = injector.get('$controller'), 
rootScope = injector.get('$rootScope'), 
newScope = rootScope.$new();
// 调用控制器
controller('MyController', {$scope: newScope});

使用这种注释风格,顺序很重要,因此$inject数组必须要匹配注入的变量顺序。这种注入的方法可以在精简代码的情况下正常运行,因为注释信息依然会打包在函数中。

内联注释

Angular提供的最后一种注释的方法是内联注释。这种语法糖和上面提到的$inject注入方法运行方式相同,但是允许我们在函数定义的时候编写内联参数。另外,它允许我们在定义时不使用一个临时变量。

内联注释允许我们在定义一个Angular对象时传递一个参数数组而不是一个函数。数组中的元素是一个注入依赖项字符串的列表,最后一个变量则是对象的函数定义。

例如:

 angular.module('myApp')
  .controller('MyController',
  ['$scope', 'greeter', function($scope, greeter) {
 }]);

内联注释方法可以在精简代码的情况下正常运行,因为我们在其中传递了一个字符串列表。我们经常将这个方法叫做方括号注释或者数组注释。

$inject API

尽管我们需要直接使用$injector的情况非常非常少,对$inject的API有一些了解会帮助我们更好的理解它的运行机制。

annotate()

annotate()函数会返回一个在初始化时会被注入到函数中的服务名称数组。annotate()函数通常在调用的时候被注入器用来决定应该注入什么服务。

annotate()函数会接收一个变量:

  • fn(函数或者数组)

fn可以是一个给定的函数,也可以是包含位于方括号标示符中的函数定义的数组。

annotate()函数会返回一个在初始化时会被注入到函数中的服务名称数组。

var injector = angular.injector(['ng', 'myApp']); injector.annotate(function($q, greeter) {});
// ['$q', 'greeter']

在你的Chrome浏览器的调试器中试验一下。

get()

get()方法接收一个参数同时会返回一个服务的实例。

  • name(字符串)

name变量是我们想要得到的实例的名称。

get()通过名称返回一个服务的实例。

has()

如果注入器知道它的注册服务中包含一个服务,has()方法会返回true,反之则返回false。它接收一个参数:

  • name(字符串)

这个字符串是我们想要在注入器的注册服务中查找的服务的名称。

instantiate()

instantiate()方法会创建一个JavaScript类型的新实例。它接收一个构造器函数并连同所有指定参数调用new操作符。它接收两个参数:

  • Type(函数)

这个函数是将要调用的注释构造器函数。

  • locals(对象 – 可选)

这个可选参数在函数被调用时提供了另外一种传递参数的办法。

instantiate()方法返回Type的一个新实例。

invoke

invoke()方法调用方法同时添加来自于$injector的方法参数。

这个invoke()方法接收三个参数:

  • fn(函数)

这个函数是将要被调用的函数。这个对于函数的参数连同注释一起被设置。

  • self(对象 – 可选)

self变量允许我们设置将要被调用方法的this变量。

  • locals(对象 – 可选)

这个可选参数在函数被调用时提供了另一种传递变量名称的方法。

ngMin

通过以上三种定义注释的方法,很重要的一点是注意到在定义一个函数的时候这些可选项都存在。然而,在具体生产过程中,刻意留心去关注参数顺序和代码膨胀是非常不方便的一件事。

ngMin工具为我们减轻了显示定义依赖项的痛苦。ngMin是一个针对Angular应用的预精简器。它会遍历我们的Angular应用并为我们设置依赖注入。

例如,它会将下面的代码:

angular.module('myApp', [])
.directive('myDirective',
function($http) { })
.controller('IndexController',
function($scope, $q) {
});

转化为下面的代码:

angular.module('myApp', [])
.directive('myDirective', [
'$http',
function ($http) { }
]).controller('IndexController', 
[ '$scope',
'$q',
function ($scope, $q) {
} ]);


ngMin为我们节省了许多输入代码的时间,同时让我们的源文件变得非常干净。

安装ngMin

为了安装ngMin,我们需要使用npm包管理器:

npm install -g ngmin

如果我们使用Grunt,我们可以安装grunt-ngmin Grunt任务。如果我们使用Rails,我们可以使用Ruby gem ngmin-rails。

使用ngMin

我们可以在命令行工具单独模式下使用ngMin,只需要为它传递两个参数:input.js和output.js或者通过stdio/stdout,如下所示:

$ ngmin inpit.js output.js

或者

$ ngmin < input.js > output.js

在上面的例子中input.js是我们的源文件,output.js是注释输出文件。

ngMin是怎样运行的

从核心上来说,ngMin使用抽象语树(AST)来遍历JavaScript源文件。通过astral – 一个AST工具框架 – 的帮助,它使用必要的注释重建了源文件,并且使用escodegen输出了更新后的文件。

ngmin希望我们的Angular源文件由逻辑声明组成。如果我们的代码语法和本书中使用的代码语法相似,ngMin将能够解析源文件并对它进行预精简。


本文译自《ng-book》第十四章,如果你觉得本文对你有帮助,请为我提供赞助 https://me.alipay.com/jabez128

评论
发表评论
3年前
赞了此文章!
WRITTEN BY
张小俊128
Intern in Baidu mobile search department。认真工作,努力钻研,期待未来更多可能。
TA的新浪微博
PUBLISHED IN
用Angular开发web应用

讲述Angular开发框架的基础知识,帮助读者快速学习并深入理解Angular的开发理念和具体应用方式

我的收藏