效果预览

来自codepen的效果预览:(引用了angularjs,已被墙,请谨慎预览)
http://codepen.io/flybywind/pen/aNjxJa

转载声明

这篇文章其实源自BenNadel的博客。在此我要分享一下自己的收获和一些更改。

主要内容

整个效果都建立在angularjs的directive里面。可以想到,作者绑定了mousemove事件,通过勾股定理计算的距离,除此之外,我还有些小收获要跟大家分享一下:

  • directive中compile和link函数的区别

  • $interpolate和$compile这2个service的区别

  • 如何把node内部包含的所有文本字母都用span包裹起来

  • relative定位和absolute定位的差别

directive中compile和link函数的区别

根据BenNadel自己在博客中的解释:

I use the compile() phase when I need to alter the structure of the DOM (Document Object Model) in a way that is based on nothing but the existing structure of DOM.

翻译成汉字就是:当我需要根据原始DOM(文档对象模型,说详细了可能看不懂,还是用简称吧~)的结构对Dom进行改变时,我使用compile函数。
道理其实也很简单,因为angluar会对Dom结构进行改变,当你进入link函数后,你看到的Dom结构可能跟你预期的不一样,找不到你要的元素了,此时很多人可能就懵圈了。在BenNadel的另一篇博客中,他就给出了一个例子:首先他在一个directive中使用了template,在transclude部分(嵌入部分)中使用了ngRepeat。他发现,只有在Compile阶段,才能访问到使用了ngRepeat的元素,并且改变它们的class;而在link阶段,是找不到的。
原因就是ngRepeat这个directive已经把那些元素从Dom树中删除了,它们会在随后被加入,但是在link阶段你是找不到的
所以,在这种可能很多人一辈子都见不到的情况下,你才必须使用compile。
在以上的例子中,BenNadel还利用priority对同一个directive进行了两阶段处理,这种奇技淫巧从来没见过,领教了。

其实很多时候,在link和compile中处理的结果是一样的,但是细节上有些差异,比如以下情况:

<span
    ng-repeat="word in [ 'Word', 'to', 'your', 'mother' ]"
    bn-compile
    bn-link>
    {{ word }}
</span>

其中bn-compile和bn-link分别是2个directive,一个在compile函数中打印出日志“compile”,并且给element增加class "c1",一个在link函数中打印出“link”,并且给element增加class "c2",c1、c2都有css样式进行展示。最终结果是生成的4个span都可以看到c1、c2的样式,但是console中compile只打印了1次,link却打印了4次。
这是因为,bn-compile是在compile阶段修改的dom,它修改的是原始Dom结构,这跟你在html文档中,手动写上 class="c1"是一样的。

当然,在link函数中,也有一些地方是compile做不到的,有的时候你可能就是要对已经部分渲染后的Dom结构进行修改,而且link函数中可以传入scope,此时你可以进行数据绑定了。这也就是下面我要讲到的。

$interpolate和$compile这2个service的区别

在BenNadel的原始文章中,他使用了compile对dom结构进行修改。但是如果我的文本内容是通过数据绑定了,如下:

<div 
    scatter-effect="#c2"
    scatter-distance="1000"
    class="tagline">
    {{hello}}
</div>

特效就失败了,如下:
图片描述

第二个div因为绑定的是动态数据,最终是以模板形式显示的。
为了弥补这个缺陷,我决定改成在link函数中对Dom进行操作。其实就是把BenNadel在compile中的代码放到link最开始执行就行了。另外,还需要对{{}}进行手动编译,此时$interpolate就要闪亮登场了。
以下是我的部分js代码:

  angular.module("Demo").directive(
        "scatterEffect",
// $compile是把字符串编译为element,所以字符串必须是dom形式
// $interpolate是把表达式和data 绑定到一起,$compile中会调用$interpolate
    function ($document, $interpolate) {
        return ({
            link: link,
            restrict: "A"
        });

    function link(scope, element, attributes) {
       // 如果是compile,则无法获取scope
       var realContent = $interpolate(element.html())(scope);
       element.empty().append(realContent);
       // 这是原来compile中的函数,element处理后,再放到link中调用
       wrapLetters(element);
           /*  other code  */
    }
 })

