AngularJs directive

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

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

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

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

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

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

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: '
', 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 元素:
    • A 属性:
    • C 类名:
    • 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

    Check out the contents!

//myDirective.html

directive transclude

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

directive transclude

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

实际上你能猜到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

接下来的这部分内容可能新手会比较难理解, 如果不适, 请稍作休息, 不要砸电脑. 上面的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

  • 对于"<" 和 "=", 你可以指定它们的绑定的是可选的, 原理和require类似, 加一个"?", 变成"

如果我们的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)