详解AngularJS中的依赖注入
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
详解AngularJS中的依赖注⼊
⼀般来说,⼀个对象只能通过三种⽅法来得到它的依赖项⽬:
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) {
}]);
内联注释⽅法可以在精简代码的情况下正常运⾏,因为我们在其中传递了⼀个字符串列表。
我们经常将这个⽅法叫做⽅括号注释或者数组注释。
尽管我们需要直接使⽤$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将能够解析源⽂件并对它进⾏预精简。