腊月的季节

angularjs之指令深入版

指令简介

对于指令,可以把它简单的理解成在特定DOM元素上运行的函数,指令可以扩展这个元素的功能。
例如,ng-click可以让一个元素能够监听click事件,并在接收到事件的时候执行angularjs表达式。正是指令使得angularjs这个框架变得强大,并且正如所见,我们可以自己创造新的指令。
angularjs应用的模块中有很多方法可以使用,其中directive()这个方法是用来定义指令的:

1
2
3
4
angular.module('myApp',[])
.directive('myDirective',function($timeout,userDefinedService){
//指令放在这里
});

1、name(字符串)
指令的名字,用来在视图中引用特定的指令。
2、factory_function(函数)
这个函数返回一个对象,其中定义了指令的全部行为。$compile服务利用这个方法返回的对象,在DOM调用指令时来构造指令的行为。

1
2
3
4
5
6
7
angular.module('myApp',[])
.directive('myDirective',function(){
//一个指令定义对象
return {
//通过设置项来定义指令,在这里进行重写
};
});

我们也可以返回一个函数代替对象来定义指令,但是像上面的例子一样,通过对象来定义是最佳的方式。当返回一个函数时,这个函数通常被称作链接传递函数,利用它我们可以定义指令的链接功能。
当angularjs启动应用时,它会把第一个参数当做一个字符串,并以此字符串为名来注册第二个参数返回的对象。angularjs编译器会解析HTML的DOM中的元素、属性、注释和CSS类名中使用了这个名字的地方,并在这些地方引用对应的指令。当它找到某个已知的指令时,就会在页面中插入指令所对应的DOM元素。
指令的工厂函数只会在编译器第一次匹配到这个指令时调用一次。和controller函数类似,我们通过\$injector.invoke来调用指令的工厂函数。
当angularjs在DOM中遇到具名的指令时,会去匹配已经注册过的指令,并通过名字在注册过的对象中查找。此时,就开始了一个指令的生命周期,指令的生命周期开始于$compile方法并结束于link方法。

指令全部设置

全部配置选项如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
angular.module('myApp',[])
.directive('myDirective',function(){
return{
restrict:String,
priority:Number,
terminal:Boolean,
template:String or Template Function:
function(tElement,tAttrs){...},
templateUrl:String,
replace:Boolean or String,
scope:Boolean or String,
transclude:String or function(scope,element,attrs,transclude,otherInjectables){...},
controllers:String,
require:String,
link:function(scope,iElement,iAttrs){...},
compile:
function(tElement,tAttrs,transclude){
return{
pre:function(scope,iElement,iAttrs,controller){...},
post:function(scope,iElement,iAttrs,controller){...}
}
return function postLink(...){...}
}
};
});

restrict(字符串配置)

restrict是一个可选的参数。它告诉angularjs这个指令在DOM中可以何种形式被声明。默认angularjs认为restrict的值是A,即以属性的形式来进行声明。
可选值如下:
E(元素)


A(属性,默认值)


C(类名)


M(注释)
<–directive:my-directive expression–>
这些选项可以单独使用,也可以混合在一起使用:

1
2
3
4
5
angular.module('myDirective',function(){
return {
restrict:'EA' //输入元素或属性
};
});

属性是用来声明指令最常用的方式,因为它能在包括老版本的IE浏览器在内的所有浏览器中正常工作,并且不需要在文档头部注册新的标签。
元素方式还是属性方式
在页面中通过元素方式创建新的指令可以将一些功能封装在元素内部。例如,如果我们想要做一个时钟,可以创建一个clock指令,然后在DOM中用如下代码来声明:


这样做可以告诉指令的使用者,这里会完整包含应用的某一些内容。这个时钟并不是对一个既有时钟的修饰或扩展,而是一个全新的单元。尽管这里也可以使用属性形式声明指令,但我们选择了元素形式,因为这样可以更明确地表达意图。
用属性形式来给一个已经存在的元素添加数据或行为。以时钟为例,假设我们更喜欢模拟时钟:


如何进行选择,通常取决于定义的指令是否包含某个组件的核心行为,或者用额外的行为、状态或者其他内容对某个核心组件进行修饰或扩展。
另外一个重要的标准,是根据指令是否创建、继承或将自己从所属的环境中隔离出去进行判断。指令的父子关系对其组成和重用性起着至关重要的作用,会有额外的内容来更加深入地讨论指令的作用域。

