AngularJS权威指南-指令详解(一)

144145次阅读
没有评论

共计 13722 个字符,预计需要花费 35 分钟才能阅读完成。

一、指令定义

指令,可以简单把它简单的理解成在特定DOM元素上运行的函数,指令可以扩展这个元素的功能。

定义指令的方法:directive(‘directiveName’,function);directiveName表示指令的名称为字符串,用驼峰命名法,应用DOM时为directive-name。function这个函数返回一个对象,其中定义了指令的全部行为。$compile服务利用这个方法返回的对象,在DOM调节指令时用来构造指令的行为。

指令的工厂函数只会在编译器第一次匹配到这个指令时调用一次。和controller函数类似,可以通过$injetor.invoke手动调用指令的工厂函数。

angular.module('my-app', []).directive('nameSpaceDirectiveName', function factory(injectables) {
    var directiveDefinitionObject = {
        restrict: string,//指令的使用方式,包括E(元素),C(类),M(注释),A(属性,默认值)
        priority: number,//指令执行的优先级
        template: string or Template Function:function(){...},//指令使用的模板,用HTML字符串的形式表示
        templateUrl: string,//从指定的url地址加载模板
        replace: bool or string,//是否用模板替换当前元素,若为false,则append在当前元素上
        transclude: bool,//是否将当前元素的内容转移到模板中
        scope: bool or object,//指定指令的作用域
        controller:string or function controllerConstructor($scope, $element, $attrs, $transclude){...},//定义与其他指令进行交互的接口函数
        controllerAs:string,
        require: string,//指定需要依赖的其他指令
        link: function postLink(scope, iElement, iAttrs) {...},//以编程的方式操作DOM,包括添加监听器等
        compile: function compile(tElement, tAttrs, transclude){//返回一个对象或链接函数
            return: {
                pre: function preLink(scope, iElement, iAttrs, controller){...},
                post: function postLink(scope, iElement, iAttrs, controller){...}
            }
            //或者
            return function postLink(...){...}
        }//编程的方式修改DOM模板的副本,可以返回链接函数
    };
    return directiveDefinitionObject;
});

一个JavaScript对象由键和值组成。当一个给定的键的值被设置为一个字符串、布尔值、数字、数组或者对象时。我们把这个键称之为属性。当把这个键设置为函数时,我们把这个键称之为方法。

  1. restrict[String]:这是一个可选参数,默认为A,字符串形式,可以多个值。

    取值

    含义

    使用示例

    E

    标签

    <my-menu title=”Products”></my-menu>

    A

    属性

    <div my-menu=”Products”></div>

    C

    <div class=”my-menu:Products”></div>

    M

    注释

    <!– directive:my-menu Products –>

    (注意:注释两端一定要留空格)

    一般考虑到浏览器的兼容性,强烈建议使用默认的属性就可以即即以属性的形式来进行声明。最后一种方式建议再不要求逼格指数的时候千万不要用。

  2. priority[Number]:优先级参数(Number),默认为0,如果一个元素上具有两个优先级相同的指令,声明在前面的会优先调用。最高优先级指令为ng-repeat,这个参数值为1000。
  3. terminal[Boolean]:这个参数告诉AngularJS停止运行当前元素上比本指令优先级低的指令,相同级别的还是会执行。
  4. template[String or Function]:template参数是可选的,必须被设置为以下两种形式之一:
    •  一段HTML文本;
      angular.module('my-app',[]).directive('myDirective', function () {
                  return { 
                      restrict: 'EAC', 
                      template:"<a href='{{ linkHref }}'>{{ linkName }}</a>"
              };
      })
    • 一个可以接受两个参数的函数,参数为tElement和tAttrs,并返回一个代表模板的字符串。tElement和tAttrs中的t代表template,是相对于instance的。
      angular.module('my-app',[]).directive('myDirective', function () {
          return {
              restrict: 'EAC',
              template: function (elem, attr) {
                  //参数elem,attr,返回一个代表模板的字符串。
                  return "<a href='" + attr.value + "'>" + attr.text + "</a>";
              }
          };
      })
  5. templateUrl[String or Function]:templateUrl是可选的参数,可以是以下类型:
    • 一个代表外部HTML文件路径的字符串;
    • 一个可以接受两个参数的函数,参数为tElement和tAttrs,并返回一个外部HTML文件路径的字符串。

    无论哪种方式,模板的URL都将通过ng内置的安全层,特别是$getTrustedResourceUrl,这样可以保护模板不会被不信任的源加 载。 默认情况下,调用指令时会在后台通过Ajax来请求HTML模板文件。

    • 在本地开发时,需要在后台运行一个本地服务器,用以从文件系统中加载HTML模板,否者会导致CORS错误;
    • 模板加载时异步的,意味着编译和链接要暂停,等待模板加载完成。

    加载大量的模板将严重拖慢一个客户端应用的速度。为了避免延迟,可以在部署应用之前 对HTML模板进行缓存。模板加载完成,AngularJS会将它默认缓存到$templateCache服务中。

    angular.module('my-app',[]).directive('myDirective', function () {
                return { 
                    restrict: 'AEC', 
                    templateUrl: function (elem, attr) {
                        return attr.value + "xxx.html";  //当然这里我们可以直接指定路径,同时在模板中可以包含表达式
                    }
            };
    })
  6. replace[Boolean]:replace是一个可选参数,如果设置了这个参数,值必须为true,因为默认值为false。默认值意味着模板会被当作子元素插入到调用此指令的元素内部:
    <div my-directive="myDirective"></div>
    angular.module('my-app', []).directive("myDirective", function () {
        return {
            replace: true,
            template: "<a href='http://www.baidu.com'>hello Angular</a>"
        }
    });

