壹 ❀ 引
在angularjs開發中,指令的使用是無處無在的,我們習慣使用指令來拓展HTML;那么如何理解指令呢,你可以把它理解成在DOM元素上運行的函數,它可以幫助我們拓展DOM元素的功能。比如最常用ng-click可以讓一個元素能監聽click事件,這里你可能就有疑問了,同樣都是監聽為什么不直接使用click事件呢,angular提供的事件指令與傳統指令有什么區別?我們來看一個例子:
<body ng-controller="myCtrl as vm"> <div class="demo"> <p ng-bind="vm.name"></p> <button ng-click="vm.changeA()" class="col1">buttonA</button> <button class="btnB col2" onclick="a()">buttonB</button> </div> </body>
angular.module('myApp', []) .controller('myCtrl', function () { let vm = this; vm.name = '聽風是風'; //通過angularjs指令綁定事件 vm.changeA = function () { vm.name = 'echo'; }; //使用原生的js綁定方式 let btn = document.querySelector(".btnB"); btn.onclick = function () { vm.name = '時間跳躍'; }; });
我們分別使用angularjs提供的事件指令與傳統事件來通過按鈕點擊,修改文本的內容,效果如下:
很奇怪,只有ng-click成功修改了文本內容,傳統的事件並不能做到這一點,怎么解決呢?其實我們手動添加$apply方法就可以了,代碼如下:
btn.onclick = function () { $scope.$apply(function () { vm.name = '時間跳躍'; }); };
我們從這個例子可以知道,當我們使用angularjs指令時,ng-click除了事件響應還做了臟檢測,當數據發生變化通知視圖重新渲染。准確來說,angular會將執行行為放入到$apply中進行調用,如果數據發生了變化,$apply會通知$digest循環,從而調用所有watcher,從而達到視圖更新的目的,當然這里扯的有點遠了,只是為了說明官方指令與傳統事件的區別。
angularjs官方提供的指令繁多,例如事件類ng-click,ng-change,樣式類ng-class,ng-style等等,如何使用這里就不一一介紹了,本文主要圍繞自定義指令展開,閱讀完本文,一起來實現屬於自己的指令吧。
貳 ❀ 創建一個簡單的指令
在angularjs還未更新出component時,我們一般會使用directive開發自定義指令或者組件,也正因為directive功能的強大,導致指令與組件概念含糊不清,所以才有后面用於做組件的component,當然對於component我們另起一篇文章再說。
directive是直接用於操作dom的函數,它甚至能直接改變dom的結構,我們從一個最簡單的directive開始:
<body ng-controller="MainCtrl as vm"> <echo></echo> </body>
angular.module('myApp', []) .controller('MainCtrl', function () { }) .directive('echo',function(){ return{ restrict:'E', replace:true, template:'<div>你好,我是聽風是風。</div>' } });
頁面效果:
我們已經實現了一個非常簡單的指令(組件),現在我們可以在頁面中盡情復用它。假設template是一個特別復雜的dom結構,通過指令我們就可以省下重復的代碼編寫,聽起來非常棒不是嗎。
<echo></echo> <echo></echo> <echo></echo>
當然angularjs自定義指令其實擁有很多靈活的屬性,用於完成更復雜的功能,一個完整的directive模板結構應該是這樣,屬性看着有點多,沒關系,接下來我們針對屬性一一細說。
angular.module('myApp', []).directive('directiveName', function () { return { restrict: String, priority: Number, terminal: Boolean, template: ' String or Template Function', templateUrl: String, replace: 'Boolean or String', scope: 'Boolean or Object', transclude: Boolean, controller: 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 /rɪˈstrɪkt/ 限制;約束;
restrict表示指令在DOM中能以哪種形式被聲明,是一個可選值,可選值范圍有E(元素)A(屬性)C(類名)M(注釋)四個值,如果不使用此屬性則默認值為EA,以下四種表現相同:
<!-- E --> <echo></echo> <!-- A --> <div echo></div> <!-- C --> <div class="echo"></div> <!-- M --> <!-- directive:echo -->
restrict的值可單個使用或者多個組合使用,比如restrict:'E'即表示只允許使用元素來聲明組件,而restrict:'EACM'則表示你可以使用四種方式的任一一種來聲明組件。
2.priority /praɪˈɒrəti/ 優先權
priority值為數字,表示指令的優先級,若一個DOM上存在多個指令時,優先級高的指令先執行,注意此屬性只在指令作為DOM屬性時起作用,我們來看個例子:
<div echo demo></div>
angular.module('myApp', [])
.controller('MainCtrl', function () {})
.directive('echo', function () {
return {
restrict: 'EACM',
priority: 10,
controller:function(){
console.log('我的優先級是10')
}
}
})
.directive('demo', function () {
return {
restrict: 'EACM',
priority: 20,
controller:function(){
console.log('我的優先級是20')
}
}
})
可以看到優先級更好的指令優先執行,若兩個指令優先級相同時,聲明在前的指令會先執行,ngRepeat的優先級為1000,它是所有內置指令中優先級最高的指令。大多數情況下我們會忽略此屬性,默認即為0;
3.terminal /ˈtɜːmɪnl/
terminal值為布爾值,用於決定優先級低於自己的指令是否還執行,例如上方例子中,我們為demo指令添加terminal:true,可以看到echo指令不會執行:
4.template /ˈtempleɪt/ 模板
template的值是一段HTML文本或一個函數,HTML文本的例子上文已有展示,這里簡單說下值為函數的情況,我們來看個例子:
<div echo name="聽風是風"></div>
angular.module('myApp', []) .controller('MainCtrl', function () {}) .directive('echo', function () { return { restrict: 'EACM', template: function (tElement, tAttrs) { console.log(tElement,tAttrs); return '<div>你好,我是' + tAttrs.name + '</div>' } } })
template函數接受兩個參數,tElement和tAttrs,這里我們分別輸出兩個屬性,可以看到tElement表示正在使用此指令的DOM元素,而tAttrs包含了使用此指令DOM元素上的所有屬性。
所以在上述例子中,我們在DOM上添加了一個name屬性,而在函數中我們通過tAttrs.name訪問了此屬性的值,所以最終DOM解析渲染為如下:
由於templateUrl相對template對於模板的處理更優雅,所以一般不會使用template。
5.templateUrl 模板路徑
相對template直接將模板代碼寫在指令中,templateUrl推薦將模板代碼另起文件保存,而這里保存對文件路徑的引用;當然templateUrl同樣支持函數,用法與template相同就我們來看一個簡單的例子:
angular.module('myApp', []) .controller('MainCtrl', function () {}) .directive('echo', function () { return { restrict: 'EACM', templateUrl: 'template/echo-template.html' } })
特別注意,在使用template與templateUrl的模板文件時,如果你使用了replace:true屬性(后面會介紹),且模板代碼DOM結構有多層,請記住使用一個父級元素包裹你所有DOM結構,否則會報錯,因為angularjs模板只支持擁有一個根元素。
正確:
<div> <span>我是聽風是風</span> <span>好好學習天天向上</span> </div>
錯誤:
<span>我是聽風是風</span> <span>好好學習天天向上</span>
其次,在使用templateUrl時,需要在本地啟動服務器來運行你的angular項目,否則在加載模板時會報錯。如果你不知道怎么搭建本地服務,推薦npm 中的 live-server,使用非常簡單,詳情請百度。
6.replace /rɪˈpleɪs/ 替換
replace值為布爾值,用於決定指令模板是否替換聲明指令的DOM元素,默認為false,我們來看兩個簡單的例子,首先指令作為元素:
<echo></echo>
值為false:
值為true:
可以看到當為true時,echo元素直接被全部替換;我們再來看看指令作為屬性:
<div echo> <span>歡迎來到聽風是風的博客</span> </div>
值為false:
值為true:
可以看到,當指令作為屬性時,replace值為false只替換聲明指令DOM的子元素為模板元素,當值為true時,整個元素都被替換成模板元素,同時還保留了屬性echo。
7.scope [skəʊp] 作用域
scope屬性用於決定指令作用域與父級作用域的關系,可選值有布爾值或者一個對象,默認為false,我們一個個介紹。
當 scope:flase 時,表示指令不創建額外的作用域,默認繼承使用父級作用域,所以指令中能正常使用和修改父級中所有變量和方法,我們來看個簡單的例子:
<body ng-controller="myCtrl"> 我是父:<input type="text" ng-model="num"> <div echo></div> </body>
angular.module('myApp', []) .controller('myCtrl', function ($scope) { let vm = this; $scope.num = 100; }).directive('echo', function () { return { restrict: 'EACM', scope: false, template: '<div>我是子:<input type="text" ng-model="num"><div>', replace: true } })
可以看到指令完全繼承了父作用域,共用了一份數據,不管我們修改父或者子指令,這份數據都將同步改變並影響彼此,這就是繼承不隔離。
當 scope:true 時表示指令創建自己的作用域,但仍然會繼承父作用域,說直白點就是,指令自己有的用自己的,沒有的找父級拿,同一份數據父級能影響指令,但指令卻無法反向影響父級,這就是繼承但隔離。
angular.module('myApp', []) .controller('myCtrl', function ($scope) { let vm = this; $scope.num = 100; $scope.name = 'echo'; }).directive('echo', function () { return { restrict: 'EACM', scope: true, template: '<div>我是子:<input type="text" ng-model="num">我的名字是:{{name}}<div>', replace: true, controller:function ($scope) { $scope.name = '聽風是風'; } } })
可以看到父子作用域都有name屬性,但指令中還是使用了自身的屬性,其次,指令中沒有的num屬性繼承自父級,當修改父級時子會同步改變,但反之父不會改變,最有趣的是一旦修改了子,父級也無法再影響子。
當 scope:{} 時,表示指令創建一個隔離作用域,此時指令作用域不再繼承父作用域,兩邊的數據不再互通:
說到這,你是否會覺得不隔離直接使用父級作用域會更方便,從使用角度來說確實如此。但實際開發中,我們自定義的指令往往會在各種上下文中使用,只有保證指令擁有隔離作用域,不會關心和不影響上下文,這樣才能極大提升指令復用性。
那么問題又來了,如果我指令需要使用父級作用域的數據怎么辦?有隔離自然有解決方案,這就得使用綁定策略了。angularjs中directive的綁定策略分為三種,@,=,和&,一一介紹。
@通常用於傳遞字符串,注意,使用@傳遞過去的一定得是字符串,而且@屬於單向綁定,即父修改能影響指令,但指令修改不會反向影響父,我們來看個例子:
<body ng-controller="myCtrl"> <input type="text" ng-model="data.name"> <echo my-name="{{data}}"></echo> </body>
angular.module('myApp', []) .controller('myCtrl', function ($scope) { $scope.data = 'echo'; }).directive('echo', function () { return { restrict: 'EACM', scope: { myName:"@" }, template: '<div><input type="text" ng-model="myName"><div>', replace: true, } })
注意,我在指令上通過my-name屬性來傳遞這個對象,但在指令scope中我們接受數據時得改為小駝峰myName,其次請留意data兩側加了{{}}包裹,使用@時這是必要的,具體效果如下:
= 用於傳遞各類數據,字符串,對象,數組等等,而且是雙向綁定,即不管修改父還是子,這份數據都會被修改,我們將上方代碼的@改為 = ,同時做小部分調整,具體效果如下:
<body ng-controller="myCtrl"> <input type="text" ng-model="data"> <echo my-name="data"></echo> </body>
angular.module('myApp', []) .controller('myCtrl', function ($scope) { $scope.data = 'echo' }).directive('echo', function () { return { restrict: 'EACM', scope: { myName: "=" }, template: '<div><input type="text" ng-model="myName"><div>', replace: true, } })
請注意,指令上傳遞data時兩邊並未使用{{}}包裹,這與@傳值還是有很大區別。
& 用於傳遞父作用域中聲明的方法,也就是通過&我們可以在指令中直接使用父的方法,我們來看個例子:
<body ng-controller="myCtrl"> <input type="text" ng-model="data"> <echo my-name="sayName(data)"></echo> </body>
angular.module('myApp', []) .controller('myCtrl', function ($scope) { $scope.sayName = function (name) { console.log(name); }; }).directive('echo', function () { return { restrict: 'EACM', scope: { myName: "&" }, template: '<div><button ng-click="myName()">點我</button><div>', replace: true, } })
這有點類似於為指令提供了一個點擊的入口,當點擊指令時實際執行的是父上面的方法,而這個方法本質上不屬於指令,所以我們沒辦法傳遞指令的值給這個方法,上方的例子傳遞的也是父作用域的值。
8.controller [kənˈtrəʊlə(r)] 控制器
我們都知道angular中控制器是很重要的一部分,我們常常在控制器操作數據通過scope作為橋梁以達到更新視圖變化的目的,很明顯指令擁有自己的scope,當然擁有自己的controller控制器也不是什么奇怪的事情。
controller的值可以是一個函數,或者一個字符串,如果是字符串指令會在應用中查找與字符串同名的構造函數作為自己的控制器函數,我們來看一個非常有趣的例子:
<body ng-controller="myCtrl as vm"> <input type="text" ng-model="name"> <div echo></div> </body>
angular.module('myApp', []) .controller('myCtrl', function ($scope) { let vm = this; $scope.name = '聽風是風'; }).directive('echo', function () { return { restrict: 'EACM', scope: {}, template: '<div><input type="text" ng-model="name"><div>', replace: true, controller: 'myCtrl' } })
在上述例子中,我們在父作用域聲明了一個變量name,有趣的是我們並未對指令傳遞name屬性,甚至還為指令添加了隔離作用域,但是因為指令的controller的值使用了與父作用域控制器相同的名字myCtrl,導致指令中也擁有了相同的controller,同樣擁有了自己name屬性,但這兩個name屬性互不干擾,畢竟有隔離作用域的存在。
如果控制器的值是一個函數,那就更簡單了,還是上面的例子我們只是改改controller的值,如下:
angular.module('myApp', []) .controller('myCtrl', function ($scope) { let vm = this; $scope.name = '聽風是風'; }).directive('echo', function () { return { restrict: 'EACM', scope: {}, template: '<div><input type="text" ng-model="name"><div>', replace: true, controller: function ($scope) { $scope.name = 'echo'; } } })
當然指令的controller的形參不止一個scope,一共有$scope,$element,$attrs,$transclude四個,我們一一介紹(指令屬性還真是多...)。
$scope:指令當前的作用域,所有在scope上綁定的屬性方法,在指令中都可以隨意使用,在上面的例子中我們已經有所展示。
$element:使用指令的當前元素,比如上面的例子,因為echo指令是加在div元素上,我們直接輸出$element屬性,可以看到就是div:
$attr:使用指令當前元素上的屬性,還是上面的例子,我們給此div添加一些額外的屬性,同樣輸出它:
<div echo name="echo" age="26"></div>
$transclude:鏈接函數,用於克隆和操作DOM元素,沒錯,通過此方法我們甚至能在controller中操作dom,注意,如果要使用此方法得保證transclude屬性值為true,來看個簡單的例子:
<body ng-controller="myCtrl"> <div attr="www.baidu.com" echo> 點我跳轉百度 </div> </body>
angular.module('myApp', []) .controller('myCtrl', function ($scope) { }).directive('echo', function () { return { restrict: 'EACM', scope: { myName: "&" }, transclude: true,//若想使用$transclude方法請設置為true controller: function ($scope, $element, $attrs, $transclude) { $transclude(function (clone) { var a = angular.element('<a>'); a.attr('href',$attrs.attr);//取得div上的attr屬性並設置給a a.text(clone.text());// 通過clone屬性可以獲取指令嵌入內容,包括文本,元素名等等,已經過JQ封裝,這里獲取文本並添加給a $element.append(a); // 將a添加到指令所在元素內 }) } } })
如果對於angularjs生命周期稍有了解,應該都知道angular會在compile階段編譯dom,在link鏈接階段綁定事件,所以官方一般是推薦在compile階段操作DOM,而非controller內部。
9.transclude
在上文controlle介紹中我們已經知道如果想在controller中使用$transclude方法必須設置transclude為true,這里我們來介紹下此屬性。
transclude的值為布爾值,默認flase,我們知道指令的模板總是會替換掉使用指令DOM的子元素,看個例子回顧下replace屬性:
<body ng-controller="myCtrl"> <div echo> <span>我是聽風</span> </div> </body>
angular.module('myApp', []) .controller('myCtrl', function ($scope) { }).directive('echo', function () { return { restrict: 'EA', template: '<p>我是echo</p>', replace: false, } })
div元素使用了echo指令,因為replace設置為false,所以div元素會被保留,但div的子元素span會被替換為指令模板p元素:
那如果我想保留div的子元素span怎么,這里就可以使用transclude屬性做到這一點,另外transclude通常與ng-transclude指令一起使用,我們再來看一個例子:
<body ng-controller="myCtrl"> <div echo> <span>我是聽風</span> </div> </body>
angular.module('myApp', []) .controller('myCtrl', function ($scope) { }).directive('echo', function () { return { restrict: 'EA', template: '<p>我是echo<span ng-transclude></span></p>', replace: false, transclude:true } })
可以看到原div中的子元素span被成功保留加入到了指令模板中添加了ng-transclude指令的元素中。
10.controllerAs
controllerAs用於設置控制器的別名,我們都知道angularjs在1.2版本之后,對於數據綁定提供了額外一種方式,第一種是綁定在scope上,第二種是使用controller as vm類似的寫法,綁定在this上。我們來看個簡單的例子:
<body ng-controller="myCtrl as vm"> <input type="text" ng-model="name1"> <div>{{name1}}</div> <input type="text" ng-model="vm.name2"> <div>{{vm.name2}}</div> </body>
angular.module('myApp', []) .controller('myCtrl', function ($scope) { $scope.name1 = 'echo'; this.name2 = '聽風是風'; })
可以看到兩種綁定效果完全一致,那么在指令中也有控制器,我們也可以通過this來綁定數據,而controllerAs定義的字段就是我們在模板上訪問數據的前綴:
<body ng-controller="myCtrl"> <div echo></div> </body>
angular.module('myApp', []) .controller('myCtrl', function ($scope) { }).directive('echo', function () { return { restrict: 'EA', template: '<p>{{vm.name}}</p>', controllerAs:'vm', controller:function (){ this.name = '聽風是風!'; } } })
11.require [rɪˈkwaɪə(r)] 需求
對於指令開發,link函數和controller中都可以定義指令需要的屬性或方法,但如果這個屬性或方法只是本指令使用,你可以定義在指令的link函數中,但如果這個屬性方法你想在別的指令中也使用,推薦定義在controller中。
而require屬性就是用來引用其它指令的controller,require的值可以是一個字符串或者一個數組,字符串就是其它指令名字,而數組就是包含多個指令名的數組,我們來看一個簡單的例子:
<body ng-controller="myCtrl"> <div echo1> <div echo2></div> </div> </body>
angular.module('myApp', []) .controller('myCtrl', function ($scope) { }).directive('echo1', function () { return { restrict: 'EA', controller: function ($scope) { this.sayAge = function () { console.log(26); } }, } }).directive('echo2', function () { return { restrict: 'EA', scope:{}, require: '^echo1', link:function (scope, elem, attrs, controller) { controller.sayAge();//26 } } })
上述例子中,我們在指令echo1的控制器中定義了一個sayName方法,注意得綁定this上;而在指令echo2中,我們require了指令echo1,這樣我們就能通過link函數的第四個參數訪問到echo1的控制器中的所有屬性方法(綁在this上的),達到方法復用。
有沒有注意到require的字符串前面有一個 ^ 標志,require的值一共可以以四種前綴來修飾,我們分別解釋:
1.無前綴
如果沒有前綴,指令將會在自身所提供的控制器中進行查找,如果沒有找到任何控制器(或具有指定名字的指令)就拋出一個錯誤。我們來看一個簡單的例子:
angular.module('myApp', []) .controller('myCtrl', function ($scope) { }).directive('echo2', function () { return { restrict: 'EA', scope: {}, require: 'echo2',//require自己 controller: function () { this.sayName = function () { console.log('聽風是風'); } }, link: function (scope, elem, attrs, controller) { controller.sayName() //聽風是風 } } })
這個例子中我們讓指令require自己,從而讓link函數中能訪問自己controller中的方法。
2.^
如果添加了^前綴,指令會在自身以及父級指令鏈中查找require參數所指定的指令的控制器,如果沒找到報錯。
3.?
同樣是在當前指令中找,如果沒找到會將null傳給link函數的第四個參數。與不加前綴的區別是提供null從而不報錯。
4.?^
?與^的組合,從當前找,如果沒找到去上層找,如果沒找到就提供null。
5.^^
Angular 1.5.6版本之后新增,表示跳過自身直接從父級開始找,找不到報錯。
12.link 鏈接函數
我們在前面介紹其它屬性時已經有粗略提及link函數了,在link函數中我們也能像在controller中一樣為模板綁定事件,更新視圖等。看個簡單的例子:
angular.module('myApp', []) .controller('myCtrl', function ($scope) { }).directive('echo2', function () { return { restrict: 'EA', scope: {}, template:'<div ng-click="vm.sayName()">點我輸出{{name}}</div>', controllerAs:'vm', controller: function () { this.sayName = function () { console.log('聽風是風'); } }, link: function (scope, elem, attrs, controller) { scope.name = '聽風是風'; } } })
link函數擁有四個參數,scope表示指令的作用域,在scope上綁定的數據在模板上都能直接訪問使用。elem表示當前使用指令的DOM元素,attrs表示當前使用指令DOM元素上的屬性,這三點與前面介紹指令controller參數一致。第四個參數controller表示指令中require的指令的controller,前面已經有例子展示,注意,如果指令沒有require其它指令,那么第四個參數就是指令自身的作用域,看個例子:
.directive('echo1', function () { return { restrict: 'EACM', replace: true, controller: function ($scope) { $scope.name = 'echo'; this.name1 = 'echo1'; }, link: function (scope, ele, att, ctrl) { console.log(ctrl); console.log(scope.name); // echo console.log(ctrl.name1); // echo1 } } })
那么現在我們知道了,在link里面scope能直接訪問自身controller中scope的屬性,而this上的屬性,同樣能通過第四個參數訪問,前期是沒require其它指令。
指令的控制器controller和link函數可以進行互換。控制器主要是用來提供可在指令間復用的行為,但鏈接函數只能在當前內部指令中定義行為,且無法在指令間復用。簡單點說link函數可以將指令互相隔離開來,而controller則定義可復用的行為。
13.compile 編譯函數
如果你想在指令模板編譯之前操作DOM,那么compile函數將會起作用,但出於安全問題一般不推薦這么做。同樣不推薦在compile中進行DOM方法綁定與數據監聽,這些行為最好都交給link或者controller來完成。
其次compile和link互斥,如果你在一個指令同時使用了compile和link,那么link函數不會執行。
肆 ❀ 總
這篇博客前前后后寫了一個星期,導致文章篇幅有點長,耗時久一方面是知識點確實多,其次是對於指令我也有很多地方需要重新細化理解,這篇文章確實算是系統學習的一個過程。
在文章結尾我只是粗略提及了link與compile函數,對於angularjs的高級用法,理解這兩兄弟由其重要,所以我打算另起一篇文章專門用來介紹link,compile與controller的區別,順帶介紹angularjs的生命周期。
使用指令或組件一定離不開生命周期鈎子函數,關於鈎子函數的介紹,我也會另起一篇文章,這兩篇文章都會在一周內完成,也算是給自己一個小目標。
那么本文就寫到這了。
如果你好奇controller,link,compile有何區別,preLink與postLink又有何不同,以及它們的執行先后感興趣,歡迎閱讀博主 angularjs link compile與controller的區別詳解,了解angular生命周期 這篇博客,對你一定有所幫助。
如果你對於directive的好兄弟component有興趣,可以閱讀博主這篇文章 一篇文章看懂angularjs component組件
如果你對於component與directive使用有所混淆,可以閱讀博主這篇文章 angularjs中directive指令與component組件有什么區別?