一个对象通常有三种方式可以获得对其依赖的控制权:
(1) 在内部创建依赖;
(2) 通过全局变量进行引用;
(3) 在需要的地方通过参数进行传递。
依赖注入是通过第三种方式实现的。其余两种方式会带来各种问题,例如污染全局作用域,使隔离变得异常困难等。依赖注入是一种设计模式,它可以去除对依赖关系的硬编码,从而可以在运行时改变甚至移除依赖关系。在运行时修改依赖关系的能力对测试来讲是非常理想的,因为它允许我们创建一个隔离的环境,从而在测试环境可以使用模拟的对象取代生产环境中的真实对象。从功能上看,依赖注入会事先自动查找依赖关系,并将注入目标告知被依赖的资源,这样就可以在目标需要时立即将资源注入进去。在编写依赖于其他对象或库的组件时,我们需要描述组件之间的依赖关系。在运行期,注入器会创建依赖的实例,并负责将它传递给依赖的消费者。
SomeClass能够在运行时访问到内部的greeter,但它并不关心如何获得对greeter的引用。为了获得对greeter实例的引用, SomeClass的创建者会负责构造其依赖关系并传递进去。基于以上原因, AngularJS使用$injetor(注入器服务)
来管理依赖关系的查询和实例化。事实上, $injetor
负责实例化AngularJS中所有的组件,包括应用的模块、指令和控制器等。
在运行时, 任何模块启动时$injetor
都会负责实例化,并将其需要的所有依赖传递进去。例如下面这段代码。这是一个简单的应用,声明了一个模块和一个控制器:
当AngularJS实例化这个模块时,会查找greeter并自然而然地把对它的引用传递进去:
而在内部, AngularJS的处理过程是下面这样的:
在任何一个AngularJS的应用中,都有$injector
在进行工作,无论我们知道与否。当编写控制器时,如果没有使用[]标记或进行显式的声明, $injector
就会尝试通过参数名推断依赖关系。
如果没有明确的声明, AngularJS会假定参数名称就是依赖的名称。因此,它会在内部调用函数对象的toString()
方法,分析并提取出函数参数列表,然后通过$injector
将这些参数注入进对象实例。注入的过程如下:
请注意,这个过程只适用于未经过压缩和混淆的代码,因为AngularJS需要原始未经压缩的参数列表来进行解析。有了这个根据参数名称进行推断的过程,参数顺序就没有什么重要的意义了, 因为AngularJS会帮助我们把属性以正确的顺序注入进去。
JavaScript的压缩器通常会将参数名改写成简单的字符,以减小源文件体积(同
时也会删除空格、空行和注释等)。如果我们不明确地描述依赖关系,AngularJS将无法根据参数名称推断出实际的依赖关系,也就无法进行依赖注入。
AngularJS提供了显式的方法来明确定义一个函数在被调用时需要用到的依赖关系。通过这种方法声明依赖,即使在源代码被压缩、参数名称发生改变的情况下依然能够正常工作。可以通过$inject
属性来实现显式注入声明的功能。函数对象的$inject
属性是一个数组,数组元素的类型是字符串,它们的值就是需要被注入的服务的名称。下面是示例代码:
对于这种声明方式来讲,参数顺序是非常重要的,因为$inject
数组元素的顺序必须和注入参数的顺序一一对应。这种声明方式可以在压缩后的代码中运行,因为声明的相关信息已经和函数本身绑定在一起了。
AngularJS提供的注入声明的最后一种方式,是可以随时使用的行内注入声明。这种方式其实是一个语法糖,它同前面提到的通过$inject
属性进行注入声明的原理是完全一样的,但允许我们在函数定义时从行内将参数传入。此外,它可以避免在定义过程中使用临时变量。在定义一个AngularJS的对象时,行内声明的方式允许我们直接传入一个参数数组
而不是一个函数。数组的元素是字符串,它们代表的是可以被注入到对象中的依赖的名字,最后一个参数就是依赖注入的目标函数对象本身。示例如下:
由于需要处理的是一个字符串组成的列表,行内注入声明也可以在压缩后的代码中正常运行。通常通过括号和声明数组的[]符号来使用它。
annotate()方法的返回值是一个由服务名称组成的数组,这些服务会在实例化时被注入到目标函数中。 annotate()方法可以帮助$injector判断哪些服务会在函数被调用时注入进去。
annotate()方法可以接受一个参数:
fn(函数或数组)
参数fn可以是一个函数,也可以是一个数组。 annotate()方法返回一个数组,数组元素的值是在调用时被注入到目标函数中的服务的名称。
get()
get()方法返回一个服务的实例
get()方法可以接受一个参数,get()根据名称返回服务的一个实例。
name(字符串)
参数name是想要获取的实例的名称。
has()
has()方法返回一个布尔值,在$injector能够从自己的注册列表中找到对应的服务时返回true,否则返回false。
has()方法能接受一个参数:
name(字符串)
参数name是我们想在注入器的注册列表中查询的服务名称。
instantiate()
instantiate()方法可以创建某个JavaScript类型的实例。它会通过new操作符调用构造函数,并将所有参数都传递给构造函数。
instantiate()方法可以接受两个参数,instantiate()方法返回Type的一个新实例。
Type(函数)
构造函数。
locals(对象,可选)
这是一个可选的参数,提供了另一种传递参数的方式。
invoke()
invoke()方法会调用方法并从$injector中添加方法参数。
invoke()方法接受三个参数,invoke()方法返回fn函数返回的值。
fn(function)
这个函数就是要调用的函数。这个函数的参数由函数声明设置。
self (object-可选)
self参数允许我们设置调用方法的this参数。
locals (object-可选)
这个可选参数提供另一种方式在函数被调用时传递参数名给该函数。
上面介绍了三种声明依赖注入的方式,可以在定义函数时选择任意一种合适的方式。但在实际生产过程中,当代码体积变得非常庞大时,写代码还要关心参数顺序将是一个耗费心力的工作。通过使用ngMin这个工具,能够减少我们定义依赖关系所需的工作量。 ngMin是一个为AngularJS应用设计的预压缩工具,它会遍历整个AngularJS应用并帮助我们设置好依赖注入。例如,它会将如下代码:
转换成下面的形式:
ngMin可以显著减少代码输入的工作量,并保持源文件的整洁。
安装
可以通过npm包管理工具来安装ngMin:
使用ngMin
我们可以在命令行界面单独使用ngMin,可以通过标准输入输出设备或标准输出流传入input.js和output.js两个参数,例如:
input.js是源文件,而 output.js则是转换过注入声明后的输出文件。
工作原理
在其内部, ngMin使用抽象语法树(Abstract Syntax Tree, AST)来遍历JavaScript源代码。借助名为astral的AST工具框架的帮助,它可以将必要的声明代码添加进源文件,并用escodegen将转换后的源文件输出。ngMin希望我们的AngularJS源代码只由逻辑定义组成。如果我们书写代码的语法和这本书里的一样,那么ngMin就可以对其进行解析和预压缩。