** "指令"是什么? **
指令是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
规范化的过程如下
- 去掉元素或属性名字前面的x- 和 data-
- 把
:
,-
,_
转换成小驼峰命名法(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 注释: ``
- E 元素:
- 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类似, 加一个"?", 变成"" 和 "=?", 这样如果我们的绑定是不可用的. 系统不会抛出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已经稳定, 我们可以知道子元素的一些信息, 如子元素个数, 布局结构等.
如果我们直接在"工厂函数"中返回一个函数, 或者在返回的"指令定义对象"中指定link属性的值为一个函数, 那么等价于在compile中返回的post-link函数. 如果指定了compile属性, 那么link属性将会被忽略.
更具体的关于这几个函数的介绍请参考ng.service.$compile