[AngularJS面面观] 17. 依赖注入 --- 注解的定义与实现
本篇文章继续介绍angular用以实现依赖注入的关键元素之一 - 注解(Annotation)。 在前几篇文章中,我们已经分析和讨论了有关angular依赖注入的几个方面:
既然我们定义的服务和数据都已经被angular注入器托管在其内部的缓存中了,接下来应该如何使用它呢?写过angular应用的同学们应该都写过下面这类代码: var testApp = angular.module('test',[]);
testApp.controller('testController',function($scope,$rootScope) {
// ......
});
我们直接在 答案是通过注解(Annotation)。所谓注解,它的本质就是给源代码添加一些元数据。有Java开发经验的同学想必都见过 那么切换到angular的上下文中,又是如何来实现注解的呢?这个注解需要解决什么问题呢?这就是本篇文章需要分析和讨论的主题。 我们已经知道定义的各种服务的函数实际上并不由我们自己来调用,而是交给angular框架提供的注入器进行调用,在调用的过程中首先我们需要知道注入器在哪个阶段会需要使用到注解提供的信息。其实在上一篇文章中在介绍注入器实例化托管对象的过程中可能发生循环依赖异常时就已经有一些线索了,这些线索就隐藏在异常的调用栈之中,我们来看看: angular.js:13920 Error: [$injector:cdep] Circular dependency found: service1 <- service2 <- service1
http://errors.angularjs.org/1.5.8/$injector/cdep?p0=service1%20%3C-%20service2%20%3C-%20service1
at angular.js:68
at getService (angular.js:4656)
at injectionArgs (angular.js:4688)
at Object.instantiate (angular.js:4730)
at Object.<anonymous> (angular.js:4573)
at Object.invoke (angular.js:4718)
at Object.enforcedReturnValue [as $get] (angular.js:4557)
at Object.invoke (angular.js:4718)
at angular.js:4517
at getService (angular.js:4664)
可以看到在几个调用点: 毫无疑问,从字面意思上就能够理解这里发生了什么。首先注入器会尝试实例化一个被托管的对象,在实例化的过程中由于该对象也存在依赖关系,需要首先解析这些依赖关系,得到依赖关系之后,才能够调用 function injectionArgs(fn,locals,serviceName) {
var args = [],// 获取注解信息
$inject = createInjector.$$annotate(fn,strictDi,serviceName);
for (var i = 0,length = $inject.length; i < length; i++) {
var key = $inject[i];
// 确保$inject中的每个key都是字符串类型,否则抛出异常
if (typeof key !== 'string') {
throw $injectorMinErr('itkn','Incorrect injection token! Expected service name as string,got {0}',key);
}
// 通过注解的key来得到的真正依赖的对象
args.push(locals && locals.hasOwnProperty(key) ? locals[key] :
getService(key,serviceName));
}
return args;
}
这段代码完成了几件事情:
因此如何构建 // 在injector.js的最后一行定义了如下代码:createInjector.$$annotate = annotate
function annotate(fn,name) {
var $inject,argDecl,last;
if (typeof fn === 'function') {
if (!($inject = fn.$inject)) {
// 没有提供$inject并且非严格模式时,使用源码解析的方式构建$inject
$inject = [];
if (fn.length) {
if (strictDi) {
if (!isString(name) || !name) {
name = fn.name || anonFn(fn);
}
throw $injectorMinErr('strictdi','{0} is not using explicit annotation and cannot be invoked in strict mode',name);
}
argDecl = extractArgs(fn);
forEach(argDecl[1].split(FN_ARG_SPLIT),function(arg) {
arg.replace(FN_ARG,function(all,underscore,name) {
$inject.push(name);
});
});
}
fn.$inject = $inject;
}
} else if (isArray(fn)) {
// 当使用Array-Style的声明方式时,去掉最后一个元素即为$inject
last = fn.length - 1;
assertArgFn(fn[last],'fn');
$inject = fn.slice(0,last);
} else {
// 抛出异常
assertArgFn(fn,'fn',true);
}
// 得到注解信息供后续使用
return $inject;
}
看看上述代码的整体逻辑,可以发现
这三种方式即为angular中注解的几种声明和工作方式。下面我们逐一进行介绍:
示例代码如下所示: var testApp = angular.module('test',[]);
testApp.controller('testCtrl',testCtrlFunc);
testCtrlFunc.$inject = ['aConstant','bConstant'];
function testCtrlFunc(a,b) {
// a 代表的就是 aConstant
// b 代表的就是 bConstant
}
这种方式简单粗暴,但是由于它还需要给函数附加一个属性,导致实际中很少用到。正是因为这一点,才有了第二种基于数组的注解声明方式的诞生。它将原来的函数替换成一个数组,数组的前n-1个元素表示的就是 因此使用这种声明方式的代码是这个样子的: var testApp = angular.module('test',['aConstant','bConstant',testCtrlFunc]);
function testCtrlFunc(a,b) {
// a 代表的就是 aConstant
// b 代表的就是 bConstant
}
其实也是换汤不换药,换了个马甲就出来骗人了。这样的写法好处缩短了一点点代码量,代码也更加紧凑了。但是这还是满足不了懒人程序员们的需求,还是太麻烦了。 于是第三种方式横空出世。在初学angular的时候,我们会写这样的代码: var testApp = angular.module('test',[]);
// 假设我们已经定义过了aConstant以及bConstant
testApp.controller('testController',function(aConstant,bConstant) {
// ......
});
这里我们既没有为控制器的函数声明 argDecl = extractArgs(fn);
forEach(argDecl[1].split(FN_ARG_SPLIT),function(arg) {
arg.replace(FN_ARG,name) {
$inject.push(name);
});
});
// extractArgs函数的定义
function extractArgs(fn) {
var fnText = Function.prototype.toString.call(fn).replace(STRIP_COMMENTS,''),args = fnText.match(ARROW_ARG) || fnText.match(FN_ARGS);
return args;
}
// 使用到的各种正则表达式
var ARROW_ARG = /^([^(]+?)=>/;
var FN_ARGS = /^[^(]*(s*([^)]*))/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^s*(_?)(S+?)1s*$/;
var STRIP_COMMENTS = /((//.*$)|(/*[sS]*?*/))/mg;
这个功能初看上去严重依赖于正则表达式。的确,它的总体思路是去解析函数的源代码,从其中提取出参数列表,然后通过参数列表来构建出需要的 来看看具体的实现过程是怎么样的:
得到了注解信息后,再来看这段代码: function injectionArgs(fn,serviceName));
}
return args;
}
该函数返回的 最后,我们来比较一下这三种注解方式。 function bootstrap(element,modules,config) {
// ......
// 如果config.strictDi为true,那么会禁用基于源代码解析的注解生成方式
var injector = createInjector(modules,config.strictDi);
// ......
}
其实这种方式不仅仅是不”严格”的问题。众所周知,应用在被部署到生产环境中之前通常还会经过一系列的处理。典型的比如JavaScript源代码的压缩和混淆,这些操作的主要目的分别是减少代码体积从而减少客户端的带宽压力以及增强代码的安全性。而混淆操作由于它会修改函数的参数名称,导致需要对象的名称和被托管对象之间的联系被切断。因此如果你的代码使用了基于源代码解析的方式实现依赖注入并且在使用前还经历了混淆处理,那么你的代码很可能就无法使用了。基于这一点,一般认为使用数组方式来实现依赖注入比较可靠, 但是这种方式相比直接使用 比如下面这款工具ng-annotate,使用它能够完成如下效果: // 定义方式如下:需要在function的内部第一行使用"ngInject"
angular.module("MyMod").controller("MyCtrl",$timeout) {
"ngInject";
...
});
// 然后通过运行ng-annotate -a 源文件 来得到自动生成的基于数组方式的注入方式:
angular.module("MyMod").controller("MyCtrl",["$scope","$timeout",$timeout) {
"ngInject";
...
}]);
但是这还是不够自动,每次还需要运行一个命令。因此借助gulp等自动化构建工具,出现了更”懒”的gulp-ng-annotate,这样依赖注入所需要的注解信息直接通过定义task的方式自动生成,而这些task还可以和其它的诸如watch等task整合在一起,每次源代码发生变化的时候都会即使更新: var gulp = require('gulp');
var ngAnnotate = require('gulp-ng-annotate');
gulp.task('default',function () {
return gulp.src('src/app.js')
.pipe(ngAnnotate())
.pipe(gulp.dest('dist'));
});
以上就是和依赖注入密切相关的注解,它在angular中的实现方式。 在下一篇文章中,会介绍angular提供给外部使用的 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |