【JS-8】
angular js自定義指令 directive 如何使用?為什么要使用封裝的自定義指令?
小課堂【武漢分院第130期】
分享人:徐恒
目錄
1.背景介紹
2.知識剖析
3.常見問題
4.解決方案
5.編碼實戰(zhàn)
6.擴(kuò)展思考
7.參考文獻(xiàn)
8.更多討論
一.背景介紹
指令定義
對于指令,可以把它簡單的理解成在特定DOM元素上運(yùn)行的函數(shù),指令可以擴(kuò)展這個元素
的功能。
例如, ng-click 可以讓一個元素能夠監(jiān)聽 click 事件,并在接收到事件的時候執(zhí)行AngularJS表
達(dá)式。正是指令使得AngularJS這個框架變得強(qiáng)大,并且正如所見,我們可以自己創(chuàng)造新的指令。
AngularJS應(yīng)用的模塊中有很多方法可以使用,其中 directive() 這個方法是用來定義指令的:
angular.module('myApp', [])
.directive('myDirective', function ($timeout, UserDefinedService) {
// 指令定義放在這里
});
directive() 方法可以接受兩個參數(shù):
1.? name (字符串)
指令的名字,用來在視圖中引用特定的指令。
2.? factory_function (函數(shù))
這個函數(shù)返回一個對象,其中定義了指令的全部行為。 $compile 服務(wù)利用這個方法返回的對
象,在DOM調(diào)用指令時來構(gòu)造指令的行為。
angular.application('myApp', [])
.directive('myDirective', function() {
// 一個指令定義對象
return {
// 通過設(shè)置項來定義指令,在這里進(jìn)行覆寫
};
});
我們也可以返回一個函數(shù)代替對象來定義指令,但是像上面的例子一樣,通過對象來定義是
最佳的方式。當(dāng)返回一個函數(shù)時,這個函數(shù)通常被稱作鏈接傳遞(postLink)函數(shù),利用它我們
可以定義指令的鏈接(link)功能。由于返回函數(shù)而不是對象會限制定義指令時的自由度,因此
只在構(gòu)造簡單的指令時才比較有用。
當(dāng)AngularJS啟動應(yīng)用時,它會把第一個參數(shù)當(dāng)作一個字符串,并以此字符串為名來注冊第
二個參數(shù)返回的對象。AngularJS編譯器會解析主HTML的DOM中的元素、屬性、注釋和CSS類名
中使用了這個名字的地方,并在這些地方引用對應(yīng)的指令。當(dāng)它找到某個已知的指令時,就會在
頁面中插入指令所對應(yīng)的DOM元素。
tip:為了避免與未來的HTML標(biāo)準(zhǔn)沖突,給自定義的指令加入前綴來代表自定義的
命名空間。AngularJS本身已經(jīng)使用了 ng- 前綴,所以可以選擇除此以外的名字。
在例子中我們使用 my- 前綴(比如 my-derictive )。
指令的工廠函數(shù)只會在編譯器第一次匹配到這個指令時調(diào)用一次。和 controller 函數(shù)類似,
我們通過 $injetor.invoke 來調(diào)用指令的工廠函數(shù)。
當(dāng)AngularJS在DOM中遇到具名的指令時,會去匹配已經(jīng)注冊過的指令,并通過名字在注冊
過的對象中查找。此時,就開始了一個指令的生命周期,指令的生命周期開始于 $compile 方法并
結(jié)束于 link 方法,后面的內(nèi)容中我們會介紹這個過程。
下面,來看看定義一個指令時可以使用的全部設(shè)置選項。
tip:一個JavaScript對象由鍵和值組成。當(dāng)一個給定鍵的值被設(shè)置為一個字符串、布
爾值、數(shù)字、數(shù)組或?qū)ο髸r,我們把這個鍵稱為屬性。當(dāng)把鍵設(shè)置為函數(shù)時,
我們把它叫做方法。
可能的選項如下所示,每個鍵的值說明了可以將這個屬性設(shè)置為何種類型或者什么樣的
函數(shù):
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: // 返回一個對象或連接函數(shù),如下所示:
function(tElement, tAttrs, transclude) {
return {
pre: function(scope, iElement, iAttrs, controller) { ... },
post: function(scope, iElement, iAttrs, controller) { ... }
}
// 或者
return function postLink(...) { ... }
}
};
});
restrict (字符串)
restrict 是一個可選的參數(shù)。它告訴AngularJS這個指令在DOM中可以何種形式被聲明。默
認(rèn)AngularJS認(rèn)為 restrict 的值是 A ,即以屬性的形式來進(jìn)行聲明。
可選值如下:
E(元素)A(屬性,默認(rèn)值)
C(類名)
M(注釋)<--directive:my-directive expression-->這些選項可以單獨使用,也可以混合在一起使用:angular.module('myDirective', function(){? ? return {? ? restrict: 'EA' // 輸入元素或?qū)傩? ? };});上面的配置可以同時用屬性或注釋的方式來聲明指令:<-- 作為一個屬性 -->
<-- 或者作為一個元素 -->
屬性是用來聲明指令最常用的方式,因為它能在包括老版本的IE瀏覽器在內(nèi)的所有瀏覽器中
正常工作,并且不需要在文檔頭部注冊新的標(biāo)簽。
盡量避免用注釋方式來聲明屬性。這種方式最初被用來聲明由多個標(biāo)簽組成的
指令。這種方法在某些情況下特別有用,比如在 table 元素內(nèi)使用 ng-repeat
指 令 , 但 在 AngularJS 1.2 中 ng-repeat 可 以 通 過 ng-repeat-start 和
ng-repeat-end 來更優(yōu)雅地滿足這個需求,注釋模式就沒有什么用武之地了。
如果你對此很好奇,可以通過Chrome開發(fā)者工具的 element 標(biāo)簽觀察一下使用
ng-repeat 時被隱式添加的注釋。
元素方式還是屬性方式
在頁面中通過元素方式創(chuàng)建新的指令可以將一些功能封裝在元素內(nèi)部。例如,如果我們想要
做一個時鐘(忽略對老版本IE瀏覽器的支持),可以創(chuàng)建一個 clock 指令,然后在DOM中用如下
代碼來聲明:
這樣做可以告訴指令的使用者,這里會完整包含應(yīng)用的某一部分內(nèi)容。這個時鐘并不是對一
個既有時鐘的修飾或擴(kuò)展,而是一個全新的單元。盡管這里也可以使用屬性形式聲明指令
(AngularJS并不在意),但我們選擇了元素形式,因為這樣可以更明確地表達(dá)意圖。
用屬性形式來給一個已經(jīng)存在的元素添加數(shù)據(jù)或行為。以時鐘為例,假設(shè)我們更喜歡模擬時鐘:
如何進(jìn)行選擇,通常取決于定義的指令是否包含某個組件的核心行為,或者用額外的行為、
狀態(tài)或者其他內(nèi)容(比如模擬時鐘)對某個核心組件進(jìn)行修飾或擴(kuò)展。
使用何種指令聲明格式的指導(dǎo)原則是能夠準(zhǔn)確表達(dá)每一段代碼的意圖,創(chuàng)造易于理解和分享
的清晰代碼。
另外一個重要的標(biāo)準(zhǔn),是根據(jù)指令是否創(chuàng)建、繼承或?qū)⒆约簭乃鶎俚沫h(huán)境中隔離出去進(jìn)行判
斷。指令的父子關(guān)系對其組成和重用性起著至關(guān)重要的作用,會有額外的內(nèi)容來更加深入地討論
指令的作用域。
priority優(yōu)先級(數(shù)值型)
優(yōu)先級參數(shù)可以被設(shè)置為一個數(shù)值。大多數(shù)指令會忽略這個參數(shù),使用默認(rèn)值0,但也有些
場景設(shè)置高優(yōu)先級是非常重要甚至是必須的。例如, ngRepeat 將這個參數(shù)設(shè)置為1000,這樣就可
以保證在同一元素上,它總是在其他指令之前被調(diào)用。
如果一個元素上具有兩個優(yōu)先級相同的指令,聲明在前面的那個會被優(yōu)先調(diào)用。如果其中一
個的優(yōu)先級更高,則不管聲明的順序如何都會被優(yōu)先調(diào)用:具有更高優(yōu)先級的指令總是優(yōu)先運(yùn)行。
ngRepeat 是所有內(nèi)置指令中優(yōu)先級最高的,它總是在其他指令之前運(yùn)行。這樣
設(shè)置主要考慮的是性能。在討論編譯參數(shù)時會更詳細(xì)介紹性能相關(guān)的內(nèi)容。
terminal (布爾型)
terminal 是一個布爾型參數(shù),可以被設(shè)置為 true 或 false 。
這個參數(shù)用來告訴AngularJS停止運(yùn)行當(dāng)前元素上比本指令優(yōu)先級低的指令。但同當(dāng)前指令
優(yōu)先級相同的指令還是會被執(zhí)行。
如果元素上某個指令設(shè)置了 terminal 參數(shù)并具有較高的優(yōu)先級,就不要再用其他低優(yōu)先級的
指令對其進(jìn)行修飾了,因為不會被調(diào)用。但是具有相同優(yōu)先級的指令還是會被繼續(xù)調(diào)用。
使用了 terminal 參數(shù)的例子是 ngView 和 ngIf 。 ngIf 的優(yōu)先級略高于 ngView ,如果 ngIf 的表
達(dá)式值為 true , ngView 就可以被正常執(zhí)行,但如果 ngIf 表達(dá)式的值為 false ,由于 ngView 的優(yōu)先
級較低就不會被執(zhí)行。
template (字符串或函數(shù))
template 參數(shù)是可選的,必須被設(shè)置為以下兩種形式之一:
?? 一段HTML文本;
?? 一個可以接受兩個參數(shù)的函數(shù),參數(shù)為 tElement 和 tAttrs ,并返回一個代表模板的字符
串。 tElement 和 tAttrs 中的 t 代表 template ,是相對于 instance 的。在討論鏈接和編譯
設(shè)置時會詳細(xì)介紹,模板元素或?qū)傩耘c實例元素或?qū)傩灾g的區(qū)別。
AngularJS會同處理HTML一樣處理模板字符串。模板中可以通過大括號標(biāo)記來訪問作用域,
例如 {{ expression }} 。
如果模板字符串中含有多個DOM元素,或者只由一個單獨的文本節(jié)點構(gòu)成,那它必須被包
含在一個父元素內(nèi)。換句話說,必須存在一個根DOM元素:
template: '\
<-- single root element -->\Click me\
When using two elements, wrap them in a parent element
\
\
另外,注意每一行末尾的反斜線,這樣AngularJS才能正確解析多行字符串。在實際生產(chǎn)中,
更好的選擇是使用 templateUrl 參數(shù)引用外部模板,因為多行文本閱讀和維護(hù)起來都是一場噩夢。
模板字符串和 templateURL 中最需要了解的重要功能,是它們?nèi)绾瓮饔糜蜴溄悠饋怼?/p>
templateUrl (字符串或函數(shù))
templateUrl 是可選的參數(shù),可以是以下類型:
?? 一個代表外部HTML文件路徑的字符串;
?? 一個可以接受兩個參數(shù)的函數(shù),參數(shù)為 tElement 和 tAttrs ,并返回一個外部HTML文件
路徑的字符串。
無論哪種方式,模板的URL都將通過AngularJS內(nèi)置的安全層,特別是 $getTrusted
ResourceUrl ,這樣可以保護(hù)模板不會被不信任的源加載。
默認(rèn)情況下,調(diào)用指令時會在后臺通過Ajax來請求HTML模板文件。有兩件事情需要知道。
?? 在本地開發(fā)時,需要在后臺運(yùn)行一個本地服務(wù)器,用以從文件系統(tǒng)加載HTML模板,否則
會導(dǎo)致Cross Origin Request Script(CORS)錯誤。
?? 模板加載是異步的,意味著編譯和鏈接要暫停,等待模板加載完成。
通過Ajax異步加載大量的模板將嚴(yán)重拖慢一個客戶端應(yīng)用的速度。為了避免延遲,可以在部
署應(yīng)用之前對HTML模板進(jìn)行緩存。在大多數(shù)場景下緩存都是一個非常好的選擇,因為AngularJS
通過減少請求數(shù)量提升了性能。
模板加載后,AngularJS會將它默認(rèn)緩存到 $templateCache 服務(wù)中。在實際生產(chǎn)中,可以提前將模板緩存到一個定義模板的JavaScript文件中,這樣就不需要通過XHR來加載模板了。
replace (布爾型)
replace 是一個可選參數(shù),如果設(shè)置了這個參數(shù),值必須為 true ,因為默認(rèn)值為 false 。默
認(rèn)值意味著模板會被當(dāng)作子元素插入到調(diào)用此指令的元素內(nèi)部:
調(diào)用指令之后的結(jié)果如下(這是默認(rèn) replace 為 false 時的情況):
如果 replace 被設(shè)置為了 true :
指令作用域
為了完全理解指令定義對象中剩下的參數(shù),需要先介紹指令作用域是如何工作的。
$rootScope 這個特殊的對象會在DOM中聲明 ng-app 時被創(chuàng)建:
Inside Div Two
上面的代碼中,我們在應(yīng)用的根作用域中設(shè)置了三個屬性: someProperty 、 siblingProperty
和 aThirdProperty 。
從這里開始,DOM中每個指令調(diào)用時都可能會:
?? 直接調(diào)用相同的作用域?qū)ο螅?/p>
?? 從當(dāng)前作用域?qū)ο罄^承一個新的作用域?qū)ο螅?/p>
?? 創(chuàng)建一個同當(dāng)前作用域相隔離的作用域?qū)ο蟆?/p>
上面的列子展示的是第一種情況。前兩個 div 是兄弟元素,可以通過 get 和 set 訪問 $rootScope 。
第二個 div 內(nèi)部的 div 同樣可以通過 get 和 set 訪問相同的根作用域。
指令嵌套并不一定意味著需要改變它的作用域。默認(rèn)情況下,子指令會被付予訪問父DOM
元素對應(yīng)的作用域的能力,這樣做的原因可以通過介紹指令的 scope 參數(shù)來理解, scope 參數(shù)默
認(rèn)是 false 。
scope 參數(shù)(布爾型或?qū)ο螅?/p>
scope 參數(shù)是可選的,可以被設(shè)置為 true 或一個對象。默認(rèn)值是 false 。
當(dāng) scope 設(shè)置為 true 時,會從父作用域繼承并創(chuàng)建一個新的作用域?qū)ο蟆?/p>
如果一個元素上有多個指令使用了隔離作用域,其中只有一個可以生效。只有指令模板中的
根元素可以獲得一個新的作用域。因此,對于這些對象來說 scope 默認(rèn)被設(shè)置為 true 。
內(nèi)置指令 ng-controller 的作用,就是從父級作用域繼承并創(chuàng)建一個新的子作用域。它會創(chuàng)
建一個新的從父作用域繼承而來的子作用域。
用這些新內(nèi)容更新一下前面的例子:
看例子scope1
Inside Div Two: {{ aThirdProperty }}
Inside Div Three: {{ aThirdProperty }}
Inside Div Four: {{ aThirdProperty }}
如果直接運(yùn)行這段代碼會報錯,因為沒有在JavaScript中定義所需的控制器,下面就來定義這
個控制器:
angular.module('myApp', [])
.controller('SomeController', function($scope) {
// 可以留空,但需要被定義
})
刷新頁面,會發(fā)現(xiàn)第二個 div 中由于 {{ aTgirdProperty }} 未定義,因此什么都沒有輸出。
第三個 div 顯示了設(shè)置在繼承來的作用域中的 data for a 3rd property ,如圖所示。
為了進(jìn)一步證明作用域的繼承機(jī)制是向下而非向上進(jìn)行的,下面再看另外一個例子,展示的
是 {{ aThirdProperty }} 從父作用域繼承而來:
Inside Div Two: {{ aThirdProperty }}
Inside Div Three: {{ aThirdProperty }}
Inside Div Four: {{ aThirdProperty }}
在JavaScript中加入 SecondController 的定義:
angular.module('myApp', [])
.controller('SomeController', function($scope) {
// 可以留空,但需要被定義
})
.controller('SecondController', function($scope) {
// 同樣可以留空
})
如果要創(chuàng)建一個能夠從外部原型繼承作用域的指令,將 scope 屬性設(shè)置為 true :
angular.module('myApp', [])
.directive('myDirective', function() {
return {
restrict: 'A',
scope: true
};
});
下面用指令來改變DOM的作用域:
Inside Div Two: {{ aThirdProperty }}
Inside Div Three: {{ aThirdProperty }}
Inside Div Four: {{ aThirdProperty }}
Outside myDirective: {{ myProperty }}
Inside myDirective: {{ myProperty }}
下面介紹最后一個和作用域相關(guān)的難題:隔離作用域。
隔離作用域
隔離作用域可能是 scope 屬性三個選項中最難理解的一個,但也是最強(qiáng)大的。隔離作用域的
概念是以面向?qū)ο缶幊虨榛A(chǔ)的。AngularJS指令的作用域中可以看到如Small Talk語言和SOLID
原則的影子。
具有隔離作用域的指令最主要的使用場景是創(chuàng)建可復(fù)用的組件,組件可以在未知上下文中使
用,并且可以避免污染所處的外部作用域或不經(jīng)意地污染內(nèi)部作用域。
創(chuàng)建具有隔離作用域的指令需要將 scope 屬性設(shè)置為一個空對象 {} 。如果這樣做了,指令的
模板就無法訪問外部作用域了:
Outside myDirective: {{ myProperty }}
Inside myDirective: {{ myProperty }}
angular.module('myApp', [])? ? .controller('MainController', function($scope) {? ? })? ? .directive('myDirective', function() {? ? ? ? return {? ? ? ? ? ? restrict: 'A',? ? ? ? ? ? scope: {},? ? ? ? ? ? priority: 100,? ? ? ? ? ? template: '
Inside myDirective {{ myProperty }}
'? ? };});
注意,這里為 myDirective 設(shè)置了一個高優(yōu)先級。由于 ngInit 指令會以非零的優(yōu)先
級運(yùn)行,這個例子將會優(yōu)先運(yùn)行 ngInit 指令,然后才是我們定義的指定,并且這個
myProperty 在 $scope 對象中是有效的。
看例子scope3.1
示例代碼的效果與將 scope 設(shè)置為 true 幾乎是相同的。下面看一下使用繼承作用域的指令的
例子,對比一下二者:
Surrounding scope: {{ myProperty }}
JavaScript代碼:
angular.module('myApp', [])
.directive('myDirective', 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
};
});
看例子scope3.2
理解了最重要的關(guān)于作用域的概念后,就可以將隔離作用域中的屬性同外部世界進(jìn)行綁定,
使得隔離作用域可以和外部進(jìn)行交互。
綁定策略
使用無數(shù)據(jù)的隔離作用域并不常見。AngularJS提供了幾種方法能夠?qū)⒅噶顑?nèi)部的隔離作用
域,同指令外部的作用域進(jìn)行數(shù)據(jù)綁定。
為了讓新的指令作用域可以訪問當(dāng)前本地作用域中的變量,需要使用下面三種別名中的一種。
本地作用域?qū)傩裕菏褂?@ 符號將本地作用域同DOM屬性的值進(jìn)行綁定。指令內(nèi)部作用域可以
使用外部作用域的變量:
@ (or @attr)
現(xiàn)在,可以在指令中使用綁定的字符串了。
雙向綁定:通過 = 可以將本地作用域上的屬性同父級作用域上的屬性進(jìn)行雙向的數(shù)據(jù)綁定。
就像普通的數(shù)據(jù)綁定一樣,本地屬性會反映出父數(shù)據(jù)模型中所發(fā)生的改變。
= (or =attr)
父級作用域綁定 通過 & 符號可以對父級作用域進(jìn)行綁定,以便在其中運(yùn)行函數(shù)。意味著對這
個值進(jìn)行設(shè)置時會生成一個指向父級作用域的包裝函數(shù)。
要使調(diào)用帶有一個參數(shù)的父方法,我們需要傳遞一個對象,這個對象的鍵是參數(shù)的名稱,值
是要傳遞給參數(shù)的內(nèi)容。
& (or &attr)
例如,假設(shè)我們在開發(fā)一個電子郵件客戶端,并且要創(chuàng)建一個電子郵件的文本輸入框:
這里有一個數(shù)據(jù)模型( ng-model ),一個函數(shù)( sendMail() )和一個字符串( from-name )。
在指令中做如下設(shè)置以訪問這些內(nèi)容:
scope: {
ngModel: '=', // 將ngModel同指定對象綁定
onSend: '&', // 將引用傳遞給這個方法
fromName: '@' // 儲存與fromName相關(guān)聯(lián)的字符串
}
transclude
transclude 是一個可選的參數(shù)。如果設(shè)置了,其值必須為 true ,它的默認(rèn)值是 false 。
嵌入有時被認(rèn)為是一個高級主題,但某些情況下它與我們剛剛學(xué)習(xí)過的作用域之間會有非常
好的配合。使用嵌入也會很好地擴(kuò)充我們的工具集,特別是在創(chuàng)建可以在團(tuán)隊、項目、AngularJS
社區(qū)之間共享的HTML代碼片段時。
嵌入通常用來創(chuàng)建可復(fù)用的組件,典型的例子是模態(tài)對話框或?qū)Ш綑凇?/p>
我們可以將整個模板,包括其中的指令通過嵌入全部傳入一個指令中。這樣做可以將任意內(nèi)
容和作用域傳遞給指令。 transclude 參數(shù)就是用來實現(xiàn)這個目的的,指令的內(nèi)部可以訪問外部
指令的作用域,并且模板也可以訪問外部的作用域?qū)ο蟆?/p>
為了將作用域傳遞進(jìn)去, scope 參數(shù)的值必須通過 {} 或 true 設(shè)置成隔離作用域。如果沒有設(shè)
置 scope 參數(shù),那么指令內(nèi)部的作用域?qū)⒈辉O(shè)置為傳入模板的作用域。
只有當(dāng)你希望創(chuàng)建一個可以包含任意內(nèi)容的指令時,才使用 transclude: true 。
嵌入允許指令的使用者方便地提供自己的HTML模板,其中可以包含獨特的狀態(tài)和行為,并
對指令的各方面進(jìn)行自定義。
下面一起來實現(xiàn)個小例子,創(chuàng)建一個可以被自定義的可復(fù)用指令。
我們來創(chuàng)建一個可以復(fù)用的側(cè)邊欄,同WordPress博客的側(cè)邊欄很相似。我們希望可以保持
CSS樣式的一致性,同時又希望可以在復(fù)用時盡量少寫HTML代碼。
如果不想讓指令內(nèi)部的內(nèi)容被模板替換,可以設(shè)置這個值為true。一般情況下需要和ngTransclude指令一起使用。
看例子scope4
例如,假設(shè)我們想創(chuàng)建一個包括標(biāo)題和少量HTML內(nèi)容的側(cè)邊欄,如下所示:
First link
Second link
為這個側(cè)邊欄創(chuàng)建一個簡單的指令,并將 transclude 參數(shù)設(shè)置為 true :
angular.module('myApp', [])? ? .directive('sidebox', function() {? ? ? ? return {? ? ? ? ? ? restrict: 'EA',? ? ? ? ? ? scope: {? ? ? ? ? ? ? ? title: '@'? ? ? ? ? ? },? ? ? ? ? ? transclude: true,? ? ? ? ? ? template: '
\
\
{{ title }}
\\\
\
'? ? ? ? };? ? });
這段代碼告訴AngularJS編譯器,將它從DOM元素中獲取的內(nèi)容放到它發(fā)現(xiàn) ng-transclude
指令的地方。
借助 transclusion ,我們可以將指令復(fù)用到第二個元素上,而無須擔(dān)心樣式和布局的一致
性問題。
例如,下面的代碼會產(chǎn)生兩個樣式完全一致的側(cè)邊欄,效果所示。
First link
Second link
GraphicsAngularJSD3Front-endStartup
如果指令使用了 transclude 參數(shù),那么在控制器(下面馬上會介紹)中就無法正常監(jiān)聽數(shù)
據(jù)模型的變化了。這就是最佳實踐總是建議在鏈接函數(shù)里使用 $watch 服務(wù)的原因。
controller (字符串或函數(shù))
controller 參數(shù)可以是一個字符串或一個函數(shù)。當(dāng)設(shè)置為字符串時,會以字符串的值為名字,
來查找注冊在應(yīng)用中的控制器的構(gòu)造函數(shù):
angular.module('myApp', [])
.directive('myDirective', function() {
restrict: 'A', // 始終需要
controller: 'SomeController'
})
// 應(yīng)用中其他的地方,可以是同一個文件或被index.html包含的另一個文件
angular.module('myApp')
.controller('SomeController', function($scope, $element, $attrs, $transclude) {
// 控制器邏輯放在這里
});
可以在指令內(nèi)部通過匿名構(gòu)造函數(shù)的方式來定義一個內(nèi)聯(lián)的控制器:
angular.module('myApp',[])
.directive('myDirective', function() {
restrict: 'A',
controller:
function($scope, $element, $attrs, $transclude) {
// 控制器邏輯放在這里
}
});
我們可以將任意可以被注入的AngularJS服務(wù)傳遞給控制器。例如,如果我們想要將 $log 服
務(wù)傳入控制器,只需簡單地將它注入到控制器中,便可以在指令中使用它了。
控制器中也有一些特殊的服務(wù)可以被注入到指令當(dāng)中。這些服務(wù)有:
1.? $scope
與指令元素相關(guān)聯(lián)的當(dāng)前作用域。
2.? $element
當(dāng)前指令對應(yīng)的元素。
3.? $attrs
由當(dāng)前元素的屬性組成的對象。例如,下面的元素:
具有如下的屬性對象:{id: "aDiv",class: "box"}
4.? $transclude
嵌入鏈接函數(shù)會與對應(yīng)的嵌入作用域進(jìn)行預(yù)綁定。
transclude 鏈接函數(shù)是實際被執(zhí)行用來克隆元素和操作DOM的函數(shù)。
在控制器內(nèi)部操作DOM是和AngularJS風(fēng)格相悖的做法,但通過鏈接函數(shù)就可以
實現(xiàn)這個需求。僅在 compile 參數(shù)中使用 transcludeFn 是推薦的做法。
例如,我們想要通過指令來添加一個超鏈接標(biāo)簽。可以在控制器內(nèi)的 $transclude 函數(shù)中實
現(xiàn),如下所示:
指令的控制器和 link 函數(shù)可以進(jìn)行互換??刂破髦饕怯脕硖峁┛稍谥噶铋g復(fù)用的行為,但鏈接函數(shù)只能在當(dāng)前內(nèi)部指令中定義行為,且無法在指令間復(fù)用。
link 函數(shù)可以將指令互相隔離開來,而 controller 則定義可復(fù)用的行為。
由于指令可以 require 其他指令所使用的控制器,因此控制器常被用來放置在多個指令間共享的動作。
如果我們希望將當(dāng)前指令的API暴露給其他指令使用,可以使用 controller 參數(shù),否則可以使用 link 來構(gòu)造當(dāng)前指令元素的功能性。如果我們使用了 scope.$watch() 或者想要與DOM元素
做實時的交互,使用鏈接會是更好的選擇。
技術(shù)上講, $scope 會在DOM元素被實際渲染之前傳入到控制器中。在某些情況下,例如使用了嵌入,控制器中的作用域所反映的作用域可能與我們所期望的不一樣,這種情況下, $scope對象無法保證可以被正常更新。
當(dāng)想要同當(dāng)前屏幕上的作用域交互時,可以使用被傳入到 link 函數(shù)中的 scope
參數(shù)。
controllerAs (字符串)
controllerAs 參數(shù)用來設(shè)置控制器的別名,可以以此為名來發(fā)布控制器,并且作用域可以訪
問 controllerAs 。這樣就可以在視圖中引用控制器,甚至無需注入 $scope 。
例如,創(chuàng)建一個 MainController ,然后不要注入 $scope ,如下所示:
angular.module('myApp')
.controller('MainController', function() {
this.name = "Ari";
});
現(xiàn)在,在HTML中無需引用作用域就可以使用 MainController 。
這個參數(shù)看起來好像沒什么大用,但它給了我們可以在路由和指令中創(chuàng)建匿名控制器的強(qiáng)大
能力。這種能力可以將動態(tài)的對象創(chuàng)建成為控制器,并且這個對象是隔離的、易于測試的。
例如,可以在指令中創(chuàng)建匿名控制器,如下所示:
require (字符串或數(shù)組)
require 參數(shù)可以被設(shè)置為字符串或數(shù)組,字符串代表另外一個指令的名字。 require 會將控
制器注入到其值所指定的指令中,并作為當(dāng)前指令的鏈接函數(shù)的第四個參數(shù)。
字符串或數(shù)組元素的值是會在當(dāng)前指令的作用域中使用的指令名稱。
scope 會影響指令作用域的指向,是一個隔離作用域,一個有依賴的作用域或者完全沒有作
用域。在任何情況下,AngularJS編譯器在查找子控制器時都會參考當(dāng)前指令的模板。
如果不使用 ^ 前綴,指令只會在自身的元素上查找控制器。
//...
restrict: 'EA',
require: 'ngModel'
//...
指令定義只會查找定義在指令作當(dāng)前用域中的 ng-model="" 。
require 參數(shù)的值可以用下面的前綴進(jìn)行修飾,這會改變查找控制器時的行為:
?
如果在當(dāng)前指令中沒有找到所需要的控制器,會將 null 作為傳給 link 函數(shù)的第四個參數(shù)。
^
如果添加了 ^ 前綴,指令會在上游的指令鏈中查找 require 參數(shù)所指定的控制器。
?^
將前面兩個選項的行為組合起來,我們可選擇地加載需要的指令并在父指令鏈中進(jìn)行查找。
沒有前綴
如果沒有前綴,指令將會在自身所提供的控制器中進(jìn)行查找,如果沒有找到任何控制器(或
具有指定名字的指令)就拋出一個錯誤。
AngularJS 的生命周期
在AngularJS應(yīng)用起動前,它們以HTML文本的形式保存在文本編輯器中。應(yīng)用啟動后會進(jìn)行
編譯和鏈接,作用域會同HTML進(jìn)行綁定,應(yīng)用可以對用戶在HTML中進(jìn)行的操作進(jìn)行實時響應(yīng)。
這個神奇的效果是如何發(fā)生的?創(chuàng)建高效率的應(yīng)用需要了解什么?
在這個過程中總共有兩個主要階段。
編譯階段
第一個階段是編譯階段。在編譯階段,AngularJS會遍歷整個HTML文檔并根據(jù)JavaScript中
的指令定義來處理頁面上聲明的指令。
每一個指令的模板中都可能含有另外一個指令,另外一個指令也可能會有自己的模板。當(dāng)
AngularJS調(diào)用HTML文檔根部的指令時,會遍歷其中所有的模板,模板中也可能包含帶有模板的
指令。
模板樹可能又大又深,但有一點需要注意,盡管元素可以被多個指令所支持或
修飾,這些指令本身的模板中也可以包含其他指令,但只有屬于最高優(yōu)先級指
令的模板會被解析并添加到模板樹中。這里有一個建議,就是將包含模板的指
令和添加行為的指令分離開來。如果一個元素已經(jīng)有一個含有模板的指令了,
永遠(yuǎn)不要對其用另一個指令進(jìn)行修飾。只有具有最高優(yōu)先級的指令中的模板會
被編譯。
一旦對指令和其中的子模板進(jìn)行遍歷或編譯,編譯后的模板會返回一個叫做模板函數(shù)的函
數(shù)。我們有機(jī)會在指令的模板函數(shù)被返回前,對編譯后的DOM樹進(jìn)行修改。
在這個時間點DOM樹還沒有進(jìn)行數(shù)據(jù)綁定,意味著如果此時對DOM樹進(jìn)行操作只會有很少
的性能開銷?;诖它c, ng-repeat 和 ng-transclude 等內(nèi)置指令會在這個時候,也就是還未與
任何作用域數(shù)據(jù)進(jìn)行綁定時對DOM進(jìn)行操作。
以 ng-repeat 為例,它會遍歷指定的數(shù)組或?qū)ο螅跀?shù)據(jù)綁定之前構(gòu)建出對應(yīng)的DOM結(jié)構(gòu)。
如果我們用 ng-repeat 來創(chuàng)建無序列表,其中的每一個 li 都會被 ng-click 指令所修飾,這
個過程會使得性能比手動創(chuàng)建列表要快得多,尤其是列表中含有上百個元素時。
與克隆 li 元素,再將其與數(shù)據(jù)進(jìn)行鏈接,然后對每個元素都循環(huán)進(jìn)行此操作的過程不同,
我們僅需要先將無需列表構(gòu)造出來,然后將新的DOM(編譯后的DOM)傳遞給指令生命周期中
的下一個階段,即鏈接階段。
一個指令的表現(xiàn)一旦編譯完成,馬上就可以通過編譯函數(shù)對其進(jìn)行訪問,編譯函數(shù)的簽名包
含有訪問指令聲明所在的元素( tElemente )及該元素其他屬性( tAttrs )的方法。這個編譯函
數(shù)返回前面提到的模板函數(shù),其中含有完整的解析樹。
這里的重點是,由于每個指令都可以有自己的模板和編譯函數(shù),每個模板返回的也都是自己
的模板函數(shù)。鏈條頂部的指令會將內(nèi)部子指令的模板合并在一起成為一個模板函數(shù)并返回,但在
樹的內(nèi)部,只能通過模板函數(shù)訪問其所處的分支。
最后,模板函數(shù)被傳遞給編譯后的DOM樹中每個指令定義規(guī)則中指定的鏈接函數(shù),
compile (對象或函數(shù))
compile 選項可以返回一個對象或函數(shù)。
理解 compile 和 link 選項是AngularJS中需要深入討論的高級話題之一,對于了解AngularJS
究竟是如何工作的至關(guān)重要。
compile 選項本身并不會被頻繁使用,但是 link 函數(shù)則會被經(jīng)常使用。本質(zhì)上,當(dāng)我們設(shè)置
了 link 選項,實際上是創(chuàng)建了一個 postLink() 鏈接函數(shù),以便 compile() 函數(shù)可以定義鏈接函數(shù)。
通常情況下,如果設(shè)置了 compile 函數(shù),說明我們希望在指令和實時數(shù)據(jù)被放到DOM中之前
進(jìn)行DOM操作,在這個函數(shù)中進(jìn)行諸如添加和刪除節(jié)點等DOM操作是安全的。
compile 和 link 選項是互斥的。如果同時設(shè)置了這兩個選項,那么會把 compile
所返回的函數(shù)當(dāng)作鏈接函數(shù),而 link 選項本身則會被忽略。
// ...compile: function(tEle, tAttrs, transcludeFn) {var tplEl = angular.element('
' +'
' +'
');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) {// 連接函數(shù)};}//...
如果模板被克隆過,那么模板實例和鏈接實例可能是不同的對象。因此在編譯函數(shù)內(nèi)部,我
們只能轉(zhuǎn)換那些可以被安全操作的克隆DOM節(jié)點。不要進(jìn)行DOM事件監(jiān)聽器的注冊:這個操作
應(yīng)該在鏈接函數(shù)中完成。
編譯函數(shù)負(fù)責(zé)對模板DOM進(jìn)行轉(zhuǎn)換。
鏈接函數(shù)負(fù)責(zé)將作用域和DOM進(jìn)行鏈接。在作用域同DOM鏈接之前可以手動操作DOM。在
實踐中,編寫自定義指令時這種操作是非常罕見的,但有幾個內(nèi)置指令提供了這樣的功能。了解
這個流程對于理解AngularJS真正的工作方式很有幫助。
鏈接
用 link 函數(shù)創(chuàng)建可以操作DOM的指令。
鏈接函數(shù)是可選的。如果定義了編譯函數(shù),它會返回鏈接函數(shù),因此當(dāng)兩個函數(shù)都定義了時,
編譯函數(shù)會重載鏈接函數(shù)。如果我們的指令很簡單,并且不需要額外的設(shè)置,可以從工廠函數(shù)(回
調(diào)函數(shù))返回一個函數(shù)來代替對象。如果這樣做了,這個函數(shù)就是鏈接函數(shù)。
下面兩種定義指令的方式在功能上是完全一樣的:
angular.module('myApp', [])
.directive('myDirective', function() {
return {
pre: function(tElement, tAttrs, transclude) {
// 在子元素被鏈接之前執(zhí)行
// 在這里進(jìn)行Don轉(zhuǎn)換不安全
// 之后調(diào)用'lihk'h函數(shù)將無法定位要鏈接的元素
},
post: function(scope, iElement, iAttrs, controller) {
// 在子元素被鏈接之后執(zhí)行
// 如果在這里省略掉編譯選項
//在這里執(zhí)行DOM轉(zhuǎn)換和鏈接函數(shù)一樣安全嗎
}
};
});
angular.module('myApp', [])
.directive('myDirective', function() {
return {
link: function(scope, ele, attrs) {
return {
pre: function(tElement, tAttrs, transclude) {
// 在子元素被鏈接之前執(zhí)行
// 在這里進(jìn)行Don轉(zhuǎn)換不安全
// 之后調(diào)用'lihk'h函數(shù)將無法定位要鏈接的元素
},
post: function(scope, iElement, iAttrs, controller) {
// 在子元素被鏈接之后執(zhí)行
// 如果在這里省略掉編譯選項
//在這里執(zhí)行DOM轉(zhuǎn)換和鏈接函數(shù)一樣安全嗎
}
}
}
});
當(dāng)定義了編譯函數(shù)來取代鏈接函數(shù)時,鏈接函數(shù)是我們能提供給返回對象的第二個方法,也
就是 postLink 函數(shù)。本質(zhì)上講,這個事實正說明了鏈接函數(shù)的作用。它會在模板編譯并同作用域
進(jìn)行鏈接后被調(diào)用,因此它負(fù)責(zé)設(shè)置事件監(jiān)聽器,監(jiān)視數(shù)據(jù)變化和實時的操作DOM。
link 函數(shù)對綁定了實時數(shù)據(jù)的DOM具有控制能力,因此需要考慮性能問題。在選擇是用編譯函數(shù)還是鏈接函數(shù)實現(xiàn)功能時,將性能影響考慮進(jìn)去。
鏈接函數(shù)的簽名如下:
link: function(scope, element, attrs) {
// 在這里操作DOM
}
如果指令定義中有 require 選項,函數(shù)簽名中會有第四個參數(shù),代表控制器或者所依賴的指
令的控制器。
// require 'SomeController',
link: function(scope, element, attrs, SomeController) {
// 在這里操作DOM,可以訪問required指定的控制器
}
如果 require 選項提供了一個指令數(shù)組,第四個參數(shù)會是一個由每個指令所對應(yīng)的控制器組
成的數(shù)組。
下面看一下鏈接函數(shù)中的參數(shù):
scope
指令用來在其內(nèi)部注冊監(jiān)聽器的作用域。
iElement
iElement 參數(shù)代表實例元素,指使用此指令的元素。在 postLink 函數(shù)中我們應(yīng)該只操作此
元素的子元素,因為子元素已經(jīng)被鏈接過了。
iAttrs
iAttrs 參數(shù)代表實例屬性,是一個由定義在元素上的屬性組成的標(biāo)準(zhǔn)化列表,可以在所有指
令的鏈接函數(shù)間共享。會以JavaScript對象的形式進(jìn)行傳遞。
controller
controller 參數(shù)指向 require 選項定義的控制器。如果沒有設(shè)置 require 選項,那么
controller 參數(shù)的值為 undefined 。
控制器在所有的指令間共享,因此指令可以將控制器當(dāng)作通信通道(公共API)。如果設(shè)置了
多個 require ,那么這個參數(shù)會是一個由控制器實例組成的數(shù)組,而不只是一個單獨的控制器。
二.知識剖析
三.常見問題
四.解決方案
五.代碼實戰(zhàn)
六.拓展思考
為什么要使用封裝的自定義指令?
封裝的意思是說對象數(shù)據(jù)和操作該數(shù)據(jù)的指令都是對象自身的一部分,封裝能夠?qū)崿F(xiàn)盡可能對外部世界隱藏數(shù)據(jù)。封裝簡單的說能屏蔽方法的復(fù)雜性,比如只要知道方法的參數(shù)類型就可以使用方法,再說降低模塊之間的耦合性,就是模塊之間的聯(lián)系,使之相互獨立,能提高系統(tǒng)的健壯性
七.參考文獻(xiàn)
學(xué)習(xí)AngularJs:Directive指令用法(完整版)
八.更多討論
為什么要使用封裝的自定義指令?
鳴謝
感謝大家觀看
BY : 徐恒
1.link函數(shù)和controller區(qū)別
2.ng-app和ng-controller聯(lián)系
3.replace和transclude區(qū)別
------------------------------------------------------------------------------------------------------------------------
技能樹.IT修真院
“我們相信人人都可以成為一個工程師,現(xiàn)在開始,找個師兄,帶你入門,掌控自己學(xué)習(xí)的節(jié)奏,學(xué)習(xí)的路上不再迷茫”。
這里是技能樹.IT修真院,成千上萬的師兄在這里找到了自己的學(xué)習(xí)路線,學(xué)習(xí)透明化,成長可見化,師兄1對1免費指導(dǎo)??靵砼c我一起學(xué)習(xí)吧 !