二、指令作用域(Scope)

<div ng-app='my-app' ng-init="one='First Date'">
    爷爷:{{ one }}
    <div ng-init="two='second Date'">
        继承爷爷:{{ one }}
        儿子:{{ two }}
        <div ng-init="three='three Date'">
            继承爷爷:{{ one }}
            继承儿子:{{ two }}
            孙子:{{three }}
        </div>
    </div>
</div>

$rootScope对象实在ng-app声明的时候创建的。其里面的DOM都会继承$rootScope。上面的代码在$rootScope中设置了三个属性:one,twe,three。从这里开始,DOM的每个指令调用时都可能会:

  • 直接调用相同的作用域。
  • 从当前作用域对象继承一个新的作用域对象。
  • 创建一个同当前作用域相隔离的独立作用域对象。

◊scope[Boolean or Object]:参数是可选的,可以被设置为true或一个对象。默认值是false。

如果一个元素上有多个指令使用了隔离作用域,其中只有一个可以生效。只有指令模板中的根元素可以获得一个新的作用域。因此,对于这些对象来说scope默认被设置为true

内置指令ng-controller的作用,就是从父级作用域继承并创建一个新的子作用域。它会创建一个新的从父作用域继承而来的子作用域。

指令嵌套并不一定意味着需要改变它的作用域。默认情况下,子指令会被付予访问父DOM元素对应的作用域的能力。学过JavaScript事件都知道冒泡时间,有向上传播,有向下传播,有独立传播。

See the Pen OypMqq by iTattoo (@ZhouYanlang) on CodePen.


①.false:继承但不隔离(下能影响上,上能影响下)

See the Pen qOrZEo by iTattoo (@ZhouYanlang) on CodePen.


②.true:继承并隔离(继承且相互独立)

See the Pen BoWKKB by iTattoo (@ZhouYanlang) on CodePen.


③.{}:隔离且不继承 (不继承且相互独立)

See the Pen RWpaav by iTattoo (@ZhouYanlang) on CodePen.

具有独立作用域的指令最主要的使用场景是创建可复用的组件,组件可以在未知上下文中使用,并且可以避免全局污染所处的外部作用域或不经意地污染内部作用域。

下面看一个使用继承作用域的指令的例子:

See the Pen rOyeWz by iTattoo (@ZhouYanlang) on CodePen.

三、绑定策略(Scope对象)

See the Pen gamroY by iTattoo (@ZhouYanlang) on CodePen.


