16. AngularJS与其它框架的混用(jQuery, Dojo)
这个问题似乎很多人都关心,但是事实是,如果了解了 ng 的工作方式,这本来就不是一个问题了。
在我自己使用 ng 的过程当中,一直是混用 jQuery 的,以前还要加上一个 Dojo 。只要了解每种框架的工作方式,在具体的代码中每个框架都做了什么事,那么整体上控制起来就不会有问题。
回到 ng 上来看,首先对于 jQuery 来说,最开始说提到过,在 DOM 操作部分, ng 与 jQuery 是兼容的,如果没有 jQuery , ng 自己也实现了兼容的部分 API 。
同时,最开始也提到过, ng 的使用最忌讳的一点就是修改 DOM 结构——你应该使用 ng 的模板机制进行数据绑定,以此来控制 DOM 结构,而不是直接操作。换句话来说,在不动 DOM 结构的这个前提之下,你的数据随便怎么改,随便使用哪个框架来控制都是没问题的,到时如有必要使用 $scope.$digest()
来通知 ng 一下即可。
下面这个例子,我们使用了 jQuery 中的 Deferred ( $.ajax
就是返回一个 Deferred ),还使用了 ng 的 $timeout
,当然是在 ng 的结构之下:
1 |
|
再把 Dojo 加进来看与 DOM 结构相关的例子。之前说过,使用 ng 就最好不要手动修改 DOM 结构,但这里说两点:
对于整个页面,你可以只在局部使用 ng ,不使用 ng 的地方你可以随意控制 DOM 。
如果 DOM 结构有变动,你可以在 DOM 结构定下来之后再初始化 ng 。
下面这个例子使用了 AngularJS , jQuery , Dojo :
1 |
|
17. 自定义过滤器
先来回顾一下 ng 中的一些概念:
module ,代码的组织单元,其它东西都是在定义在具体的模块中的。
app ,业务概念,可能会用到多个模块。
service ,仅在数据层面实现特定业务功能的代码封装。
controller ,与 DOM 结构相关联的东西,即是一种业务封装概念,又体现了项目组织的层级结构。
filter ,改变输入数据的一种机制。
directive ,与 DOM 结构相关联的,特定功能的封装形式。
上面的这几个概念基本上就是 ng 的全部。每一部分都可以自由定义,使用时通过各要素的相互配合来实现我们的业务需求。
我们从最开始一致打交道的东西基本上都是 controller 层面的东西。在前面,也介绍了 module 和 service 的自定义。剩下的会介绍 filter 和 directive 的定义。基本上这几部分的定义形式都是一样的,原理上是通过 provider 来做注入形式的声明,在实际操作过程中,又有很多 shortcut 式的声明方式。
过滤器的自定义是最简单的,就是一个函数,接受输入,然后返回结果。在考虑过滤器时,我觉得很重要的一点: 无状态 。
具体来说,过滤器就是一个函数,函数的本质含义就是确定的输入一定得到确定的输出。虽然 filter 是定义在 module 当中的,而且 filter 又是在 controller 的 DOM 范围内使用的,但是,它和具体的 module , controller , scope 这些概念都没有关系(虽然在这里你可以使用 js 的闭包机制玩些花样),它仅仅是一个函数,而已。换句话说,它没有任何上下文关联的能力。
过滤器基本的定义方式:
1 | var app = angular.module('Demo', [], angular.noop); |
上面的代码定义了一个叫做 map 的过滤器。使用时:
1 | <p>示例数据: {{ a|map }}</p> |
过滤器也可以带参数,多个参数之间使用 : 分割,看一个完整的例子:
1 | <div ng-controller="TestCtrl"> |
18. 自定义指令directive
这是 ng 最强大的一部分,也是最复杂最让人头疼的部分。
目前我们看到的所谓“模板”系统,只不过是官方实现的几个指令而已。这意味着,通过自定义各种指令,我们不但可以完全定义一套“模板”系统,更可以把 HTML 页面直接打造成为一种 DSL (领域特定语言)。
18.1. 指令的使用
使用指令时,它的名字可以有多种形式,把指令放在什么地方也有多种选择。
通常,指令的定义名是形如 ngBind 这样的 “camel cased” 形式。在使用时,它的引用名可以是:
ng:bind
ng_bind
ng-bind
x-ng-bind
data-ng-bind
你可以根据你自己是否有 “HTML validator” 洁癖来选择。
指令可以放在多个地方,它们的作用相同:
<span my-dir="exp"></span>
作为标签的属性<span class="my-dir: exp;"></span>
作为标签类属性的值<my-dir></my-dir
> 作为标签<!-- directive: my-dir exp -->
作为注释
这些方式可以使用指令定义中的 restrict 属性来控制。
可以看出,指令即可以作为标签使用,也可以作为属性使用。仔细考虑一下,这在类 XML 的结构当中真算得上是一种神奇的机制。
18.2. 指令的执行过程
ng 中对指令的解析与执行过程是这样的:
浏览器得到 HTML 字符串内容,解析得到 DOM 结构。
ng 引入,把 DOM 结构扔给 $compile 函数处理:
找出 DOM 结构中有变量占位符
匹配找出 DOM 中包含的所有指令引用
把指令关联到 DOM
关联到 DOM 的多个指令按权重排列
执行指令中的 compile 函数(改变 DOM 结构,返回 link 函数)
得到的所有 link 函数组成一个列表作为 $compile 函数的返回
执行 link 函数(连接模板的 scope)。
18.3. 基本的自定义方法
自定义一个指令可以非常非常的复杂,但是其基本的调用形式,同自定义服务大概是相同的:
1 | <p show style="font-size: 12px;"></p> |
如果在 directive 中直接返回一个函数,则这个函数会作为 compile 的返回值,也即是作为 link 函数使用。这里说的 compile 和 link 都是一个指令的组成部分,一个完整的定义应该返回一个对象,这个对象包括了多个属性:
priority
terminal
scope
controller
require
restrict
template
templateUrl
replace
transclude
compile
link
上面的每一个属性,都可以单独探讨的。
下面是一个完整的基本的指令定义例子:
1 | <div> |
上面这个自定义的指令,做的事情就是解析节点中的文本内容,然后修改它,再把生成的新内容填充到节点当中去。其间还涉及了节点属性值 lines 的处理。这算是指令中最简单的一种形式。因为它是“一次性使用”,中间没有变量的处理。比如如果节点原来的文本内容是一个变量引用,类似于 ,那上面的代码就不行了。这种情况麻烦得多。后面会讨论。
18.4. 属性值类型的自定义
官方代码中的 ng-show 等算是我说的这种类型。使用时主要是在节点加添加一个属性值以附加额外的功能。看一个简单的例子:
1 | <p color="red">有颜色的文本</p> |
我们定义了一个叫 color 的指令,可以指定节点文本的颜色。但是这个例子还无法像 ng-show 那样工作的,这个例子只能渲染一次,然后就无法根据变量来重新改变显示了。要响应变化,我们需要手工使用 scope 的 $watch
来处理:
1 | <div ng-controller="TestCtrl"> |
18.5. Compile的细节
指令的处理过程,是 ng 的 Compile 过程的一部分,它们也是紧密联系的。继续深入指令的定义方法,首先就要对 Compile 的过程做更细致的了解。
前面说过, ng 对页面的处理过程:
浏览器把 HTML 字符串解析成 DOM 结构。
ng 把 DOM 结构给 $compile ,返回一个 link 函数。
传入具体的 scope 调用这个 link 函数。
得到处理后的 DOM ,这个 DOM 处理了指令,连接了数据。$compile
最基本的使用方式:
1 | var link = $compile('<p>{{ text }}</p>'); |
上面的 $compile
和 link 调用时都有额外参数来实现其它功能。先看 link 函数,它形如:
function(scope[, cloneAttachFn]
第二个参数 cloneAttachFn 的作用是,表明是否复制原始节点,及对复制节点需要做的处理,下面这个例子说明了它的作用:
1 | <div ng-controller="TestCtrl"></div> |
cloneAttachFn 对节点的处理是有限制的,你可以添加 class ,但是不能做与数据绑定有关的其它修改(修改了也无效):
1 | app.controller('TestCtrl', function($scope, $compile){ |
修改无效的原因是,像 这种所谓的 Interpolate 在
$compile
中已经被处理过了,生成了相关函数(这里起作用的是 directive 中的一个 postLink 函数),后面执行 link 就是执行了 $compile
生成的这些函数。当然,如果你的文本没有数据变量的引用,那修改是会有效果的。
前面在说自定义指令时说过, link 函数是由 compile 函数返回的,也就像前面说的,应该把改变 DOM 结构的逻辑放在 compile 函数中做。
$compile
还有两个额外的参数:
$compile(element, transclude, maxPriority);
maxPriority 是指令的权重限制,这个容易理解,后面再说。
transclude 是一个函数,这个函数会传递给 compile 期间找到的 directive 的 compile 函数(编译节点的过程中找到了指令,指令的 compile 函数会接受编译时传递的 transclude 函数作为其参数)。
但是在实际使用中,除我们手工在调用 $compile
之外,初始化时的根节点 compile 是不会传递这个参数的。
在我们定义指令时,它的 compile 函数是这个样子的:
function compile(tElement, tAttrs, transclude) { ... }
事实上, transclude 的值,就是 directive 所在的 原始 节点,把原始节点重新做了编译之后得到的 link 函数(需要 directive 定义时使用 transclude 选项),后面会专门演示这个过程。所以,官方文档上也把 transclude 函数描述成 link 函数的样子(如果自定义的指令只用在自己手动 $compile
的环境中,那这个函数的形式是可以随意的):
{function(angular.Scope[, cloneAttachFn]}
所以记住,定义指令时, compile 函数的第三个参数 transclude ,就是一个 link ,装入 scope 执行它你就得到了一个节点。
18.6. transclude的细节
transclude 有两方面的东西,一个是使用 $compile
时传入的函数,另一个是定义指令的 compile 函数时接受的一个参数。虽然这里的一出一进本来是相互对应的,但是实际使用中,因为大部分时候不会手动调用 $compile
,所以,在“默认”情况下,指令接受的 transclude 又会是一个比较特殊的函数。
看一个基本的例子:
1 | var app = angular.module('Demo', [], angular.noop); |
我们定义了一个 more 指令,它的 compile 函数的第三个参数,就是我们手工 $compile
时传入的。
如果不是手工 $compile
,而是 ng 初始化时找出的指令,则 transclude 是一个 link 函数(指令定义需要设置 transclude 选项):
1 | <div more>123</div> |
18.7. 把节点内容作为变量处理的类型
回顾最开始的那个代码显示的例子,那个例子只能处理一次节点内容。如果节点的内容是一个变量的话,需要用另外的思路来考虑。这里我们假设的例子是,定义一个指令 showLenght ,它的作用是在一段文本的开头显示出这段节点文本的长度,节点文本是一个变量。指令使用的形式是:
1 | <div ng-controller="TestCtrl"> |
从上面的 HTML 代码中,大概清楚 ng 解析它的过程(只看 show-length 那一行):
解析 div 时发现了一个 show-length 的指令。
如果 show-length 指令设置了 transclude 属性,则 div 的节点内容被重新编译,得到的 link 函数作为指令 compile 函数的参数传入。
如果 show-length 指令没有设置 transclude 属性,则继续处理它的子节点( TextNode )。
不管是上面的哪种情况,都会继续处理到 这段文本。
发现 是一个 Interpolate ,于是自动在此节点中添加了一个指令,这个指令的 link 函数就是为 scope 添加了一个
$watch
,实现的功能是是当 scope 作 $digest
的时候,就更新节点文本。
与处理 时添加的指令相同,我们实现 showLength 的思路,也就是:
修改原来的 DOM 结构
为 scope 添加 $watch
,当 $digest
时修改指定节点的文本,其值为指定节点文本的长度。
代码如下:
1 | app.directive('showLength', function($rootScope, $document){ |
上面代码中,因为设置了 transclude 属性,我们在 showLength 的 link 函数(就是 return 的那个函数)中,使用 func 的第三个函数来重塑了原来的文本节点,并放在我们需要的位置上。然后,我们添加自己的节点来显示长度值。最后给当前的 scope 添加 $watch
,以更新这个长度值。
关注 web翎云阁,定时推送,互动精彩多,若你有更好的见解,欢迎留言探讨!