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

简介:

本节书摘来自华章出版社《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;
            };
        }
    };
});

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

相关文章
|
前端开发 JavaScript
《AngularJS深度剖析与最佳实践》推荐序
《AngularJS深度剖析与最佳实践》推荐序
|
JavaScript 前端开发 定位技术
|
安全 前端开发 测试技术
《AngularJS深度剖析与最佳实践》一导读
现在,软件不但运行在PC上,还要运行在智能手机上,运行在各种Pad上,屏幕分辨率更是多到让研发和测试工程师发怵的地步。
1401 0