AngularJS系统学习之Directive(指令)
本文转自https://www.w3ctech.com/topic/1612
在这系列的上一篇文章,我讨论了scope事件以及digest循环的行为。这一次,我将谈论指令。这篇文章包括 独立的scope,内嵌,link函数,编译器,指令控制器等等。
如果这个图表看起来非常的费解,那么这篇文章很适合你。
(Image credit: Angular JS documentation) (Large version)
声明: 这篇文字是基于 AngularJS v1.3.0 tree.
到底什么是指令(directive)?Link
AngularJS中,指令是 通常是小的 组件, 这意味着跟DOM交互。他经常被用作顶层DOM的抽象层,大多数的操作可以不用jQuery,jqLite等包装的DOM元素。通过使用表达式、其他的指令来得到你想要的结果是高明的。
在AngularJS的核心里,指令可以绑定元素的属性(例如可见性,class列表,内部文本,内部HTML或者值)到scope的属性或表达式。最值得注意的是,一旦监测到scope中的变化被标记,这些绑定就会被更新。反过来也是相似的,使用$observe函数能够监测DOM属性,当监测到属性变化时会触发一个回调。
简单的说,指令是AngularJS中很重要的一面。如果你精通指令,那么处理AngularJS程序你将不会有任何问题。同理,如果你不设法理解指令,你将很难将其用在合适的地方。熟练指令需要时间,尤其是你在尝试不仅仅是用jQuery封装代码就完事。
在AngularJS,你能够建立组件化的指令、服务和控制器,它们可以复用,只要复用是合理的。例如你有一个简单的指令,基于一个监测的scope表达式来切换class ,在你的代码中用来标识你的特定组件的状态,我想那是一个十分通用的指令,能够在你的程序里到处使用到。你可以有一个服务集成键盘快捷键的服务、控制器、指令和其他注册快捷键的服务,它支持所有键盘快捷键的处理在一个自包含的服务中。
指令也是可重用的功能,但经常被分配给 DOM 片段,或者模板,而不是仅仅提供功能。是时候深入了解 AngularJS 指令及其他的用法了。
创建一个指令 Link
之前,我列出了 AngularJS 中 scope 上可用的属性,我用他来解释 digest 机制以及 scope 如何操作的。 我会用同样的方式来解释指令, 但是这次我将剖析指令的工厂函数返回的对象的属性,以及每个这些属性如何影响我们所定义的指令。
首先要注意下指令的名字, 来看一个简单的例子。
angular.module('PonyDeli').directive('pieceOfFood', function () {
var definition = { //
尽管在上面的的代码片段中我们定义了一个命名为'pieceOfFood'的指令,AngularJS约定在 HTML 标记里使用破折号的形式连接名字。如果这个指令作为一个属性实现,那么我在 HTML 中就会像这样调用:
<span piece-of-food></span>
默认情况,指令只能作为属性被触发。但是如果你想改变这种方式,你可以使用 restrict 属性。
如何定义一个指令作为标签使用。
angular.module('PonyDeli').directive('pieceOfFood', function () {
return {
restrict: 'E',
template: // ...
};
});
出于某种原因,我无法捉摸它们决定混淆什么,本来一个很有表达能力的框架,却以单个大写字母结尾来定义指令是如何被限制的。GitHub 上有一个可用的restrict选项列表, restrict 的默认值是 EA
- 'A': attributes are allowed
- 'A': 允许作为一个属性
<span piece-of-food></span>
- 'E': elements are allowed
- 'E': 允许作为一个元素
<piece-of-food></piece-of-food>
- 'C': as a class name
- 'C': 作为一个类名
<span class='piece-of-food'></span>
- 'M': as a comment
- 'M': 作为一个注释
<!-- directive: piece-of-food -->
- 'AE': You can combine any of these to loosen up the restriction a bit.
- 'AE': 可以结合上面的任意值来放松限制。
千万别用 'C' 或者 'M' 来限制你的指令。 用 'C' 不能使之在标记中凸显出来, 用 'M' 是为了向后兼容。 如果你觉得有趣, 你可以用一个例子来设置 restrict 为 'ACME'。
不幸的是, 指令定义对象的其他属性是很难理解的。
如何设置一个指令与父级 scope 交互。
因为我们在之前的文章中大范围的谈论了 scope,知道如何正确地使用 scope 属性,所以不应为此感到痛苦。我们从默认值开始, scope: false,使作用域链保持不受影响:依照我在上篇文章提到的规则你将得到与元素相关联的所有作用域。
当你的指令不会和 scope有互动,保持作用域链不变显然是有用的,但这种情况很少发生。一种更常见有用的情景是,不改变作用域创建一个指令,给他一个作用域,实例化多次并且只跟一个scope属性交互———指令的名字。跟默认值restrict: 'A'结合是最有表达力的。(下面的代码在 Codepen上可用)
angular.module('PonyDeli').directive('pieceOfFood', function () {
return {
template: '{{pieceOfFood}}',
link: function (scope, element, attrs) {
attrs.$observe('pieceOfFood', function (value) {
scope.pieceOfFood = value;
});
}
};
});
<body ng-app='PonyDeli'>
<span piece-of-food='Fish & Chips'></span>
</body>
这里有几个值得注意的点我们还没讨论到。你将在后面的章节了解到 link 属性。 暂且想一下作为一个控制器如何操作每个实例化的指令。
在指令的链接函数里,我们可以获得元素上的属性集合。这个集合有一个特殊的方法,叫$observe(), 当一个属性变化时可以触发一个回调。没有监听属性变化时,属性永远不会对应到scope上,也无法绑定到我们的模板上。
我们可以改下上面的代码,通过引进scope.$eval,让他更可用。记得他是如何依靠scope被用来解析一个表达式的吗?看下面的代码帮助我们更好的理解(也可以查看 Codepen )。
var deli = angular.module('PonyDeli', []);
deli.controller('foodCtrl', function ($scope) {
$scope.piece = 'Fish & Chips';
});
deli.directive('pieceOfFood', function () {
return {
template: '{{pieceOfFood}}',
link: function (scope, element, attrs) {
attrs.$observe('pieceOfFood', function (value) {
scope.pieceOfFood = scope.$eval(value);
});
}
};
});
<body ng-app='PonyDeli' ng-controller='foodCtrl'>
<span piece-of-food='piece'></span>
</body>
这个例子中,通过 scope 我解析出了属性的值 piece,这个值定义在controller 中的 $scope.piece。当然,直接使用模板方式如{{piece},但是那样需要你特别注意你想追踪的scope属性。这种方式增加了一点灵活性,但当你想在在所有的指令间共享scope时, 如果你尝试用同样的scope添加多个指令则会导致意外的结果 。
好玩的子作用域 Link
你可以创建一个子作用域来解决这个问题, 他继承自父级的原型。为了创建子作用域, 你仅仅需要声明 scope: true。
var deli = angular.module('PonyDeli', []);
deli.controller('foodCtrl', function ($scope) {
$scope.pieces = ['Fish & Chips', 'Potato Salad'];
});
deli.directive('pieceOfFood', function () {
return {
template: '{{pieceOfFood}}',
scope: true,
link: function (scope, element, attrs) {
attrs.$observe('pieceOfFood', function (value) {
scope.pieceOfFood = scope.$eval(value);
});
}
};
});
<body ng-app='PonyDeli' ng-controller='foodCtrl'>
<p piece-of-food='pieces[0]'></p>
<p piece-of-food='pieces[1]'></p>
</body>
正如你所见,现在我们可以使用指令的多个实例来达到预期的效果,因为每个指令都创建了自己的作用域。 但是,这里有一个局限:一个元素的多个指令都是一个相同的作用域。
注意:如果同一元素的多个指令需要新的作用域,那么只会创建一个作用域。
独立的,隔绝的scopeLink
最后一个选项是用来创建一个本地的,独立的作用域。独立的作用域跟子作用域不同在于前者不是继承自他的父级(但是也可以通过 scope.$parent 访问)。你可以像这样声明一个独立的作用域:scope: {}。你可以添加一些属性到这个对象,用来从父级scope获取数据绑定并且当前作用域也可访问。很像restrict,独立scope的属性简洁但语法复杂,你可以用符号例如:&,@ 和=来定义属性的绑定方式。
你可以省略属性名如果你打算使用你本地scope的属性名。那就是说,pieceOfFood: '=' 是 pieceOfFood: '=pieceOfFood'的简写;他们是相等的。
选择你的武器: @,& 或者= Link
那么,这些符号是什么意思?下面枚举的例子,可以帮助你破解他们
属性观察器: @ Link
使用 @ 绑定父级作用域]监测属性的结果。
<body ng-app='PonyDeli' ng-controller='foodCtrl'>
<p note='You just bought some {{type}}'></p>
</body>
deli.directive('note', function () {
return {
template: '{{note}}',
scope: {
note: '@'
}
};
});
这等效于观察属性变化来更新本地scope。当然,用 @ 符号是更多的“AngularJS”。
deli.directive('note', function () {
return {
template: '{{note}}',
scope: {},
link: function (scope, element, attrs) {
attrs.$observe('note', function (value) {
scope.note = value;
});
}
};
});
当指令的选项很复杂时,属性监测器很有用。如果我们想通过改变选项来改变指令的行为,我们自己写代码使用attrs.$observe创建检测,比AngularJS 内部去做更有意义,更快。
这个例子中,仅仅替换了 scope.note = value , 如上面的$observe操作所示,任何你想要添加到$watch监听上都应该这样写。
注意:请记住,当遇到 @时,我们谈论的是观察和属性,而不是绑定到父作用域。
表达式构造器: & Link
使用 & 提供一个 表达式解析函数 ,他的上下文是父级作用域。
<body ng-app='PonyDeli' ng-controller='foodCtrl'>
<p note='"You just bought some " + type'></p>
</body>
deli.directive('note', function () {
return {
template: '{{note()}}',
scope: {
note: '&'
}
};
});
下面,我已经在link函数里扼要地实现一个相同的功能 ,这个例子中你看不到 & 。这个比用 @ 要长一点点,因为他 是在属性里解析表达式的,也构建了一个可重用的功能。
deli.directive('note', function ($parse) {
return {
template: '{{note()}}',
scope: {},
link: function (scope, element, attrs) {
var parentGet = $parse(attrs.note);
scope.note = function (locals) {
return parentGet(scope.$parent, locals);
};
}
};
});
真如我们所见,表达式构造器会生成了一个依赖父级scope的方法。你可以随时执行他,甚至可以监测到输出的变化。这个方法在父级scope应该作为只读的查询对待。这样在两种情况下非常有用,当你需要监听父级scope的变化时,这种情况下你应该在表达式函数 note()上设置一个监听, 本质上就像上面的例子。
另一种情况是, 当你需要访问父级scope方法时会派上用场。假设父级scope有一个方法用来更新一个 table,而你的本地 scope用来显示一个table的行。 如果按钮在子scope里,那么通过使用 & 绑定和使用父级scope的刷新方法是很有用的。这仅仅是个人的例子 —— 你或许更喜欢用事件来处理这类事情, 甚至用某种方式构造你的程序来避免一些复杂的事。
双向数据绑定:= Link
使用 = 设置 本地scope与父级scope间的双向数据绑定。
<body ng-app='PonyDeli' ng-controller='foodCtrl'>
<button countable='clicks'></button>
<span>Got {{clicks}} clicks!</span>
</body>
deli.directive('countable', function () {
return {
template:
'' +
'Click me {{remaining}} more times! ({{count}})' +
'',
replace: true,
scope: {
count: '=countable'
},
link: function (scope, element, attrs) {
scope.remaining = 10;
element.bind('click', function () {
scope.remaining--;
scope.count++;
scope.$apply();
});
}
};
});
双向数据绑定比 & 或者 @ 更复杂一点。
deli.directive('countable', function ($parse) {
return {
template:
'' +
'Click me {{remaining}} more times! ({{count}})' +
'',
replace: true,
scope: {},
link: function (scope, element, attrs) {
// you're definitely better off just using '&'
var compare;
var parentGet = $parse(attrs.countable);
if (parentGet.literal) {
compare = angular.equals;
} else {
compare = function(a,b) { return a === b; };
}
var parentSet = parentGet.assign; // or throw
var lastValue = scope.count = parentGet(scope.$parent);
scope.$watch(function () {
var value = parentGet(scope.$parent);
if (!compare(value, scope.count)) {
if (!compare(value, lastValue)) {
scope.count = value;
} else {
parentSet(scope.$parent, value = scope.count);
}
}
return lastValue = value;
}, null, parentGet.literal);
// I told you!
scope.remaining = 10;
element.bind('click', function