美文网首页angular
自定义指令详解

自定义指令详解

作者: 东方一号蓝 | 来源:发表于2016-09-27 11:08 被阅读350次

    原创性声明:本文完全为笔者原创,请尊重笔者劳动力。转载务必注明原文地址。

    指令详解

    一个指令的定义应当是如下这个样子:

    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() { //此时这个函数叫做“链接传递函数”
                
            }
    });
    

    但是一般地,都采用返回对象的形式,这样指令的定义可以更丰富。返回函数的情况只有在定义非常简单的指令时才可能会使用。

    下面看指令中第二个参数——函数——返回对象的详细配置!!!

    <i style="color:red">code:</i>

    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模板文件,有两个特别需要注意的:

    1. 在本地开发时,需要在后台运行一个本地服务器,用以从文件系统加载HTML模板,否则会导致Cross Origin Request Script(CORS)错误。
    2. 模板加载是异步的,意味着编译和链接要暂停,等待模板加载完成。

    通过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中将存在值,为什么呢?<b>难道隔离作用域只是隔离了模板与外界作用域,而当前指令应用的DOM元素中用其他指令定义的变量仍然可以在外面被访问?</b>

    为此,在此进行对比演示:

    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对应的作用域),因此可以访问外部变量。

    <b style="color:red">scope为对象时的绑定策略</b>

    scope为Object时,像上面的空对象的情况肯定是不适用的。angularJS提供了几种方法,可以将指令内部的隔离作用域和指令外部的作用域进行数据绑定。

    1. @ (or @attr) : 本地作用域属性。使用 @ 符号将本地作用域与DOM属性的值进行绑定,指令内部作用域可以访问并使用外部作用域的变量,<b>常用于DOM中属性值为固定参数</b>。
    2. = (or =attr) : 双向绑定。使用=符号将本地作用域中的属性和DOM属性的值进行双向绑定,那么当DOM属性值随时改变时,指令中的值也会改变,同时反过来也是一样的。<b>常用于DOM中对应属性值是动态的,如ng-model</b>。
    3. & (or &attr) : 父级作用域绑定。<b>主要用于运行其中的函数,也就是说这个值在指令中设置后,会生成一个指向父级作用域的包装函数。如果要调用带有参数的父方法,则需要在DOM指令属性值的函数形参中传入一个对象,对象的键是参数名,值是参数值。</b>

    如下面的例子:

    code:

    <input type="text" ng-model="to"/>
    <!-- 调用指令 -->
    <div scope-example ng-model="to"  on-send="sendMail(email)"
        from-name="ari@fullstack.io">
    </div>
    

    自定义指令 scope-example 中如果要访问此处的数据(模型to、 函数方法sendMail(email)以及字符串"ari@fullstack.io")的话,就必须配置scope为对象,如下:

    scope: {
        ngModel: '=', // 将ngModel同指定对象绑定
        onSend: '&', // 将引用传递给这个方法
        fromName: '@' // 储存与fromName相关联的字符串
    }
    

    注意指令中本地变量的命名规则(驼峰法)。如果不想用驼峰法,想自定义随便取名,也可以指定要绑定的外部DOM变量,如下:

    scope: {
        a: '=ngModel', // 将ngModel同指定对象绑定
        b: '&onSend', // 将引用传递给这个方法
        c: '@fromName' // 储存与fromName相关联的字符串
    }
    

    那么a 的值就是 to的值, b的值就是 sendMail(email) 方法的引用,c 的值就是ari@fullstack.io

    这就是三种绑定策略的不同以及各自的适用场景!

    8. transclude

    可选参数。Boolean值,默认值为false。定义为true时,它会将整个DOM嵌入到指令内部定义的模板中,包括DOM中的其他指令。

    <b>只有当你希望创建一个可以包含任意内容的指令时, 才使用transclude: true。</b>

    为了将作用域传递进去, 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可以是字符串或函数:

    1. String : 以该字符串为值去整个项目中查找同名注册的controller
    2. function: 匿名构造函数定义的内联controller

    code:

    angular.module('myApp',[])
        .directive('myDirective', function() {
            restrict: 'A',
            controller:
                function($scope, $element, $attrs, $transclude) {
                    // 控制器逻辑放在这里
                }
        });
    

    我们可以将任意可以被注入的AngularJS服务传递给控制器。例如,如果我们想要将$log服
    务传入控制器,只需简单地将它注入到控制器中,便可以在指令中使用它了。上面的例子,有:

    • $scope :与指令元素相关联的当前作用域
    • $element: 指令元素,即当前指令应用的DOM元素
    • $attrs: 由指令元素的属性和属性值所组成的对象。如:<div id="aDiv"class="box"></div>的$attrs值为:{id: "aDiv", class: "box"}
    • $transclude: transclude链接函数是实际被执行用来克隆元素和操作DOM的函数。

    指令的控制器和link函数可以进行互换。控制器主要是用来提供可在指令间复用的行为,但
    链接函数只能在当前内部指令中定义行为,且无法在指令间复用。<b>由于指令可以require其他指令所使用的控制器,因此控制器常被用来放置在多个指令间共享的动作。</b>

    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。
    • ^ 如果在指令链的父指令中没有找到需要的控制器,则会进一步往指令链上游寻找需要的控制器。
    • ?^ <b>教程的解释是:</b>我们可选择地加载需要的指令并在父指令链中进行查找。
    • 没有前缀的情况: 如果没有前缀,则指令就会在自身所提供的控制器中进行查找,如果没有找到控制器(或者没有找到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的值,其中也存在很多问题。

    相关文章

      网友评论

        本文标题:自定义指令详解

        本文链接:https://www.haomeiwen.com/subject/nnucyttx.html