壹 ❀ 引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生命周期 这篇博客,对你必定有所帮助。