壹 ❀ 引html
在angularjs開發中,指令的使用是無處無在的,咱們習慣使用指令來拓展HTML;那麼如何理解指令呢,你能夠把它理解成在DOM元素上運行的函數,它能夠幫助咱們拓展DOM元素的功能。好比最經常使用ng-click可讓一個元素能監聽click事件,這裏你可能就有疑問了,一樣都是監聽爲何不直接使用click事件呢,angular提供的事件指令與傳統指令有什麼區別?咱們來看一個例子:angularjs
<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提供的事件指令與傳統事件來經過按鈕點擊,修改文本的內容,效果以下:npm
很奇怪,只有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等等,如何使用這裏就不一一介紹了,本文主要圍繞自定義指令展開,閱讀完本文,一塊兒來實現屬於本身的指令吧。服務器
貳 ❀ 建立一個簡單的指令app
在angularjs還未更新出component時,咱們通常會使用directive開發自定義指令或者組件,也正由於directive功能的強大,致使指令與組件概念含糊不清,因此纔有後面用於作組件的component,固然對於component咱們另起一篇文章再說。dom
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>' } });
頁面效果:post
咱們已經實現了一個很是簡單的指令(組件),如今咱們能夠在頁面中盡情複用它。假設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(註釋)四個值,若是不使用此屬性則默認值爲E,如下四種表現相同:
<!-- 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。
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生命週期 這篇博客,對你必定有所幫助。