本节书摘来自华章出版社《AngularJS深度剖析与最佳实践》一书中的第2章,第2.6节,作者 雪狼 破狼 彭洪伟,更多章节内容可以访问云栖社区“华章计算机”公众号查看

2.6 指令

指令(directive)是Angular中一个很重要的概念,相当于一个自定义的HTML元素,在Angular官方文档中称它为HTML语言的DSL(特定领域语言)扩展。
按照指令的使用场景和作用可以分为两种类型的指令:它们分别为组件型指令(Component)和装饰型器指令(Decorator),它们的分类命名,并不是笔者独创的新法,它是在Angular 2.x中提出的概念,笔者认为它们也同样可以使用于Angular 1.x。
组件型指令主要是为了将复杂而庞大的View分离,使得页面的View具有更强的可读性和维护性,实现“高内聚低耦合”和“分离关注点”的有效手段;而装饰器型指令则是为DOM添加行为,使其具有某种能力,如自动聚焦(autoFocus)、双向绑定、可点击(ngClick)、条件显示/隐藏(ngShow/ngHide)等能力,同时它也是链接Model和View之间的桥梁,保持View和Model的同步。在Angular中内置的大多数指令,是属于装饰器型指令,它们负责收集和创建$watch,然后利用Angular的“脏检查机制”保持View的同步。
对于组件型指令和装饰器型指令的这两种区分是非常重要的,它们在写法、业务含义、适用范围等方面都有非常明显的区别,理解了它们,对于我们日常的指令开发也具有很好的指导作用。

2.6.1 组件型指令

组件型指令是一个小型的、自封装和内聚的一个整体,它包含业务所需要显示的视图以及交互逻辑,比如:我们需要在首页放置一个登录框和一个FAQ列表,如果我们把它们都直接写在首页的视图和控制器中,那么首页的视图和控制器将会变得非常庞大,这样不利于我们的分工协作和页面的长期维护。这时候更好的方案应该是,把它们拆分成两个独立的内聚的指令login-panel和faq-list,然后分别将和两个指令嵌入到首页。
注意,我们在这里拆出这两个指令的直接目的不是为了复用,更重要的目的应该是分离View,促进代码结构的优化,达到更好的语义化和组件化,当然对于这样独立内聚的指令,有时我们还能意外地获得更好的复用性。
组件型指令应该是满足封装的自治性、内聚性的,它不应该直接引用当前页面的DOM结构、数据等。如果存在需要的信息,则可以通过指令的属性传递或者利用后端服务接口来自我满足。如login-panel应该在其内部访问登录接口来实现自我的功能封装。它的Scope应该是独立的(isolated),不需要对父作用域的结构有任何依赖,否则一旦父作用域的结构发生改变,可能它也需要相应地变更,这种封装是很脆弱的。更好的封装应该是“高内聚低耦合”的,内聚是描述组件内部实现了它所应该包含的逻辑功能,耦合则描述它和外部组件之间应该是尽量少的相互依赖。
组件型指令的写法通常是这样的:

