原創性聲明:本文完全爲筆者原創,請尊重筆者勞動力。轉載務必註明原文地址。
指令詳解
一個指令的定義應當是如下這個樣子:
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() { //此時這個函數叫做“鏈接傳遞函數”
}
});
但是一般地,都採用返回對象的形式,這樣指令的定義可以更豐富。返回函數的情況只有在定義非常簡單的指令時纔可能會使用。
下面看指令中第二個參數——函數——返回對象的詳細配置!!!
code:
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
中將存在值,爲什麼呢?難道隔離作用域只是隔離了模板與外界作用域,而當前指令應用的DOM元素中用其他指令定義的變量仍然可以在外面被訪問?
爲此,在此進行對比演示:
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對應的作用域),因此可以訪問外部變量。
scope爲對象時的綁定策略
scope爲Object時,像上面的空對象的情況肯定是不適用的。angularJS提供了幾種方法,可以將指令內部的隔離作用域和指令外部的作用域進行數據綁定。
@
(or@attr
) : 本地作用域屬性。使用@
符號將本地作用域與DOM屬性的值進行綁定,指令內部作用域可以訪問並使用外部作用域的變量,常用於DOM中屬性值爲固定參數。=
(or=attr
) : 雙向綁定。使用=
符號將本地作用域中的屬性和DOM屬性的值進行雙向綁定,那麼當DOM屬性值隨時改變時,指令中的值也會改變,同時反過來也是一樣的。常用於DOM中對應屬性值是動態的,如ng-model。&
(or&attr
) : 父級作用域綁定。主要用於運行其中的函數,也就是說這個值在指令中設置後,會生成一個指向父級作用域的包裝函數。如果要調用帶有參數的父方法,則需要在DOM指令屬性值的函數形參中傳入一個對象,對象的鍵是參數名,值是參數值。
如下面的例子:
code:
<input type="text" ng-model="to"/>
<!-- 調用指令 -->
<div scope-example ng-model="to" on-send="sendMail(email)"
from-name="[email protected]">
</div>
自定義指令 scope-example
中如果要訪問此處的數據(模型to
、 函數方法sendMail(email)
以及字符串"[email protected]"
)的話,就必須配置scope爲對象,如下:
scope: {
ngModel: '=', // 將ngModel同指定對象綁定
onSend: '&', // 將引用傳遞給這個方法
fromName: '@' // 儲存與fromName相關聯的字符串
}
注意指令中本地變量的命名規則(駝峯法)。如果不想用駝峯法,想自定義隨便取名,也可以指定要綁定的外部DOM變量,如下:
scope: {
a: '=ngModel', // 將ngModel同指定對象綁定
b: '&onSend', // 將引用傳遞給這個方法
c: '@fromName' // 儲存與fromName相關聯的字符串
}
那麼a
的值就是 to
的值, b
的值就是 sendMail(email)
方法的引用,c
的值就是[email protected]
。
這就是三種綁定策略的不同以及各自的適用場景!
8. transclude
可選參數。Boolean值,默認值爲false。定義爲true時,它會將整個DOM嵌入到指令內部定義的模板中,包括DOM中的其他指令。
只有當你希望創建一個可以包含任意內容的指令時, 才使用transclude: true。
爲了將作用域傳遞進去, 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:由指令元素的屬性和屬性值所組成的對象。如:‘<divid="aDiv"class="box"></div>‘的 attrs值爲:{id: "aDiv", class: "box"}
- $transclude: transclude鏈接函數是實際被執行用來克隆元素和操作DOM的函數。
指令的控制器和link函數可以進行互換。控制器主要是用來提供可在指令間複用的行爲,但
鏈接函數只能在當前內部指令中定義行爲,且無法在指令間複用。由於指令可以require其他指令所使用的控制器,因此控制器常被用來放置在多個指令間共享的動作。
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。
- ^ 如果在指令鏈的父指令中沒有找到需要的控制器,則會進一步往指令鏈上游尋找需要的控制器。
- ?^ 教程的解釋是:我們可選擇地加載需要的指令並在父指令鏈中進行查找。
- 沒有前綴的情況: 如果沒有前綴,則指令就會在自身所提供的控制器中進行查找,如果沒有找到控制器(或者沒有找到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的值,其中也存在很多問題。
補充: 更多內容,可以在簡書上關注我(東方一號藍)