什么是指令 (directive)
自定义HTML元素和属性
可以把它简单的理解成在特定DOM元素上运行的函数,指令可以扩展这个元素
的功能。
指令的简单实例
angular.module('myApp', [])
.directive('myDirective', function() {
return {
restrict: 'E',
replace: true,
template: '<a href="http://google.com">Click me to go to Google</a>'
};
});
四种使用方式
<my-directive></my-directive>
<div my-directive></div>
<div class="my-directive"></div>
<!--directive:my-directive-->
向指令中传递数据
在指令中设置相应的属性
template: '<a href="{{ myUrl }}">{{ myLinkText }}</a>'
在元素中传入相应 的属性值
<div my-directive
my-url="http://google.com"
my-link-text="Click me to go to Google">
</div>
共享作用域产生的问题
如果控制器被移除,或者在控制器的作用域中也
定义了一个叫myUrl的属性,我们就被迫要修改代码,这是成本很高且让人沮丧的事情。
scope: {
someProperty: "needs to be set"
}
创建了一个隔离作用域,该scope中的的属性,只能在指令 的方法或指令的模板字符串中使用
与父级的scope绑定,通过属性来传递值
scope: {
someProperty: '@'
}
//对应 的属性值
<div my-directive
some-property="someProperty with @ binding">
</div>
名称不一样的情况
scope: {
someProperty: '@someAttr'
}
//对应 的是some-attr,而不是som-property
<div my-directive
some-attr="someProperty with @ binding">
</div>
内置指令
ng-开头
directive的参数
- name(字符串)
-
- 指令的名字,用来在视图中引用特定的指令。
- factory_function (函数)
-
- 这个函数返回一个对象,其中定义了指令的全部行为。$compile服务利用这个方法返回的对象,在DOM调用指令时来构造指令的行为。
-
- 也可以直接 返回 一个函数 ,这个函数通常被称作链接传递(postLink)函数,利用它我们可以定义指令的链接(link)功能。
directive的解析流程
- 当AngularJS启动应用时,它会把第一个参数当作一个字符串,并以此字符串为名来注册第二个参数返回的对象。
- AngularJS编译器会解析主HTML的DOM中的元素、属性、注释和CSS类名中使用了这个名字的地方,并在这些地方引用对应的指令。
- 当它找到某个已知的指令时,就会在页面中插入指令所对应的DOM元素。
- 指令的工厂函数只会在编译器第一次匹配到这个指令时调用一次。和controller函数类似,我们通过$injetor.invoke来调用指令的工厂函数。
- 当AngularJS在DOM中遇到具名的指令时,会去匹配已经注册过的指令,并通过名字在注册过的对象中查找
- 此时,就开始了一个指令的生命周期,指令的生命周期开始于$compile方法并
结束于link方法
directive的全部设置
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 Object,
transclude: Boolean,
controller: String or
function(scope, element, attrs, transclude, otherInjectables) {... },
controllerAs: 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(元素)
<my-directive></my-directive>
A(属性,默认值)
<div my-directive="expression"></div>
C(类名)
<div class="my-directive:expression;"></div>
M(注释)
<--directive:my-directive expression-->
这些选项可以单独使用,也可以混合在一起使用
angular.module('myDirective', function() {
return {
restrict: 'EA' // 输入元素或属性
};
});
//调用 方式
<-- 作为一个属性 -->
<div my-directive></div>
<-- 或者作为一个元素 -->
<my-directive></my-directive>
元素方式 和属性方式的选择
- 元素:完整的 功能 ,不是对某些元素的扩展
<my-clock></my-clock>
- 属性: 给已经 存在的元素添加数据或行为
<my-clock clock-display="analog"></my-clock>
对my-clock的扩展
- 标准: 定义的指令是否包含某个组件的核心行为,或者用额外的行为、状态或者其他内容(比如模拟时钟)对某个核心组件进行修饰或扩展
- 另外一个重要的标准,是根据指令是否创建、继承或将自己从所属的环境中隔离出去进行判断。
priority 优先级(数值型)
可选 ,默认值 是0, 值越高,优先级越高,会比其他指令 先调用
如果一个元素上,有 两个相同 优先级的指令 ,声明 在前面的优先调用
ngRepeat是所有内置指令中优先级最高的,1000
terminal(布尔型)
terminal是一个布尔型参数,可以被设置为true或false。
作用
这个参数用来告诉AngularJS停止运行当前元素上比本指令优先级低的指令。但同当前指令
优先级相同的指令还是会被执行。
例子
使用了terminal参数的例子是ngView和ngIf。ngIf的优先级略高于ngView,如果ngIf的表
达式值为true,ngView就可以被正常执行,但如果ngIf表达式的值为false,由于ngView的优先级较低就不会被执行。
template(字符串或函数)
可选,两种形式:
- 一段HTML文本;
- 一个可以接受两个参数的函数,参数为tElement和tAttrs,并返回一个代表模板的字符
串
参数为tElement和tAttrs是什么?
tElement: 模板所在的元素
tAttrs: 模板 所在的元素的属性
模板的要求 :
- AngularJS会同处理HTML一样处理模板字符串。模板中可以通过大括号标记来访问作用域,
例如{{ expression }}。 - 如果模板字符串中含有多个DOM元素,或者只由一个单独的文本节点构成,那它必须被包
含在一个父元素内。换句话说,必须存在一个根DOM元素:
template: '\
<div> <-- single root element -->\
<a href="http://google.com">Click me</a>\
<h1>When using two elements, wrap them in a parent element</h1>\
</div>\
- 另外,注意每一行末尾的反斜线,这样AngularJS才能正确解析多行字符串。在实际生产中,更好的选择是使用templateUrl参数引用外部模板,因为多行文本阅读和维护起来都是一场噩梦。
templateUrl(字符串或函数)
可选 参数 ,两种方式
- 一个代表外部HTML文件路径的字符串;
- 一个可以接受两个参数的函数,参数为tElement和tAttrs,并返回一个外部HTML文件
路径的字符串。
实现原理
- 模板的URL都将通过AngularJS内置的安全层, 特别是$getTrustedResourceUrl,这样可以保护模板不会被不信任的源加载。
- 调用指令时会在后台通过Ajax来请求HTML模板文件
- 在本地开发时,需要在后台运行一个本地服务器,用以从文件系统加载HTML模板,否则
会导致Cross Origin Request Script(CORS)错误。 –所以需要 开启xampp - 模板加载是异步的,意味着编译和链接要暂停,等待模板加载完成。
- 通过Ajax异步加载大量的模板将严重拖慢一个客户端应用的速度, 可以在部署之前 对HTML模板进行缓存 ,(第28章)
replace(布尔型)
可选参数 ,布尔型 ,默认值 是false
false: 模板会被 当作 子元素插入到调用 些指令 的元素内部
<div some-directive></div>
.directive('someDirective', function() {
return {
template: '<div>some stuff here<div>'
};
});
调用 后的样子:
<div some-directive>
<div>some stuff here<div>
</div>
true:的样子,当作同级的元素
<div>some stuff here<div>
scope 指令的作用域
可选 ,默认是false
false: 使用父作用域,指令中对属性的修改,会直接 作用到父级作用域中
true: 从父作用域继承 ,并创建一个新的作用域对象 ,指令 中可以访问 父级的作用域,修改不会影响 到父级
{}: 创建一个隔离作用域,不能访问 父级的作用域,修改也不会影响 到父级
隔离作用域的绑定策略
- @: 本地作用域属性 ,使用@符号将本地作用域同DOM属性的值进行绑定。指令内部作用域可以
使用外部作用域的变量 - =: 双向绑定:通过=可以将本地作用域上的属性同父级作用域上的属性进行双向的数据绑定。
就像普通的数据绑定一样,本地属性会反映出父数据模型中所发生的改变。 - & 与父级的方法绑定
<input type="text" ng-model="to" />
<!-- 调用指令 -->
<div scope-example ng-model="to" on-send="sendMail(email)" from-name="ari@fullstack.io" />
scope: {
ngModel: '=', // 将ngModel同指定对象绑定
onSend: '&', // 将引用传递给这个方法
fromName: '@' // 储存与fromName相关联的字符串
}
transclude 嵌入
可选参数 ,默认值 是false,决定 元素下的html代码 是否嵌入到模板 中
只有当你希望创建一个可以包含任意内容的指令时,才使用transclude: true。
- 例子:
<div sidebox title="Links">
<ul>
<li>First link</li>
<li>Second link</li>
</ul>
</div>
angular.module('myApp', [])
.directive('sidebox', function() {
return {
restrict: 'EA',
scope: {
title: '@'
},
transclude: true,
template: '<div class="sidebox">\
<div class="content">\
<h2 class="header">{{ title }}</h2>\
<span class="content" ng-transclude>\
</span>\
</div>\
</div>'
};
});
嵌入 的位置 是 ng-transclude标签片,
- 不足:
如果指令使用了transclude参数,那么在控制器(下面马上会介绍)中就无法正常监听数
据模型的变化了。这就是最佳实践总是建议在链接函数里使用$watch服务的原因。
controller(字符串或函数)
controller参数可以是一个字符串或一个函数
angular.module('myApp', [])
.directive('myDirective', function() {
restrict: 'A', // 始终需要
controller: 'SomeController'
})
// 应用中其他的地方,可以是同一个文件或被index.html包含的另一个文件
angular.module('myApp')
.controller('SomeController', function($scope, $element, $attrs, $transclude) {
// 控制器逻辑放在这里
});
可以在指令内部通过匿名构造函数的方式来定义一个内联的控制器
angular.module('myApp', [])
.directive('myDirective', function() {
restrict: 'A',
controller: function($scope, $element, $attrs, $transclude) {
// 控制器逻辑放在这里
}
});
参数说明 :
-
scope与指令元素相关联的当前作用域。−
element 当前指令对应的元素。
- $attrs 由当前元素的属性组成的对象。例如,下面的元素:
<div id="aDiv"class="box"></div>
$attrs接收到的值 如下 :
{
id: "aDiv",
class: "box"
}
- $transclude
可以嵌入链接 函数 ,链接 函数 会与对应 的嵌入 作用域进行预绑定
作用:
用来克隆元素和操作DOM的函数。 -
- 在控制器内部操作DOM是和AngularJS风格相悖的做法,但通过链接函数就可以
实现这个需求。仅在compile参数中使用transcludeFn是推荐的做法。
添加 超链接的实例:
- 在控制器内部操作DOM是和AngularJS风格相悖的做法,但通过链接函数就可以
angular.module('myApp')
.directive('link', function() {
return {
restrict: 'EA',
transclude: true,
controller: function($scope, $element, $transclude, $log) {
$transclude(function(clone) {
var a = angular.element('<a>');
a.attr('href', clone.text());
a.text(clone.text());
$log.info("Created new a tag in link directive");
$element.append(a);
});
}
};
});
clone 参数 是什么?
与link的差别:
指令的控制器和link函数可以进行互换。控制器主要是用来提供可在指令间复用的行为,但链接函数只能在当前内部指令中定义行为,且无法在指令间复用。
link函数可以将指令互相隔离开来,而controller则定义可复用的行为。
如果我们希望将当前指令的API暴露给其他指令使用,可以使用controller参数,否则可以
使用link来构造当前指令元素的功能性。
如果我们使用了scope.$watch()或者想要与DOM元素做实时的交互,使用链接会是更好的选择。
controllerAs(字符串)
controllerAs参数用来设置控制器的别名,可以以此为名来发布控制器
作用:
在路由和指令中创建匿名控制器的强大
能力。这种能力可以将动态的对象创建成为控制器,并且这个对象是隔离的、易于测试的。
require(字符串或数组)
加载 其他 指令 的控制器
作为当前 指令 链接 函数 的第四个 参数
修饰:
- ?:如果在当前指令中没有找到所需要的控制器,会将null作为传给link函数的第四个参数
- ^:如果添加了^前缀,指令会在上游的指令链中查找require参数所指定的控制器。
- ?^:将前面两个选项的行为组合起来,我们可选择地加载需要的指令并在父指令链中进行查找。
- 没有前缀:如果没有前缀,指令将会在自身所提供的控制器中进行查找,如果没有找到任何控制器(或具有指定名字的指令)就抛出一个错误。
angularjs的生命周期
编译阶段
- 编译阶段的过程
在编译阶段,AngularJS会遍历整个HTML文档并根据JavaScript中的指令定义来处理页面上声明的指令。
一旦对指令和其中的子模板进行遍历或编译,编译后的模板会返回一个叫做模板函数的函
数。
我们有机会在指令的模板函数被返回前,对编译后的DOM树进行修改。
在这个时间点DOM树还没有进行数据绑定,意味着如果此时对DOM树进行操作只会有很少
的性能开销。
基于此点,ng-repeat和ng-transclude等内置指令会在这个时候,也就是还未与
任何作用域数据进行绑定时对DOM进行操作。
例子:ng-repeat 它会遍历指定的数组或对象,在数据绑定之前构建出对应的DOM结构。
一个指令一旦编译完成,马上就可以通过编译函数对其进行访问,编译函数的签名包含有访问指令声明所在的元素(tElemente)及该元素其他属性(tAttrs)的方法。这个编译函
数返回前面提到的模板函数,其中含有完整的解析树。
由于每个指令都可以有自己的模板和编译函数,每个模板返回的也都是自己
的模板函数。链条顶部的指令会将内部子指令的模板合并在一起成为一个模板函数并返回,但在树的内部,只能通过模板函数访问其所处的分支。
最后,模板函数被传递给编译后的DOM树中每个指令定义规则中指定的链接函数
compile(对象或函数)
- 作用:在指令和实时数据被放到DOM中之前进行DOM操作,在这个函数中进行诸如添加和删除节点等DOM操作是安全的。
- compile和link选项是互斥的。如果同时设置了这两个选项,那么会把compile所返回的函数当作链接函数,而link选项本身则会被忽略。
- 编译函数负责对模板DOM进行转换。
- 链接函数负责将作用域和DOM进行链接。
链接阶段
用link函数创建可以操作DOM的指令。
作用:负责设置事件监听器,监视数据变化和实时的操作DOM。
参数:
// require 'SomeController',
link: function(scope, element, attrs, SomeController) {
// 在这里操作DOM,可以访问required指定的控制器
}
- scope 指令用来在其内部注册监听器的作用域。
- iElement iElement参数代表实例元素,指使用此指令的元素。在postLink函数中我们应该只操作此
元素的子元素,因为子元素已经被链接过了。 - iAttrs iAttrs参数代表实例属性,是一个由定义在元素上的属性组成的标准化列表,可以在所有指令的链接函数间共享。会以JavaScript对象的形式进行传递。
- controller controller参数指向require选项定义的控制器。如果没有设置require选项,那么
controller参数的值为undefined
实际的应用场景例子
- 为元素绑定一个点击事件,点击后,获取 下cookie,提交日志信息
- 监听元素上的keydown和keyup等事件,做相应 的处理