// 声明一个指令
angular.module('com.ngnice.app').directive('jobCategory', function () {return {// 可以用作HTML元素,也可以用作HTML属性restrict: 'EA',// 使用独立作用域scope: {configure: '='},// 指定模板templateUrl: 'components/configure/tree.html',// 声明指令的控制器controller: function JobCategoryCtrl($scope) {...}};
});

指令中return的这个结果,我们称之为“指令定义对象”。
restrict属性用来表示这个指令的应用方式,它的取值可以是E(元素)、A(属性)、C(类名)、M(注释)这几个字母的任意组合,工程实践中常用的是E、A、EA这三个,对于C、M笔者并不建议使用它们。对于组件型指令来说,标准的用法是E,但是为了兼容IE8,通常也支持一个A,因为IE8的自定义元素需要先用document.createElement注册,用A可以省去注册的麻烦。
scope有三种取值:不指定(undefined)/false、true或一个哈希对象。
不指定或为false时,表示这个指令不需要新作用域。它直接访问现有作用域上的属性或方法,也可以不访问作用域。如果同一节点上有新作用域或独立作用域指令,则直接使用它,否则直接使用父级作用域。
为true时,表示它需要一个新作用域,可以跟本节点上的其他新作用域指令共享作用域,如果任何指令都没有新作用域,它就会创建一个。
为哈希对象时,表示它需要一个独立的(isolated)作用域。所谓独立作用域,是指独立于父作用域,它不会从父节点自动继承任何属性,这样的话,就不会无意间引用到父节点上的属性,导致意料之外的耦合。
要注意,一个节点上如果已经出现了一个独立作用域指令,那么就不能再出现另一个独立作用域指令或者新作用域指令,否则使用scope的代码将无法区分两者,如果自动将两个作用域合并,又会失去“独立性”。总之,记住一句话:独立作用域指令是“排它”的。
那么哈希对象的内容呢?它表示的是属性绑定规则,如:

{// 绑定字面量name: '@',// 绑定变量details: '=',// 绑定事件onUpdate: '&'
}

这里我们绑定了三个属性,以为例,name的值将被绑定为字符串'test',而details的值不是'details',而是绑定到父页面scope上一个名为details的变量,当父页面scope的details变量变化时,指令中的值也会随之变化—即使绑定到number等原生类型也一样。而onUpdate绑定的则是一个回调函数,它是父页面scope上一个名为updateIt的函数。当指令代码中调用scope.onUpdate()的时候,父页面scope的updateIt就会被调用。当然,name也同样可以绑定到变量,但是要通过绑定表达式的方式,比如中,name将会绑定到父页面scope中的name变量,并且也会同步更新。
记住,对于组件型指令,更重要的是内容信息的展示,所以我们一般不涉及指令的link函数,而应该尽量地将业务逻辑放置在Controller中。
组件化的开发方式以及组件化的复用,是我们在前端开发中一直追求的一个理想目标。从最初的iframe、jQuery UI、Extjs、jQuery easyui,我们一直在不懈地朝着组件化的方向前进。Angular首次在其框架中提出指令这种以HTML DSL方式进行语义化、组件化扩展的方式,就我们在这里描述的组件型指令。笔者也更愿意将它称为“Directive as component”(指令即组件)。只要告诉大家下面的实例代码是一个在线支付页面,相信大家很快就能从页面中读懂它业务逻辑了:

<form novalidate name="orderForm" ng-submit="processPage();"><error-panel errors="order.errors"></error-panel><fieldset class="field-group">
<post-address class="post-address" view-model="order.poastAddress" post-address-change="order.postAddressChange();"></post-address></fieldset><fieldset class="field-group"><payment-way class="payment-way" viewmodel="order.paymentWay"></payment-way></fieldset><fieldset class="field-group"><item-list class="item-list" viewmodel="order.items"></item-list></fieldset>.....<div class="submit"><button class="btn primary-btn">提交订单</button></div>
</form>

从上面的代码中,我们能很快地识别出此页面包含:全局错误显示、邮寄地址、在线支付方式、购买商品信息这几个领域概念,然而对于它们的修改和维护也很容易,组件更加的内聚,并且遵守单一职责原则(SRP)。这就是“Directive as component”和组件型指令的迷人之处。
继Angular的指令之后,React也推出了以JSX模板为核心的类HTML语法扩展,以此来实现组件化的开发,而且它也是React中的最重要的核心概念。Google和Mozilla也在推进Web Component技术,它主要以Custom Elements、HTML Templates、Shadow DOM、HTML Imports四大技术为核心,让我们能够像浏览器开发者一样使用HTML、CSS、JavaScript来构建更酷、更炫、独立的HTML节点,使得我们能够快速的应对越来越复杂、多样化的用户体验要求,而不是继续等待浏览器厂商来实现它们。
有兴趣的读者,可以自行关于Web Component的资料。可以参考:http://webcomponents.org/、Google的polymer框架:http://www.polymer-project.org/以及Mozilla的X-Tags框架:http://x-tags.org/。

2.6.2 装饰器型指令

对于装饰器型指令,其定义方式则如下:

angular.module('com.ngnice.app').directive('twTitle', function () {return {// 用作属性restrict: 'A',link: function (scope, element, attrs) {...}};
});

装饰器型指令主要用于添加行为和保持View和Model的同步,所以它不同于组件型指令,我们经常需要进行DOM操作。其restrict属性通常为A,也就是属性声明方式,这种方式更符合装饰器的语义:它并不是一个内容的主体,而是附加行为能力的连接器。
同时,由于多个装饰器很可能被用于同一个指令,包括独立作用域指令,所以装饰器型指令通常不使用新作用域或独立作用域。如果要访问绑定属性,该怎么做呢?仍然看前面的例子,假如不使用独立作用域,我们该如何获取这些属性的值呢?
对于@型的绑定,我们可以直接通过attrs取到它:attrs.name等价于name: '@'。
对于=型的绑定,我们可以通过scope.$eval取到它:scope.$eval(attrs.details)等价于details: '='。
&型的绑定理解起来会稍有困难,先看代码:scope.$eval(attrs.onUpdate, {times: 3});。
和=型绑定一样,onUpdate属性在本质上是当前scope上的一个表达式。特殊的地方在于,这个表达式是一个函数,$eval发现它是函数时,就可以传一个参数表(在Angular中称之为locals)给它。onUpdate表达式中可以使用的参数名和它的参数值,都来自这个参数表。
使用的时候,我们可以在视图中引用这个哈希对象的某个属性作为参数,比如对于刚才的定义,视图中的on-update="updateIt(times)"所引用的times变量就来自我们刚才在callback中传入的times属性,而updateIt函数被调用时将会接收到它,参数值是3。

$scope.updateIt = function(times) {// 这里times的值应该是3,但是这个times不需要跟视图和指令中的名称一致,它叫什么都可以。但视图和指令中的名称必须一致
};
在装饰器指令中,其实还有一种细分的分支,它完全不操纵DOM,只是对当前scope进行处理,如添加成员变量、函数等。代码如下:
angular.module('com.ngnice.app').directive('twToggle', function () {return {restrict: 'A',scope: true,link: function(scope) {scope.$node = {folded: false,toggle: function() {this.folded = !this.folded;}};}};
});

使用的时候:

<ul><li ng-repeat="item in items" tw-toggle=""><span ng-click="$node.toggle()">切换</span><ul ng-if="$node.folded">...</ul></li>
</ul>

它的作用是在当前元素的作用域上创建一个名为$node的哈希对象,这个哈希对象具有一组自定义的属性和方法,可用来封装交互逻辑。
也许你已经想到了,这种类型的指令还可以进一步改进。如何改进呢?

angular.module('com.ngnice.app').directive('twToggle', function () {return {restrict: 'A',scope: true,controller: function($scope) {$scope.folded = false;$scope.toggle = function() {$scope.folded = !$scope.folded;};}};
});

它好在哪里?笔者不直接给出答案,请读者自行分析,并尝试理解它,这对于指令的认识是很重要的。

《AngularJS深度剖析与最佳实践》一2.6 指令相关推荐

  1. 《AngularJS深度剖析与最佳实践》一第1章 从实战开始

    本节书摘来自华章出版社<AngularJS深度剖析与最佳实践>一书中的第1章,作者 雪狼 破狼 彭洪伟,更多章节内容可以访问云栖社区"华章计算机"公众号查看 第1章 从 ...

  2. 《AngularJS深度剖析与最佳实践》一2.2 模块

    本节书摘来自华章出版社<AngularJS深度剖析与最佳实践>一书中的第2章,第2.2节,作者 雪狼 破狼 彭洪伟,更多章节内容可以访问云栖社区"华章计算机"公众号查看 ...

  3. 《AngularJS深度剖析与最佳实践》一2.11 消息

    本节书摘来自华章出版社<AngularJS深度剖析与最佳实践>一书中的第2章,第2.11节,作者 雪狼 破狼 彭洪伟,更多章节内容可以访问云栖社区"华章计算机"公众号查 ...

  4. 《AngularJS深度剖析与最佳实践》一2.10 承诺

    本节书摘来自华章出版社<AngularJS深度剖析与最佳实践>一书中的第2章,第2.10节,作者 雪狼 破狼 彭洪伟,更多章节内容可以访问云栖社区"华章计算机"公众号查 ...

  5. 《AngularJS深度剖析与最佳实践》一2.9 服务

    本节书摘来自华章出版社<AngularJS深度剖析与最佳实践>一书中的第2章,第2.9节,作者 雪狼 破狼 彭洪伟,更多章节内容可以访问云栖社区"华章计算机"公众号查看 ...

  6. 《AngularJS深度剖析与最佳实践》一1.3 创建项目

    本节书摘来自华章出版社<AngularJS深度剖析与最佳实践>一书中的第1章,第1.3节,作者 雪狼 破狼 彭洪伟,更多章节内容可以访问云栖社区"华章计算机"公众号查看 ...

  7. 《AngularJS深度剖析与最佳实践》一1.6 实现AOP功能

    本节书摘来自华章出版社<AngularJS深度剖析与最佳实践>一书中的第1章,第1.6节,作者 雪狼 破狼 彭洪伟,更多章节内容可以访问云栖社区"华章计算机"公众号查看 ...

  8. 《AngularJS深度剖析与最佳实践》一1.4 实现第一个页面:注册

    本节书摘来自华章出版社<AngularJS深度剖析与最佳实践>一书中的第1章,第1.4节,作者 雪狼 破狼 彭洪伟,更多章节内容可以访问云栖社区"华章计算机"公众号查看 ...

  9. 《AngularJS深度剖析与最佳实践》一1.5 实现更多功能:主题

    本节书摘来自华章出版社<AngularJS深度剖析与最佳实践>一书中的第1章,第1.5节,作者 雪狼 破狼 彭洪伟,更多章节内容可以访问云栖社区"华章计算机"公众号查看 ...

最新文章

  1. Html,xhtml,xml的定义和区别
  2. python numpy.r_ 与 numpy.c_的用法
  3. 自己动手写一个 SimpleVue
  4. c语言选择夹答案,单片机串口通讯制作说明
  5. 单点登录 之 OAuth
  6. ffmpy3与ffmpeg的简单使用
  7. android 控件置于屏幕最底端
  8. python中numpy.transpose()函数详解
  9. Image.fromarray的用法(实现array到image的转换)
  10. Eclipse安装教程
  11. python拆分PDF
  12. 和与余数的和同余理解_同余及同余特性
  13. 制作u盘winpe启动盘_如何制作U盘启动盘
  14. 台式计算机有哪些硬件,台式机包括什么
  15. PS背后的神秘AI力量 是Adobe憋了十年的神功
  16. 时钟芯片 服务器,通用实时时钟芯片
  17. Android 原生 多屏显示 (分屏) 原理 解析
  18. 最新美团java开发3轮技术面+hr面 点评(总结分析)
  19. 白魔法师-牛客小白月赛25
  20. 《炬丰科技-半导体工艺》不破坏MEMS结构的颗粒去除方法

热门文章

  1. 按键去抖动c语言编程,单片机实现电脑键盘去抖的编程设计
  2. springboot---微信小程序上传文件(word/pdf文件)
  3. CreateFont()函数的MSDN翻译
  4. java 获取 yyyymmdd_从JS日期对象获取YYYYMMDD格式的字符串?
  5. 这样的心态,值得拥有
  6. -Cannot use v-for on stateful component root element because it renders multiple elements
  7. 微信扫码登入 改变二维码样式
  8. 在html中对页面大小的设置吗,网页设计一般页面尺寸怎么设置呢?
  9. endnote添加引文格式
  10. X/Open和OSF