在以上代码中,element.html()返回的是字符串,比如在处理第一个div时,就是Happy Friday You <em>Beautiful</em> People!,第二个div就是{{hello}},开始我尝试用$compile编译这个字符串,结果控制台显示 unrecognized expression: Happy Friday You <em>Beautiful</em> People!以及unrecognized expression: {{hello}} !问了一下google大叔,发现原来要用$interpolate才行。
$compile是把字符串编译为element,所以字符串必须是dom形式;$interpolate是把表达式和data绑定到一起,$compile中会调用$interpolate,后者只是前者的子过程。显然,在我这个情况下,用$interpolate才行。
其实Happy Friday You <em>Beautiful</em> People!扔到chrome,会自动给你加html和body使其正常显示,但是angular没法替你加,加上就乱套了。所以$compile必须接受标准的html语句,此处可以用正则判断一下。我就懒得写了。

如何把node内部包含的所有文本字母都用span包裹起来

这句话尽管很长,但是做起来,其实也很难(⊙﹏⊙)b。在此我只能奉上大神的代码和我的膝盖了,谨和诸君分享:

            function findTextNodes(parent) {
                // node是一个DOMElement, map返回一个jQuery对象,所以要想获得array可以使用toArray() or get()
                /* 我的注释
                    If the node is an element node, the nodeType property will return 1.
                    If the node is an attribute node, the nodeType property will return 2.
                    If the node is a text node, the nodeType property will return 3.
                    If the node is a comment node, the nodeType property will return 8.
                */
                // 此处的angular.element就是jQuery
                var textNodes = angular.element(parent).contents().map(
                    function operator(i, node) {
                        return ((node.nodeType === 1) ? findTextNodes(node) : node);

                    }
                );

                return (textNodes.toArray());

            }

            // I find and wrap each text-based letter, within the given parent, 
            // in its own Span tag.
            function wrapLetters(parent) {

                findTextNodes(parent).forEach(
                    function(node) {
                        // Replace each individual letter with a Span tag.         
                        var wrappedHtml = node.nodeValue.replace(/(\S)/g, "<span class='scatter-item'>$1</span>");
                        // 最值得推敲的在这里:
                        var fragment = angular.element(document.createDocumentFragment())
                            .append(wrappedHtml);

                        node.parentNode.insertBefore(fragment[0], node);
                        node.parentNode.removeChild(node);
                        // 以下方法不行:jquery生成的元素列表默认丢弃了首尾空格!
                        //$(wrappedHtml).insertBefore(node);
                        //node.parentNode.removeChild( node );
                    }
                );

            }

其实提取textNode,然后正则替换,最后插入原来的位置,听起来不难,但是如果利用jQuery把字符串转换为dom,那么两头的空格就会丢掉,比如Happy Friday You 这个node,正则替换后wrappedHtml是这样的:
图片描述

jQuery转换后,前后的空格都没了。所以不能用它来转换。
(哪位大神如果知道有可以控制jquery不丢空格的方法,请不吝赐教!)
最后,BenNadel大神也只好使用原生js进行dom树的替换了。

relative定位和absolute定位的差别

下面我们说点关于css的知识。我经常对absolute元素进行top、left、right、bottom定位,我也知道这样做之前,父元素必须用relative,但是我还真不知道原来relative元素也可以用TLRB这四兄弟进行操作!真是丢人,确实需要反思啊。下面附上英文解释和中文对照翻译:

static Default value. Elements render in order, as they appear in the document flow
absolute The element is positioned relative to its first positioned (not static) ancestor element
fixed The element is positioned relative to the browser window
relative The element is positioned relative to its normal position, so "left:20px" adds 20 pixels to the element's LEFT position
initial Sets this property to its default value.

static是position的默认值,什么都不加就是那种效果;
absolute,大家最熟悉,TLRB指定的是和最近的relative父元素之间的相对位置
fixed,和浏览器窗口的相对位置,也比较简单。
重点来了,relative是和初始位置的相对位置。这也正是relative这个名字的含义,以前都没仔细想过。。。
initial就是元素的初始值。有些元素的初始值可能不是static。反正各种奇葩的元素都可能,或者将来可能出现。
所以,总结一下,absolute、fixed和relative都是position类型,只是相对的位置不同而已。
BenNadel正是利用了relative的这个特性,给每个span元素都设置为:

span.scatter-item {
    position: relative ;
    z-index: 100 ;
}

然后,在mousemove事件中,调整top、left的值,使它们在鼠标距离目标元素远的时候,偏离自己的位置;在鼠标进入目标元素时,重新回到自己的位置。
(z-index好像没用~~)

结语

终于码完了


flybywind
1.1k 声望38 粉丝