以上为bootstrap选项卡切换的效果。
为了让新的指令作用域可以访问当前本地作用域的变量,需要使用下面三种别名的一种:

  • 本地作用域属性(@):使用@符号将本地作用域同DOM属性的值进行绑定。指令内部的作用域可以使用外部作用域的变量;传递一个字符串作为属性的值例如:name:@/name:@attr。前者表示全局的属性。后者表示指定属性名。
  • 双向绑定(=):通过=可以将本地作用域上的属性同父级作用域上的属性进行双向的数据绑定。本地属性会反映出父数据模型中所发生的变化。使用父作用域中的一个属性,绑定数据到指令的属性中。例如:name:=/name:=attr。
  • 父级作用域绑定(&):通过&符号可以对父级作用域进行绑定,以便在其运行函数使用父作用域中的一个函数,可以在指令中调用。例如:name:&/name:&functionName。
scope:{
    name: "@",          // name 值传递 (字符串,单向绑定)
    amount: "=",        // amount 引用传递(双向绑定)
    save: "&"           // 保存操作
}

可能上面的看的有点不明白,那下面说的通俗一点。

scope: 创建指令的作用范围,scope在指令中作为属性标签传递。Scope 是创建可以复用指令的必要条件,
每个指令(不论是处于嵌套指令的哪一级)都有其唯一的作用域,它不依赖于父scope。

  • name: “@” (值传递,单向绑定):”@”符号表示变量是值传递。指令会检索从父级scope中传递而来字符串中的值。指令可以使用该值但无法修改,是最常用的变量。
  • amount: “=” (引用,双向绑定):”=”符号表示变量是引用传递。指令检索主Scope中的引用取值。值可以是任意类型的,包括复合对象和数组。指令可以更改父级Scope中的值,所以当指令需要修改父级Scope中的值时我们就需要使用这种类型。
  • save: “&” (表达式):“&”符号表示变量是在父级Scope中启作用的表达式。它允许指令实现比修改值更高级的操作。

扩展阅读:
AngularJS开发指南05:指令
理解AngularJS的作用域Scope

举例说明

<div ng-controller="testC">
    <say-hello speak="content">美女</say-hello>
</div>
<script>
    var app = angular.module('my-app', []);
    app.controller('testC', function ($scope) {
        $scope.content = '今天天气真好!';
    });
    app.directive('sayHello', function () {
        return {
            restrict: 'E',
            scope: {
                say: '=speak'
            },
            template: '<div>hello,<b ng-transclude></b>,{{say}}</div>',
            replace: true,
            transclude: true

        };
    });
</script>

执行的流程是这样的:
① 指令被编译的时候会扫描到template中的{ {say} },发现是一个表达式;
② 查找scope中的规则:通过speak与父作用域绑定,方式是传递父作用域中的属性;
③ speak与父作用域中的content属性绑定,找到它的值“今天天气真好!”
④ 将content的值显示在模板中
这样我们说话的内容cont就跟父作用域绑定到了一其,如果动态修改父作用域的content的值,页面上的内容就会跟着改变,正如你点击“换句话”所看到的一样。

在举个栗子

<script type="text/ng-template" id="expanderTemp.html">
    <div class="mybox">
        <div class="mytitle" ng-click="toggleText()">{{title}}</div>
        <div ng-transclude ng-show="showText" id="dis"></div>
    </div>
</script>
<div ng-controller="testC">
    <expander etitle="title">{{text}}</expander>
</div>
<script>
    var app = angular.module('my-app', []);
    app.controller('testC', function ($scope) {
        $scope.title = '个人简介';
        $scope.text = '大家好,我是一名前端工程师,我正在研究AngularJs,欢迎大家与我交流';
    });
    app.directive('expander', function () {
    return {
        restrict: 'E',
        replace: true,
        transclude: true,
        templateUrl: 'expanderTemp.html',
        scope: {
            title: '=etitle'
        },
        link: function (scope, element, attris) {
            scope.showText = false;
            scope.toggleText = function () {
                scope.showText = !scope.showText;
            }
        }
    };
});
</script>

