原創性聲明:本文完全為筆者原創,請尊重筆者勞動力。轉載務必注明原文地址。
指令詳解
一個指令的定義應當是如下這個樣子:
code:
angular.module('myApp', [])
.directive('myDirective', function (UserDefinedService) {
// 指令定義放在這里
});
其中,fun中的注入參數為angular自帶或用戶定義的服務,需要在指令內部中調用。分析其結構:
-
angular.module('myApp', [])
:是聲明整個應用對象的。 -
.directive('myDirective', fun(){})
:directive方法接受兩個參數:字符串和函數。
字符串myDirective
是用以在視圖中引用特定的指令。而函數則返回一個對象,這個對象中定義了指令的全部行為,$compile服務利用這個方法返回的對象,在DOM調用指令時來構造指令的行為。即:
angular.module('myApp', [])
.directive('myDirective', function (UserDefinedService) {
return {
}
});
當然除了返回一個對象,其實也可以返回一個函數:
angular.module('myApp', [])
.directive('myDirective', function (UserDefinedService) {
return function() { //此時這個函數叫做“鏈接傳遞函數”
}
});
但是一般地,都采用返回對象的形式,這樣指令的定義可以更豐富。返回函數的情況只有在定義非常簡單的指令時才可能會使用。
下面看指令中第二個參數——函數——返回對象的詳細配置!!!
<i style="color:red">code:</i>
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(...){...}
}
}
});
下面分別對各個配置項進行詳細說明:
1. restrict
- 非必須
- 可選值:'EACM'
EACM指的是指令在DOM視圖中的應用形式,如下:
E: (元素)
<my-directive></my-directive>
A: (屬性,默認值)
<div my-directive="expression"></div>
C: (類名)
<div class="my-directive:expression;"></div>
M: (注釋)
<--directive:my-directive expression-->
這些值可以單獨使用,也可以混合使用。其中A是推薦的方式,因為它的兼容性更好,也更容易擴充。
2. priority
優先級。它的作用是聲明指令的優先級,當多個指令用在同一個DOM元素上時,哪個會先執行呢?就取決于這個參數。如果兩個指令的優先級一樣,那么聲明在前的會先被調用并執行。
例如 ng-repeat
的優先級就是1000,因此,它總是比其他指令更優先執行。
3. terminal
Boolean值,它的作用是告訴angularJS是否停止運行當前元素上比本指令優先級更低的指令,但與當前指令優先級同級的指令仍然會被執行的。如下面的例子:
<div>
<p my-terminal-test1 my-terminal-test2></p>
</div>
angular.module('angularLearningApp')
.directive('myTerminalTest1', function() {
return {
restrict: 'A',
priority: 1,
template: '百度',
link: function (scope, element, attrs) {
console.log("myTerminalTest1");
}
}
})
.directive('myTerminalTest2', function() {
return {
restrict: 'A',
priority: 2,
terminal: false, //現將terminal設置為false
link: function(scope, element, attrs) {
console.log("myTerminalTest2");
element[0].textContent += '谷歌';
}
}
});
顯然div中的內容是 百度谷歌
,如果將terminal設置為true,則顯示的結果為 谷歌
,這是因為 myTerminalTest1
指令的優先級低于 myTerminalTest2
,而terminal為true因此,低于它的指令將不被執行。
4. template
template有兩種形式:
- String //模板字符串
- function(tElement, tAttrs){ ...; return templateStr;//返回模板字符串}
需要注意的是: template返回的模板中,DOM結構中必須存在一個根節點。在實際的開發中,更常使用的是templateUrl,因為可以避免字符串拼接,那是可讀性、維護性很差的方式。另外,template中最為重要的東西是controller與本指令中template變量的數據傳遞。
5. templateUrl
同樣有兩種形式:
- String // 模板html文件路徑
- function(tElement, tAttrs){...; return templatePath;//返回模板html路徑}
默認情況下,調用指令會在后臺通過ajax請求html模板文件,有兩個特別需要注意的:
- 在本地開發時,需要在后臺運行一個本地服務器,用以從文件系統加載HTML模板,否則會導致Cross Origin Request Script(CORS)錯誤。
- 模板加載是異步的,意味著編譯和鏈接要暫停,等待模板加載完成。
通過Ajax異步加載大量的模板將嚴重拖慢一個客戶端應用的速度。為了避免延遲,可以在部
署應用之前對HTML模板進行緩存。在大多數場景下緩存都是一個非常好的選擇,因為AngularJS
通過減少請求數量提升了性能。更多關于緩存的內容請查看第28章。
模板加載后, AngularJS會將它默認緩存到$templateCache服務中。在實際生產中,可以提
前將模板緩存到一個定義模板的JavaScript文件中,這樣就不需要通過XHR來加載模板了。更多內容請查看第34章。
6. replace
默認值是false,表示模板的內容將會被插入到視圖中應用指令元素的內部。如果設置為true,則表示替代,即插入到視圖中時,應用指令的html元素將被刪除,取而代之的是html模板。
7. scope
可選參數
- boolean 默認是false,即該指令并不會創建新的作用域,改指令內部或外部的作用域是一樣的。當為true時,會從父作用域繼承并創建一個新的作用域對象,即該指令內部和外部并不是在一個作用域內。
- Object :設置此屬性也被稱為“隔離作用域”。
scope為Boolean時
code:
<div 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>
angular.module('myApp', [])
.controller('SomeController', function($scope) {
// 可以留空,但需要被定義
})
.controller('SecondController', function($scope) {
// 同樣可以留空
});
angular.module('myApp', [])
.directive('myDirective', function() {
return {
restrict: 'A',
//scope: true
};
});
首先,將把 scope:true
注釋掉即設置scope為默認的false。此時的結果是:
view:
<div 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-scope-test ng-init="myProperty = 'wow, this is cool'">
Inside myDirective: {{ myProperty }}<div>
</div>
</div>
</div>
顯而易見,Outside myDirective和Inside myDirecitve都將是有值的,即使myProperty的值是在指令標簽中定義的,但因為指令中的配置項scope為false,該指令并沒有產生一個新的作用域,因此,在這個指令標簽內部和外部都是在一個作用域下,即:SecondController對應的作用域下,所以值都是有的。
但如果將 scope:true
釋放掉,那么該指令就會產生一個獨立作用域,此作用域繼承父作用域,但是在該作用域中定義的變量myProperty,就無法在該指令外部調用了,因此,結果就是:
Outside myDirective:
Inside myDirective: wow, this is cool
這就是scope為boolean值時的作用。
scope為Object時————隔離作用域
設置scope配置屬性值為Object時,指令的模板就無法訪問外部作用域了。也因此,不受外部作用域變量的影響,因此,隔離作用域常用來創建可復用的指令組件。
code:
<div ng-controller='SomeController2'>
Outside myDirective: { { myProperty2 } }
<div my-directive-scope-test2 ng-init="myProperty2 = 'wow, this is cool!'">
Inside myDirective: { { myProperty2 } }
</div>
</div>
angular.module('myApp', [])
.controller('SomeController2', function($scope) {
})
.directive('myDirectiveScopeTest2', function() {
return {
restrict: 'A',
scope: {}, //對象
priority: 100,
template: '<div>Inside myDirective: { { myProperty2 } }</div>'
};
});
view:
<div ng-controller='SomeController2'>
Outside myDirective: {{ myProperty2 }}
<div my-directive-scope-obj-test ng-init="myProperty2 = 'wow, this is cool!'">
Inside myDirective: {{ myProperty2 }}
</div>
</div>
Inside myDirective
中將不會出現值,沒錯,因為scope隔離了模板與外界作用域。
但是Outside myDirective
中將存在值,為什么呢?<b>難道隔離作用域只是隔離了模板與外界作用域,而當前指令應用的DOM元素中用其他指令定義的變量仍然可以在外面被訪問?</b>
為此,在此進行對比演示:
code:
<div ng-controller="ScopeValueCompareController"
ng-init="myProperty='wow,this is so cool'">
Surrounding scope: {{ myProperty }}
<div my-inherit-scope-directive></div>
<div my-directive></div>
</div>
angular.module('myApp', [])
.controller('ScopeValueCompareController', function($scope) {
})
.directive('myDirective3', function() {
return {
restrict: 'A',
template: 'Inside myDirective, isolate scope: {{ myProperty }}',
scope: {}
};
})
.directive('myInheritScopeDirective', function() {
return {
restrict: 'A',
template: 'Inside myDirective, isolate scope: {{ myProperty }}',
scope: true
};
});
view:
<div ng-controller="ScopeValueCompareController"
ng-init="myProperty3='wow,this is so cool'">
Surrounding scope: {{ myProperty3 }}
<div my-directive3></div>
<div my-inherit-scope-directive></div>
</div>
scope為{}時,指令內模板作用域被隔離開,所以是沒有值得。scope為true時,指令內新建了一個作用域,但他繼承父級作用域(這里是ScopeValueCompareController對應的作用域),因此可以訪問外部變量。
<b style="color:red">scope為對象時的綁定策略</b>
scope為Object時,像上面的空對象的情況肯定是不適用的。angularJS提供了幾種方法,可以將指令內部的隔離作用域和指令外部的作用域進行數據綁定。
-
@
(or@attr
) : 本地作用域屬性。使用@
符號將本地作用域與DOM屬性的值進行綁定,指令內部作用域可以訪問并使用外部作用域的變量,<b>常用于DOM中屬性值為固定參數</b>。 -
=
(or=attr
) : 雙向綁定。使用=
符號將本地作用域中的屬性和DOM屬性的值進行雙向綁定,那么當DOM屬性值隨時改變時,指令中的值也會改變,同時反過來也是一樣的。<b>常用于DOM中對應屬性值是動態的,如ng-model</b>。 -
&
(or&attr
) : 父級作用域綁定。<b>主要用于運行其中的函數,也就是說這個值在指令中設置后,會生成一個指向父級作用域的包裝函數。如果要調用帶有參數的父方法,則需要在DOM指令屬性值的函數形參中傳入一個對象,對象的鍵是參數名,值是參數值。</b>
如下面的例子:
code:
<input type="text" ng-model="to"/>
<!-- 調用指令 -->
<div scope-example ng-model="to" on-send="sendMail(email)"
from-name="ari@fullstack.io">
</div>
自定義指令 scope-example
中如果要訪問此處的數據(模型to
、 函數方法sendMail(email)
以及字符串"ari@fullstack.io"
)的話,就必須配置scope為對象,如下:
scope: {
ngModel: '=', // 將ngModel同指定對象綁定
onSend: '&', // 將引用傳遞給這個方法
fromName: '@' // 儲存與fromName相關聯的字符串
}
注意指令中本地變量的命名規則(駝峰法)。如果不想用駝峰法,想自定義隨便取名,也可以指定要綁定的外部DOM變量,如下:
scope: {
a: '=ngModel', // 將ngModel同指定對象綁定
b: '&onSend', // 將引用傳遞給這個方法
c: '@fromName' // 儲存與fromName相關聯的字符串
}
那么a
的值就是 to
的值, b
的值就是 sendMail(email)
方法的引用,c
的值就是ari@fullstack.io
。
這就是三種綁定策略的不同以及各自的適用場景!
8. transclude
可選參數。Boolean值,默認值為false。定義為true時,它會將整個DOM嵌入到指令內部定義的模板中,包括DOM中的其他指令。
<b>只有當你希望創建一個可以包含任意內容的指令時, 才使用transclude: true。</b>
為了將作用域傳遞進去, scope參數的值必須通過{}或true設置成隔離作用域。如果沒有設
置scope參數,那么指令內部的作用域將被設置為傳入模板的作用域。嵌入允許指令的使用者方便地提供自己的HTML模板,其中可以包含獨特的狀態和行為,并對指令的各方面進行自定義。看一個簡單的例子,一個包括標題和少量html內容的側邊欄。
code:
<div sidebox title="Links">
<ul>
<li>First link</li>
<li>Second link</li>
</ul>
</div>
為這個側邊欄創建一個簡單的指令,設置transclude為true:
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>'
};
});
view:
<div sidebox title="Links">
<ul>
<li>First link</li>
<li>Second link</li>
</ul>
</div>
此時,在瀏覽器中生成的DOM結構為:
<div sidebox title="Links"> <!-- a:原來DOM中應用sidebox指令的標簽 -->
<div class="sidebox"> <!-- c:sidebox指令中定義的模板 -->
<div class="content"> <!-- c -->
<h2 class="header">{ { title } }</h2> <!-- c -->
<span class="content" ng-transclude> <!-- c -->
<ul> <!-- b 的子標簽內容 -->
<li>First link</li> <!-- b -->
<li>Second link</li> <!-- b -->
</ul>
</span> <!-- c -->
</div> <!-- c -->
</div> <!-- c -->
</div> <!-- a -->
顯然,transclude設為true后,angularJS將該指令應用的DOM元素(a)的內部所有元素(b)都嵌入到了指令模板中聲明ng-transclude的元素(c)內,并全部套入到a中,再被渲染出來。
transclude和ng-transclude是聯合使用的。
9.controller
controller可以是字符串或函數:
- String : 以該字符串為值去整個項目中查找同名注冊的controller
- function: 匿名構造函數定義的內聯controller
code:
angular.module('myApp',[])
.directive('myDirective', function() {
restrict: 'A',
controller:
function($scope, $element, $attrs, $transclude) {
// 控制器邏輯放在這里
}
});
我們可以將任意可以被注入的AngularJS服務傳遞給控制器。例如,如果我們想要將$log服
務傳入控制器,只需簡單地將它注入到控制器中,便可以在指令中使用它了。上面的例子,有:
- $scope :與指令元素相關聯的當前作用域
- $element: 指令元素,即當前指令應用的DOM元素
- $attrs: 由指令元素的屬性和屬性值所組成的對象。如:
<div id="aDiv"class="box"></div>
的$attrs值為:{id: "aDiv", class: "box"}
- $transclude: transclude鏈接函數是實際被執行用來克隆元素和操作DOM的函數。
指令的控制器和link函數可以進行互換。控制器主要是用來提供可在指令間復用的行為,但
鏈接函數只能在當前內部指令中定義行為,且無法在指令間復用。<b>由于指令可以require其他指令所使用的控制器,因此控制器常被用來放置在多個指令間共享的動作。</b>
10.controllerAs
字符串。這個參數用以設置控制器的別名,以此名發布控制器,并且作用域可以訪問controllerAs。
code:
angular.module('myApp')
.directive('myDirective', function() {
return {
restrict: 'A',
template: '<h4>{{ myController.msg }}</h4>',
controllerAs: 'myController',
controller: function() {
this.msg = "Hello World"
}
}
});
11. require
字符串或數組。可選值。當值為字符串時,它應當是另一個指令的名字。require是將其值所指定的指令中的控制器注入到當前指令中,并作為當前指令的link函數的第四個參數。而這個被注入進來的控制器(位于指令鏈接的父指令中)會首先被當前指令查找,查找當然是根據require的值決定的,不過給這個值予以不同的前綴,會影響它的查找行為:
- ? 尋找require值對應的指令中的控制器,如果在指令鏈的父指令(即require的值所對應的指令)中沒有找到需要的控制器,則當前指令中的link函數的第四個參數將會是null。
- ^ 如果在指令鏈的父指令中沒有找到需要的控制器,則會進一步往指令鏈上游尋找需要的控制器。
- ?^ <b>教程的解釋是:</b>我們可選擇地加載需要的指令并在父指令鏈中進行查找。
- 沒有前綴的情況: 如果沒有前綴,則指令就會在自身所提供的控制器中進行查找,如果沒有找到控制器(或者沒有找到require的值所對應的指令),就會拋出一個錯誤。
code:
<hello>
<div>hello</div>
<beautiful good>
beautiful
</beautiful>
</hello>
angular.module("myApp", [])
.directive("hello",function(){
return {
restrict : "E",
controller : function($scope){
$scope.name = "張三";
this.information = {
name : $scope.name,
age : 25,
job : "程序員"
}
},
link : function(scope){
}
}
})
.directive("beautiful",function(){
return {
restrict : "E",
require : "?good",
controller : function(){
this.name = "beautiful";
},
link : function (scope,element,attrs,good) {
console.log(good.name)
}
}
}).
directive("good",function(){
return {
restrict : "A",
require : "?^hello",
controller : function(){
this.name = "good";
},
link : function (scope,element,attrs,hello) {
console.log(hello.information)
}
}
});
12.compile
該屬性的屬性值是一個函數內部返回一個對象,或者函數。理解compile和link函數是angularJS需要討論的高級話題之一,對于了解angularJS是如何工作的是至關重要的。
本質上,當我們設置了link選項,實際上是創建了一個postLink()鏈接函數,以便compile()函數可以定義函數。
通常情況下,如果我們設置了complie()函數,說明我們希望在指令和實時數據被放到DOM中之前對DOM進行操作,在這個函數中進行諸如,添加和刪除節點等DOM操作是安全的。
特別注意:compile函數和link函數是互斥的。即,如果同時設置了這兩個配置項,那么angularJS會選擇compile函數的返回函數作為link函數,而本身link函數的配置會被完全忽略。
- 編譯函數compile內部通常用來轉換可以被安全操作的DOM節點,不要對DOM進行事件監聽注冊。
- 鏈接函數link負責將DOM和作用域進行鏈接。
如下一個例子:
compile: function(tEle, tAttrs, transcludeFn) {
var tplEl = angular.element('<div>' +
'<h2></h2>' +
'</div>');
var h2 = tplEl.find('h2');
h2.attr('type', tAttrs.type);
h2.attr('ng-model', tAttrs.ngModel);
h2.val("hello");
tEle.replaceWith(tplEl);
return function(scope, ele, attrs) {
// 連接函數
};
}
13 link函數
link函數用來創建可以操作DOM的屬性。當定義了編譯函數來取代鏈接函數時,鏈接函數是我們能提供給返回對象的第二個方法,也就是postLink函數。本質上講,這個事實正說明了鏈接函數的作用。它會在模板編譯并同作用域進行鏈接后被調用,因此它負責設置事件監聽器,監視數據變化和實時的操作DOM。
鏈接函數一共有四個參數:
- scope : 指令用來在其內部注冊監聽器的作用域
- iElement: 代表實例元素,即使用此指令的元素。在postLink函數中,我們應該只操作這個元素和其子元素,因為這些元素已經被鏈接過了。
- iAttrs: 代表實例屬性,一個由定義在元素上的屬性組成的標準化列表,可以在所有指令的鏈接函數間共享。會以javascript對象的形式進行傳遞。
- controller: 這個參數只有在當前指令存在
require
選項時才會有,否則就是undefined。如果require的值是另一個指令A,那么controller的值就是這個指令A中的controller;如果require的值是另一個單獨的controller,那么當前controller的值就是這個controller;如果require指向多個控制器,那么當前controller就是一個由這些多個控制器組成的數組。
link函數是指令中最為常用的一個配置項。它和controller函數最大的區別就是功能性區分,前者是用以操作DOM,后者用以指令間傳遞。
自定義指令的配置項中complie、link、controller等還有很深的水,需要進一步去探究。關于指令的link中如何訪問到視圖中的ng-model的值,其中也存在很多問題。