AngularJs directive

作者: 菲汐 | 来源:发表于2016-06-24 23:36 被阅读461次

    ** "指令"是什么? **
    指令是Angular中一个很重要的概念, 它是附加在HTML元素上的自定义标记, 在Angular官方文档中称之为HTML语言的DSL ( 特定领域语言 ) 扩展

    根据指令的使用场景和作用可以分为组件型指令和装饰器型指令, 组件型的指令主要是为了将复杂的View分离, 使用View具有更强的可读性和维护性. 例如Tab, Accordion. 装饰器型的指令主要是为DOM添加行为, 例如ngShow, 让DOM具有条件显示的能力.

    ** 指令的匹配 **
    我们在JS文件中定义了myDirective指令

    angular.module('app',[]).directive('myDirective', function(){
    })
    

    我们如何在HTML上去使用这个指令?

    <div my-directive></div>
    

    除了这种最常见的方式之外, 你还可以通过下面这几种格式来匹配一个指令, 加上前缀更符合HTML5的规范.

    <div data-my-directive></div>
    <div x-my-directive></div>
    <div my:directive></div>
    <div my_directive></div>
    

    Angular把一个元素的标签和属性名字规范化, 通常我们的指令采用小驼峰命名法, 比如ngModel. 然而HTML是不能区分大小写的, 所以我们无法在HTML上直接使用, 取而代之的是用破折号间隔的形式, 比如ng-model

    规范化的过程如下

    1. 去掉元素或属性名字前面的x- 和 data-
    2. :, -, _转换成小驼峰命名法(camelCase)

    ** 创建指令 **
    和控制器一样, 指令也是注册在模块上的. 要注册一个指令, 你可以用 module.directive API.
    可以接受directiveName和directiveFactory两个参数注册单个指令, 也可以接受key/value形式的哈希对象, 注册多个指令, 这里的key对应directiveName, value对应directiveFactory.

    myModule.directive('myDirective', function factory($log) {
        return {
            priority: 0,
            templateNamespace: 'html',
            template: '<div></div>',
            templateUrl: 'myDirective.html',
            replace: false,
            transclude: false,
            restrict: 'EACM',
            scope: false,
            require: '^anotherDirective',
            controller: function($scope){},
            controllerAs: 'vm', 
            bindToController: false,
            compile: function compile(tElement, tAttrs, transclude) {
                return {
                    pre: function preLink(scope, iElement, iAttrs, controller) {...},
                    post: function postLink(scope, iElement, iAttrs, controller) {...}
                }
            },
            link: function postLink(scope, iElement, iAttrs) {... }
        };
    });
    
    

    上面代码中的factory函数, 是"工厂函数", 它是用来创建指令的. 它只会被调用一次, 就是当编译器第一次匹配到相应指令的时候, 你可以在其中进行任何初始化的工作.
    工厂函数返回的对象是"指令定义对象", 给编译器提供了生成指令需要的细节.

    • restrict
      可以指定EACM中的任意一个字母或组合, 它是用来限制指令的声明格式的. 默认是"A". 常用"E", "A" 或 "EA".
      • E 元素: <my-directive></my-directive>
      • A 属性: <div my-directive="exp"> </div>
      • C 类名: <div class="my-directive: exp;"></div>
      • M 注释: ``
    • priority
      当一个元素上有多个指令, 通过优先级排序, 然后执行compile函数. 在后文"指令的生命周期"会涉及到更多细节. 默认为0, 常用指令优先级如下:
      • ngRepeat : 1000
      • ngSwitchWhen : 800
      • ngIf : 600
      • ngInclude : 400
      • ngView : 400
    • terminal
      设置为true, 意味着元素上优先级小于当前指令的其他指令都不会执行, 也就是说执行到当前指令就结束了, 相同优先级的指令不包含在内.
    • templateNamespace
      指定template的文档类型, 可选'html'、'svg'、'math', 默认为'html'.
    • template
      模板, 将当前的元素替换为模板的内容, 这个替换过程会自动将元素的属性添加到新元素上. 可以指定一个函数, 动态的返回模板, 这个函数可以接受两个参数, 第一个是当前元素, 第二个是该元素上的属性集合.
    • templateUrl
      较长的模板直接写在JS文件中, 是难以接受的, 可以加载外部文件的内容作为模板, 因为模板加载是异步的, 所以编译和链接都会等到加载完成后再执行.
      除了直接指定字符串值以外, 还可以指定一个函数, 同template.
    templateUrl: function(elem,attr){
        return 'template' + attr.name + '.html'
    }
    
    • require
      为了给父子指令或者兄弟指令的Controller之间搭建一个桥梁. 被require的指令的Controller会作为当前指令link函数的第四个参数. 这样就可以调用外部Controller的方法. 它的值与link函数的参数有以下几种对应方式.

      • 字符串: 对应单个controller
      • 数组: 对应一个数组, 通过顺序获取controller
      • 对象: key随便取, value为需要require的指令, 对应一个对象, 通过key获取controller

      require有两个修饰符号:"?"、"^", 还可以组合使用"?^", "?^^"

      • ? : 如果require没有找到相应的指令, 不要抛出异常.
      • 无前缀: $compile服务只从当前节点查找
      • ^ : 在当前节点和父级节点查找
      • ^^ : 只会在父级节点查找.
    • replace
      设置成true, Angular会用模板的内容来替换当前元素, 但是这里有个坑需要注意, 我们必须保证模板内容只有一个根节点, 否则会抛出Invalid Template Root Exception.

    • scope
      指令作用域, 有三种指定方式

      • 不指定scope 或设置为 false : 表示这个指令不需要创建新作用域. 如果元素上有新作用域或独立作用域指令, 则直接使用它, 没有则使用父级作用域.
      • true : 表示指令需要一个新作用域, 如果元素上有多个指令要求创建了新作用域, 那么只有一个新作用域会被创建. 能从父作用域继承.
      • 哈希对象 : 表示指令需要一个独立的作用域, 它不会从父节点自动继承任何属性, 既然是独立作用域, 所以一个元素上只能有一个独立作用域指令. 至于哈希对象的内容, 请看"改变指令的scope"小节.
    • transclude
      设置成true, 配合ngTransclude指令使用, 可以将指令包裹的子元素添加进模板进行编译, 但是这里又一个坑需要注意, 被包裹的子元只能访问指令外部的作用域 ( scope ) , 而不能访问指令自己的作用域.

    //index.html
    <my-directive>
        Check out the contents!
    </my-directive>
    //myDirective.html
    <div>
        <h1>directive transclude</h1>
        <span ng-transclude></span>
    </div>
    

    上面的代码会生成如下代码

    <div>
        <h1>directive transclude</h1>
        <span>Check out the contents!</span>
    </div>
    
    • controllerAs
      Angular从1.2开始引入了新语法Controller as. 在此之前, 我们需要在controller中注入$scope服务, 才能在视图中使用一些变量. 现在我们可以不注入$scope, 完成同样的事情.
    //js
    angular.module('app').controller('MyController', function(){
        var vm = this;
        vm.name = 'John Doe';
    })
    //template
    <span ng-bind="vm.name"></span>
    

    实际上你能猜到Angular在内部做了什么

    if(directive.controllerAs){
        locals.$scope[directive.controllerAs] = controllerInstance;
    }
    
    • bindToController
      指令中通过scope:{}属性声明的变量仍然会被自动绑定到$scope, 而不是vm上, 通过设置bindToController为true, scope上的变量会自动绑定到vm.
    • controller 请看"指令的生命周期小节"
    • compile 请看"指令的生命周期小节"
    • link 请看"指令的生命周期小节"

    ** 改变指令的scope **
    默认情况下, 指令获取它父节点的controller的scope. 但这并不适用于所有情况. 如果将父controller的scope暴露给指令, 那么他们可以随意地修改 scope 的属性. 在某些情况下, 你希望指令能够添加一些仅限内部使用的属性和方法. 那么你可以使用上一小节所说的true 或 哈希对象. 先看一个例子

    //scope属性
    {
      name: '@',
      detail: '=',
      job: '<',
      update: '&'  
    }
    //html
    <my-directive name="John" detail="detail" age="age" update="update(times)">
    

    接下来的这部分内容可能新手会比较难理解, 如果不适, 请稍作休息, 不要砸电脑. 上面的scope属性会为指令创建一个独立的作用域, 假设其为'A', 父级作用域为'B'

    • '@' 将独立作用域中的变量与DOM属性绑定.
      绑定结果总是一个字符串, A.name的值被绑定为"John". 除了直接绑定字符串, 我们还可以绑定表达式, 比如name="{{name}}", 因为表达式最终解析出来也是一个字符串, 如果B.name的值发生了变化, A.name的值也会随之变化.
    • '<' 单向数据绑定.
      A.job的值被绑定到B.job, 类似于@类型的job="{{job}}". 通常情况下B.job的变化会同步到A.job. A.job的变化不会同步到B.job, 但如果你绑定的job变量是一个对象, 那么A.job.property的变化就会映射到B.job上, 因为它们的引用是同一个.
      注意这里是不需要加{{}}的. 直接指定变量名就可以
    pscope.job= {
        title: 'myDetail',
        content: 'myContent'
    }
    
    • '=' 双向数据绑定.
      顾名思义, 相比于'<', B.detail的变化能同步到A.detail上, A.detail的变化也能同步到B.detail上 .
    • '&' 绑定函数或执行表达式.
      当我们调用A.update(), B.update()就会被调用. 假设B.update如下, 我们怎么传参? Are you kidding me ? A.update(3)不就行了? too young to simple, 回头看一下我们的DOM, update="update(times)", 在调用的时候必须这样A.update({times: 3}). 注意这两个地方的参数名必须一致. 但B.update的形参, 你想叫什么都可以.
    B.update = function(times){
        //times can be any name you want
        return count + times;
    }
    

    我们还可以指定为一个可执行的表达式, 实际上就是一个Js语句. Angular会自动为我们创建一个函数包裹住这个执行表达式

    update="count = count + 1"
    

    使用须知,

    • 你有可能曾经看到过name: '@whatever'这种写法, 它和直接使用'@'有什么区别呢? 其实name是我们当前指令scope上的变量名, whatever是我们写在HTML上的属性名, 它们是可以不同的, 但是当它们名字相同时, 就可以简写为'@', 实际上等价于 name: '@name'.
    \\scope
    {
      name: '@whatever',
    }
    \\html
    <my-directive whatever='John'>
    
    • 对于"<" 和 "=", 你可以指定它们的绑定的是可选的, 原理和require类似, 加一个"?", 变成"<?" 和 "=?", 这样如果我们的绑定是不可用的. 系统不会抛出Non-Assignable Expression

    如果我们的DOM不变, 但是我们scope定义为false, 也就是不使用独立作用域, 我们该如何获取这些属性的值?

    • 对于@型的绑定, 获取DOM属性字符串, attrs.property
    • 对于<, =型的绑定, 获取父scope上表达式的值, scope.$eval(attrs.detail)
    • 对于&型的绑定, scope.$eval(attrs.update, {times: 1})

    ** 指令的生命周期 **

    myModule.directive('myDirective', function factory($log) {
        $log.info('...Injecting...');
        return {
            controller: function(){
                $log.info('...Controller...');
            },
            compile: function compile(tElement, tAttrs, transclude) {
                $log.info('...Compile...');
                return {
                    pre: function preLink(scope, iElement, iAttrs, controller) {
                        $log.info('...Pre-Link...');
                    },
                    post: function postLink(scope, iElement, iAttrs, controller) {
                        $log.info('...Post-Link...');
                    }
                }
            }
        };
    });
    
    

    Angular中, 一个指令从开始解析到生效, 按照顺序一共会经历Inject, Compile, Controller, Pre-Link, Post-Link几个过程.

    • Injecting
      因为"工厂函数"是可以被注入的, 我们可以在这里获取依赖的服务, 在Angular第一次使用这个指令之前, 会
      先调用$Injector注入函数来注入服务, 这个过程只会发生一次. 如果应用中没有使用过指令, 那么连注入都不会发生.
      在返回"指令定义对象"之前的代码作用域是一个闭包, 也就是$log.info('...Injecting...');这里, 这个区域是所有指令实例共享的作用域, 可以在这里设置指令的默认配置信息, 但是建议大家不要这么做, 尝试把配置抽取到一个Constant中可能会更好.

    • Compile
      这个函数会在每一个指令被实例化时执行一次. 它接受两个参数, DOM元素和Attributes集合. 在这个阶段我们是无法访问$scope的, 通常在这个阶段我们要做的是修改DOM节点. 修改完成的节点稍后会被自动$compile, 变成Live DOM ( 感知scope变化自动更新自己 ) . compile函数的最后一句用来返回link函数. 指令的编译参考[HTML Compiler]

    • Controller
      在进入link阶段之前, Angular会根据我们在指令中声明的scope属性, 创建一个独立或非独立的scope, 利用$injector注入$scope服务, 然后调用指令中的Controller来初始化这个scope.

    • Link
      当Controller初始化好指令的$scope后, 将正式进入解析过程, 它分为两个阶段Pre-link和Post-link, 它们对于指令的每个实例来说, 只会执行一次. 对ngRepeat来说是每个循环体都会执行一次. 这里可以使用已经被初始化好的$scope对象, 但是这里的scope并不是被注入的, 而是以参数的形式传入进来的, link函数的参数依次是scope, element, attrs, controller(被require的指令的内部Controller), transcludeFn.

      Pre-link和Post-link的区别在于它们执行在不同的阶段, angular会按照从父节点->子节点的顺序依次执行所有节点的pre-link函数, 等到所有节点的pre-link函数都执行完毕, 则开始以子节点->父节点的顺序, 依次执行post-link函数. 这样可以保证在执行post-link函数时, 所有子节点的DOM已经稳定, 我们可以知道子元素的一些信息, 如子元素个数, 布局结构等.

      Paste_Image.png
      如果我们直接在"工厂函数"中返回一个函数, 或者在返回的"指令定义对象"中指定link属性的值为一个函数, 那么等价于在compile中返回的post-link函数. 如果指定了compile属性, 那么link属性将会被忽略.

    更具体的关于这几个函数的介绍请参考ng.service.$compile

    相关文章

      网友评论

        本文标题:AngularJs directive

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