priority优先级(数字)

优先级参数可以被设置为一个数值。大多数指令会忽略这个参数,使用默认值0,但也有些场景设置高优先级是非常重要甚至是必须的。例如,ngRepeat将这个参数设置为1000,这样就可以保证在同一元素上,它总是在其他指令之前被调用。
注意:ngRepeat是所有内置指令中优先级最高的,它总是在其他指令之前运行。这样设置主要考虑的是性能。

terminal(布尔型)

terminal是一个布尔型参数,可以被设置为true或false。
这个参数用来告诉angularjs停止运行当前元素上比本指令优先级低的指令。但同当前指令优先级相同的指令还是会被执行。
使用了terminal参数的例子是ngView和ngIf。ngIf的优先级略高于ngView,如果ngIf的表达式值为true,ngView就可以被正常执行,但如果ngIf表达式的值为false,由于ngView的优先级较低就不会被执行。

template(字符串或函数)

template参数是可选的,必须被设置为以下两种形式之一:
一段HTML文本。
一个可以接受两个参数的函数,参数为tElement和tAttrs,并返回一个代表模板的字符串。tElement和tAttrs中的t代表template,是相对于instance。在讨论链接和编译设置时会详细介绍,模板元素或属性与实例元素或属性之间的区别。
angularjs会同处理HTML一样处理模板字符串。模板中可以通过大括号来访问作用域,例如。
如果模板字符串中含有多个DOM元素,或者只由一个单独的文本节点构成,那它必须被包含在一个父元素内。换句话说,必须存在一个根DOM元素:

1
2
3
4
5
template: '\
<div><-- single root element -->\
<a href="http://www.baidu.com">Click me</a>\
<h1>when using two elements,wrap them in a parent element</h1>\
</div>\'

另外,注意每一行末尾的反斜线,这样angularjs才能正确解析多行字符串。在实际生产中,更好的选择是使用templateUrl参数引用外部模板。
模板字符串和templateURL中最需要了解的重要功能,是它们如何同作用域链接起来。

templateUrl(字符串或函数)

templateUrl是可选的参数,可以是以下类型:
一个代表外部HTML文件路径的字符串
一个可以接受两个参数的函数,参数为tElement和tAttrs,并返回一个外部HTML文件路径的字符串。
无论哪种方式,模板的URL都将通过AngularJS内置的安全层,特别是$getTrustedResourceUrl,这样可以保护模板不会被不信任的源加载。
默认情况下,调用指令时会在后台通过ajax来请求HTML模板文件。有两件事需要知道。
在本地开发时,需要在后台运行一个本地服务器,用以从文件系统加载HTML模板,否则会导致Cross Origin Request script(CORS)错误.
模板加载时异步的,意味着编译和链接要暂停,等待模板加载完成。
通过Ajax异步加载大量的模板将严重拖慢一个客户端应用的速度。为了避免延迟,可以在部署应用之间对HTML模板进行缓存。在大多数场景下缓存都是一个非常好的选择,因为angularjs通过减少请求数量提升了性能。
模板加载后,angularjs会将它默认缓存到$templateCache服务中。在实际生产中,可以提前将模板缓存到一个定义模板的javascript文件中,这样就不需要通过XHR来加载模板了。

replace(布尔型)

replace是一个可选参数,如果设置了这个参数,值必须为true,因为默认值为false。默认值意味着模板会被当做子元素插入到调用此指令的元素内部:

1
2
3
4
5
6
<div some-directive></div>
.directive('someDirective',function(){
return{
template:'<div>some stuff here</div>'
};
});

调用指令之后的结果如下(这是默认replace为false时的情况):

1
2
3
<div some-directive>
<div>some stuff here</div>
</div>

如果replace被设置为了true:

1
2
3
4
5
6
.directive('someDirective',function(){
return {
replace:true,//修饰过
template:'<div>some stuff here</div>'
};
});

指令调用后的结果

1
<div>some stuff here</div>

指令作用域

为了完全理解指令定义对象中剩下的参数,需要先介绍指令作用域是如何工作的。
$rootScope这个特殊的对象会在DOM中声明ng-app时被创建:

1
2
3
4
5
<div ng-app="myApp" ng-init="someProperty='some data'"></div>
<div ng-init="siblingProperty='more data'">
Inside Div Two
<div ng-init="aThirdProperty"></div>
</div>

上面的代码中,我们在应用的根作用域中设置了三个属性:someProperty、siblingProperty和anotherSiblingProperty。
从这里开始,DOM中每个指令调用时都可能会:
直接调用相同的作用域对象。
从当前作用域对象继承一个新的作用域对象。
创建一个同当前作用域想隔离的作用域对象。
上面的例子展示的是第一种情况。前两个div是兄弟元素,可以通过get和set访问$rootScope。第二个div内部的div同样可以通过get和set访问相同的根作用域。
指令嵌套并不一定意味着需要改变它的作用域。默认情况下,子指令会被付予访问父DOM元素对应的作用域的能力,这样做的原因可以通过介绍指令的scope参数来理解,scope参数默认是false。

scope参数(布尔型或对象)

scope参数是可选的,可以被设置为true或一个对象。默认值是false。
当scope设置为true时,会从父作用域继承并创建一个新的作用域对象。
如果一个元素上有多个指令使用了隔离作用域,其中只有一个可以生效。只有指令模板中的根元素可以获得一个新的作用域。因此,对于这些对象来说scope默认被设置为true。
内置指令ng-controller的作用,就是从父级作用域继承并创建一个新的子作用域。它会创建一个新的从父作用域继承而来的子作用域。
用这些新内容更新一下前面的例子:

1
2
3
4
5
6
7
8
9
10
11
<div ng-app="myApp" ng-init="someProperty='some data'">
<div ng-init="siblingProperty='moredata'">
Inside Div Two:{{aThirdProperty}}
<div ng-init="aThirdProperty='data for 3rd property'" ng-controller="someController">
Inside Div Three :{{aThirdProperty}}
<div ng-init="aFourthProperty">
Inside Div Four:{{aThirdProperty}}
</div>
</div>
</div>
</div>

如果直接运行代码会报错,因为没有javascript中定义所需的控制器,下面就来定义这个控制器:

1
2
3
4
angular.module('myApp',[])
.controller('SomeController',function($scope){
//可以留空,但需要被定义
});

刷新页面,会发现第二个div中由于未定义,因此什么都没有输出。第三个div显示了设置在继承来的作用有中的data for a 3rd property。
为了进一步证明作用域的继承机制是向下而非向上进行的,下面再看另外一个例子,展示的是从父作用域继承而来:

1
2
3
4
5
6
7
8
9
10
<div ng-app="myApp" ng-init="someProperty='some data'"></div>
<div ng-init="siblingProperty='moredata'">
Inside Div Two:{{aThirdProperty}}
<div ng-init="aThirdProperty='data for 3rd property'" ng-controller="SomeController">
Inside Div Three:{{aThirdProperty}}
<div ng-controller="SecondController">
Inside Div Four:{{aThirdProperty}}
</div>
</div>
</div>

输出

1
2
3
Inside Div:Two;
Inside Div Three:data for 3rd property
Inside Div Four:data for 3rd property

在javascript中加入SecondController的定义:

1
2
3
4
5
6
7
angular.module('myApp',[])
.controller('SomeController',function($scope){
//可以留空,但需要被定义
})
.controller('SecondController',function($scope){
//同样可以留空
})

如果要创建一个能够从外部原型继承作用域的指令,将scope属性设置为true:

1
2
3
4
5
6
7
angular.module('myApp',[])
.directive('myDirective',function(){
return {
restrict:'A',
scope:true
};
});

下面用指令来改变DOM的作用域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div ng-app="myApp" ng-init="someProperty='some data'"></div>
<div ng-init="siblingProperty='moredata'">
Inside Div Two:{{aThirdProperty}}
<div ng-init="aThirdProperty='data for 3rd property'" ng-controller="SomeController" >
Inside Div Three:{{aThirdProperty}}
<div ng-controller="SecondController">
Inside Div Four:{{aThirdProperty}}
<br/>
Outside myDirective:{{myProperty}}
<div my-directive ng-init="myProperty='wow,this is cool'">
Inside myDirective:{{myProperty}}
</div>
</div>
</div>
</div>

热评文章