首先我们定义模板的时候使用了ng的一种定义方式<script type=”text/ng-template” id=”expanderTemp.html”>,在指令中就可以用templateUrl根据这个id来找到模板。指令中的 {{title}}表达式由scope参数指定从etitle传递,etitle指向了父作用域中的title。为了实现点击标题能够展开收缩内容,我们把这部分逻辑放在了link函中,link函数可以访问到指令的作用域,我们定义showText属性来表示内容部分的显隐,定义 toggleText函数来进行控制,然后在模板中绑定好。 如果把showText和toggleText定义在controller中,作为$scope的属性呢?显然是不行的,这就是隔离作用域的意义所在,父 作用域中的东西除了title之外通通被屏蔽。

上面的例子中,scope参数使用了=号来指定获取属性的类型为父作用域的属性,如果我们想在指令中使用父作用域中的函数,使用&符号即可,是同样的原理。

使用controller和require进行指令间通信

使用指令来定义一个ui组件是个不错的想法,首先使用起来方便,只需要一个标签或者属性就可以了,其次是可复用性高,通过controller可以动态控制ui组件的内容,而且拥有双向绑定的能力。

想一下我们进行模块化开发的时候的原理,一个模块暴露(exports)对外的接口,另外一个模块引用(require)它,便可以使用它所提供的服务了。ng的指令间协作也是这个原理,这也正是自定义指令时controller参数和require参数的作用。

controller参数用于定义指令对外提供的接口,它的写法如下:

controller: function controllerConstructor($scope, $element, $attrs, $transclude)

它是一个构造器函数,将来可以构造出一个实例传给引用它的指令。controller可以使用的参数,作用域、节点、节点的属性、节点内容的迁移,这 些都可以通过依赖注入被传进来,所以你可以根据需要只写要用的参数。

require参数便是用来指明需要依赖的其他指令,它的值是一个字符串,就是所依赖的指令的名字,这样框架就能按照你指定的名字来从对应的指令上面寻找定义好的controller了。不过还稍稍有点特别的地方,为了让框架寻找的时候更轻松些,我们可以在名字前面加个小小的前缀:^,表示从父节点上寻找,使用起来像这样。require :’^directiveName’,如果不加,$compile服务只会从节点本身寻找。
另外还可以使用前缀:?,此前缀将告诉$compile服务,如 果所需的controller没找到,不要抛出异常。

所需要了解的知识点就这些,接下来是例子时间,这是从书上抄来的一个例子,我们要做的是一个手风琴菜单,就是多个折叠菜单并列在一起,此例子用来展示指令间的通信再合适不过。

<script type="text/ng-template" id="expanderTemp.html">
    <div class="mybox">
        <div class="mytitle" ng-click="toggleText()">{{title}}</div>
        <div ng-transclude ng-show="showText" id="dis"></div>
    </div>
</script>
<div ng-controller="testC">
    <accordion>
        <expander ng-repeat="expander in expanders" etitle="expander.title">{{expander.text}}</expander>
    </accordion>
</div>
<script>
    var app = angular.module('my-app', []);
    app.controller('testC', function ($scope) {
        $scope.expanders = [
            {
                title: '个人简介',
                text: '大家好,我是一名前端工程师,我正在研究AngularJs,欢迎大家与我交流。'
            },
            {
                title: '我的爱好',
                text: '运动类:乒乓球。电脑类:前端技术、打LOL。其他类:欣赏美女'
            },
            {
                title: '性格及工作',
                text: '追求完美主义的处女座极品男人就是我啦~严重的代码洁癖以及对垃圾代码的零容忍!希望通过自己的努力进入理想的公司工作。'
            }
        ];
    });
    app.directive('accordion', function () {
        return {
            restrict: 'E',
            template: '<div ng-transclude></div>',
            replace: true,
            transclude: true,
            controller: function () {
                var expanders = [];
                this.gotOpended = function (selectedExpander) {
                    angular.forEach(expanders, function (e) {
                        if (selectedExpander != e) {
                            e.showText = false;
                        }
                    });
                }
                this.addExpander = function (e) {
                    expanders.push(e);
                }
            }
        }
    });
    app.directive('expander', function () {
        return {
            restrict: 'E',
            replace: true,
            transclude: true,
            require: '^?accordion',
            templateUrl: 'expanderTemp.html',
            scope: {
                title: '=etitle'
            },
            link: function (scope, element, attris, accordionController) {
                scope.showText = false;
                accordionController.addExpander(scope);
                scope.toggleText = function () {
                    scope.showText = !scope.showText;
                    accordionController.gotOpended(scope);
                }
            }
        };
    });
