AngularJs directive

** "指令"是什么? **
指令是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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,858评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,372评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,282评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,842评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,857评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,679评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,406评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,311评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,767评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,945评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,090评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,785评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,420评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,988评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,101评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,298评论 3 372
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,033评论 2 355

推荐阅读更多精彩内容