</script>

下面具体讲解transclude,controller,require

  1. transclude[Boolean]:transclude是一个可选的参数。默认值是false。嵌入通常用来创建可复用的组件,典型的例子是模态对话框或导航栏。我们可以将整个模板,包括其中的指令通过嵌入全部传入一个指令中。指令的内部可以访问外部指令的作用域,并且模板也可以访问外部的作用域对象。为了将作用域传递进去,scope参数的值必须通过{}或true设置成隔离作用域。如果没有设置scope参数,那么指令内部的作用域将被设置为传入模板的作用域。
    <div sidebox title="Links">
        <ul>
            <li>1</li>
            <li>2</li>
        </ul>
    </div>
    <div side-box title="TagCloud">
        <div class="tagcloud">
            <a href="">Graphics</a>
            <a href="">ng</a>
            <a href="">D3</a>
            <a href="">Front-end</a>
            <a href="">Startup</a>
        </div>
    </div>
    <script>
        var app = angular.module('my-app', []);
        app.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>'
            };
        });
        app.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>'
            };
        });
    </script>

    只有当你希望创建一个可以包含任意内容的指令时,才使用transclude: true。上面这段代码告诉ng编译器,将它从DOM元素中获取的内容放到它发现ng-transclude指令的地方。如果指令使用了transclude参数,那么在控制器无法正常监听数据模型的变化了。建议在链接函数里使用$watch服务。再来你看个官网的例子:

    <script type="text/ng-template" id="expanderTemp.html">
        <div class="alert">
            <a href class="close" ng-click="close()">&times;</a>
            <div ng-transclude></div>
        </div>
    </script>
    <div ng-controller="Controller">
        <my-dialog ng-hide="dialogIsHidden" on-close="hideDialog()">
            Check out the contents, {{name}}!
        </my-dialog>
    </div>
    <script>
        var app = angular.module('my-app', []);
        app.controller('Controller', ['$scope', '$timeout', function ($scope, $timeout) {
            $scope.name = 'Tobias';
            $scope.hideDialog = function () {
                $scope.dialogIsHidden = true;
                $timeout(function () {
                    $scope.dialogIsHidden = false;
                }, 2000);
            };
        }]);
        app.directive('myDialog', function () {
            return {
                restrict: 'E',
                replace: true,
                transclude: true,
                scope: {
                    close: '&onClose'
                },
                require: '^?accordion',
                templateUrl: 'expanderTemp.html'
            };
        });
    </script>
  2. controller[String or Function]: controller参数可以是一个字符串或一个函数。当设置为字符串时,会以字符串的值为名字,来查找注册在应用中的控制器的构造函数:
    /*第一种方式*/
    angular.module('my-app', [])
            .controller('SomeController', function ($scope, $element, $attrs, $transclude) {
                //控制逻辑放这里
            })
            .directive('myDirective', function () {
                return {
                    restrict: 'A',
                    controller: 'SomeController'
                }
            })

    可以在指令内部通过匿名函数的方式来定义一个内联的控制器:

    /*第二种方式*/
    angular.module('my-app', [])
            .directive('myDirective', function () {
                return {
                    restrict: 'A',
                    controller: function ($scope, $element, $attrs, $transclude) {
                        //控制逻辑放这里
                    }
                }
            })

    控制器也有一些特殊的服务(参数)可以被注入到指令中:
    ♠. $scope:与指令元素相关联的当前作用域。
    ♣. $element:当前指令对应的元素。
    ♥. $attrs:由当前元素的属性组成的对象。 列入下面的元素

    <div id="aDiv"class="box"></div>
    具有如下的属性对象:{ id: "aDiv", class: "box" }

    ♦. $transclude:嵌入链接函数会与对应的嵌入作用域进行预绑定。transclude链接函数是实际被执行用来克隆元素和操作DOM的函数。

    <my-link value="http://www.baidu.com">百度</my-link>
    <div my-link value="http://www.google.com">谷歌</div>
    <script>
        angular.module('my-app', [])
                .directive('myLink', function () {
                    return {
                        restrict: 'EA',
                        transclude: true,
                        controller: function ($scope, $element, $attrs, $transclude) {
                            $transclude(function (clone) {
                                var a = angular.element('<a>');
                                a.attr('href', $attrs.value);
                                a.text(clone.text());
                                $element.append(a);
                            });
                        }
                    };
                });
    </script>

    指令的控制器和Link函数可以进行互换。控制器主要是用来提供可在指令间复用的行为,但链接函数只能在当前内部指令中定义行为,且无法再指令间复用。
    ω.仅在compile参数中使用transcludeFn是推荐的做法。
    ω.link函数可以将指令互相隔离开来,而controller则定义可复用的行为
    ω.如果我们希望将当前指令的API暴露给其他指令使用,可以使用controller参数,否则可以使用link来构造当前指令元素的功能性(即内部功能)。如果我们使用了scope.$watch()或者想要与DOM元素做实时的交互,使用链接会是更好的选择
    ω.使用了嵌入,控制器中的作用域所反映的作用域可能与我们所期望的不一样,这种情况下,$scope对象无法保证可以被正常更新。
    ω.当想要同当前屏幕上的作用域交互时,可以使用传入到link函数中的scope参数。

  3. controllerAs[String]:controllerAs参数用来设置控制器的别名,可以以此为名发布控制器,并且作用域可以访问controllerAs。这样就可以在视图中引用控制器甚至无需注入$scope。
    <div ng-controller="MainController as main">
        <input type="text" ng-model="main.name"/>
        <span>{{ main.name }}</span>
    </div>
    <script>
        angular.module('my-app', [])
                .controller('MainController', function () {
                    this.name = "Halower";
                });
    </script>

    控制器的别名使路由和指令具有创建匿名控制器的强大能力。这种能力可以将动态的对象创建成为控制器,并且这个对象是隔离的、易于测试。

    <div ng-controller="MainController as main">
        <input type="text" ng-model="main.name"/>
        <span>{{ main.name }}</span>
        <div my-directive></div>
    </div>
    <script>
        angular.module('my-app', [])
                .controller('MainController', function () {
                    this.name = "Halower";
                })
                .directive('myDirective', function () {
                    return {
                        restrict: "A",
                        template: "<p>{{main.name}}</p>",
                        controllerAs: "main",
                        controller: function () {
                            this.name = "hello Angular"
                        }
                    }
                })
    </script>
  4. require[String or Array]:require为字符串代表另外一个指令的名字。require会将控制器注入到其所指定的指令中,并作为当前指令的链接函数的第四个参数。字符串或数组元素的值是会在当前指令的作用域中使用的指令名称。scope会影响指令作用域的指向,是一个独立作用域,一个有依赖的作用域或者完全没有作用域。在任何情况下,ng编译器在查找子控制器时都会参考当前指令的模板。
    • 如果不使用^前缀,指令只会在自身的元素上查找控制器。指令定义只会查找定义在指令作当前用域中的ng-model=””。如<div my-directive ng-model=”object></div>
    • 如果使用?前缀,在当前指令中没有找到所需要的控制器,会将null作为传给link函数的第四个参数。
    • 如果添加了^前缀,指令会在上游的指令链中查找require参数所指定的控制器。
    •  如果将前面两个?^前缀选项的行为组合起来,我们可选择地加载需要的指令并在父指令链中进行查找
    • 如果没有任何前缀,指令将会在自身所提供的控制器中进行查找,如果没有找到任何控制器(或具有指定名字的指令)就抛出一个错误。

正文完
 0
Chou Neil
版权声明:本站原创文章,由 Chou Neil 于2015-10-01发表,共计13722字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。