AngularJS Performance

02 Dec 2014

艰难历程

系统里面使用AngularJS 做了一个类似Trello 的看板页面, 用来显示任务列表. 但是发现, 打开任务详情几次之后, 就非常卡了.

  1. 最初以为是看板里面元素太多, 造成ng-repeat 性能下降引起的问题. 于是升级了一下Angular 的版本到1.3, 并使用其中的bind once 的特性来绑定页面上那些不会再变化的内容.

  2. 尝试使用Developer Tool 里面的Profile, 结果不会用. 看不出对象是不是释放掉了.

    对象太多, 完全找不到了

    图: 对象太多, 完全找不到了

  3. 使用了个笨办法, 在任务的对象放一个很大的数组.

    function alloc() {
        var largeObject = [];
        for (var i = 0; i < 1000000; i++) {
            largeObject.push(10000000 * i + "?");
        }
        return largeObject;
    }
    $scope.task.largeArray = alloc();
    

    这下效果明显了. 然后就发现, 任务详情的窗口关闭之后, task 对象还一直存在.

    Task 对象不能被释放

    图: Task 对象不能被释放

    一开始还以为是Profile 的时候没有GC 的关系, 但是后来看文档发现, 进行Profile 会自动先进行GC.

  4. 是什么引起task 对象不能释放呢? 首先怀疑是$modal. 会不会是这里面有什么bug?

    于是写了个简单的页面来进行测试:

    $scope.modal = function modal() {
        $modal.open({
            templateUrl: 'angular/views/modal.html',
            controller: function ModalCtrl($scope, $modalInstance) {
                $scope.modalLargeObject = alloc();
    
                $scope.close = function close() {
                    $modalInstance.close($scope.modalLargeObject);
                };
            }
        }).result.then(function() {
            console.info('close');
        }, function() {
            console.info('dismiss');
        });
    };
    

    多次弹出Modal 的Profile

    图: 多次弹出Modal 的Profile

  5. 对Profile 界面上的几个列不太明白是干啥的, 找了一下:

    Shallow size

    This is the size of memory that is held by the object itself.

    对象本身占用内存的大小,不包含对其他对象的引用.

    Retained size

    This is the size of memory that is freed once the object itself is deleted along with its dependent objects that were made unreachable from GC roots.

    该对象被 GC 之后所能回收到内存的总和.

    从 obj1 入手,图中蓝色节点代表仅仅只有通过 obj1 才能直接或间接访问的对象。因为可以通过 GC Roots 访问,所以左图的 obj3 不是蓝色节点;而在右图却是蓝色,因为它已经被包含在 retained 集合内。

    所以对于左图, obj1 的 retained size 是 obj1, obj2, obj4 的 shallow size 总和;右图的 retained size 是 obj1, obj2, obj3, obj4 的 shallow size 总和。 obj2 的 retained size 可以通过相同的方式计算。

  6. 那到底是什么原因, 使得Task 不能被释放呢? 该如何定位问题?

    我还是使用最笨的办法, 先把任务详情的html 和Controller 里面的内容都注释掉, 然后一点点放开, 看到底是哪一部分使得内存无法释放.

    经过好长好长好长...一段时间的折腾, 终于发现问题点:

    // 好多个directive 都有这样的代码
    element.parents().click(function(){
        $scope.closePanel();
        $scope.$apply();
    });
    

    这里面的问题是什么?

    给element 的所有parent (包括document.body) 都绑定了click 事件. 那么Modal 关闭之后, 这个事件绑定还存在, 这样的话, 这个事件引用到的资源就无法释放了.

    这里还涉及到闭包.

    Closures / 闭包

    Closures are functions that refer to independent (free) variables.

    In other words, the function defined in the closure 'remembers' the environment in which it was created.

    function startAt(x) {
        function incrementBy(y) {
            return x + y;
        }
        return incrementBy;
    }
    
    var closure1 = startAt(1);
    var closure2 = startAt(5);
    

    既然发现问题, 于是进行调整:

    function closeHandler = function(){
        $scope.closePanel();
        $scope.$apply();
    };
    var parents = element.parents();
    parents.on("click", closeHandler);
    
    $scope.$on("$destroy", function() {
        // 当$scope 销毁的时候, 解除这些事件绑定
        parents.off("click", closeHandler);
    })
    

    把这样的都修改完, 然后进行测试, 内存基本上比较平稳了. Profile 也不会出现大量无法释放的Task 对象. 观察Chrome 的Task Manager 里面的JavaScript Memory 变化, 内存能够得到释放(上涨了一段之后, 会进行回收, 释放后的内存使用量和开始时差不多).

    Task Manager 的JavaScript Memory

    图: Task Manager 的JavaScript Memory

另一个性能问题

任务列表里面, 点击任务名称, 要等很长时间(1 分多钟)才会弹出任务详情.

这个感觉跟内存没有关系了, 于是尝试使用Developer Tool 里面的Timeline 来看这一段时间发生了什么事.

TaskName 花了很多时间执行

图: TaskName 花了很多时间执行

看下代码:

$timeout(function() {
    $scope.isMultiRow = elem.height() > LINE_HEIGHT;
    Tasks.length = 0;
}, 0);

看来是因为任务数量多的时候这里触发太多次消息循环了($timeout 默认情况下会触发AngularJS 的消息循环).

这个代码和一个界面设计有关, 在和需求商量之后, 把这个$timeout 处理去掉了.

如果$timeout 里面的代码不需要触发消息循环, 那么应该给$timeout 调用加上第三个参数, 并传递false 过去.

$timeout(function() {
    // code
}, 0, false);

几个小技巧

  1. 在Developer Tool 的console 面板里面, 可以用$0, $1, ..., $4, 来引用在element 面板最近点击到的元素.

    在element 上选取元素

    图: 在element 上选取元素

    在console 上使用$0 获取dom

    图: 在console 上使用$0 获取dom

  2. console.time

    console.time("Array initialize");
    var array= new Array(1000000);
    for (var i = array.length - 1; i >= 0; i--) {
        array[i] = new Object();
    };
    console.timeEnd("Array initialize");
    

    console.time 输出

    图: console.time 输出

参考
  1. https://developer.chrome.com/devtools/docs/javascript-memory-profiling

  2. http://www.cnblogs.com/Wayou/p/chrome-console-tips-and-tricks.html

  3. http://www.slideshare.net/gonzaloruizdevilla/finding-and-debugging-memory-leaks-in-javascript-with-chrome-devtools

  4. http://en.wikipedia.org/wiki/Closure_(computer_programming)

  5. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures

  6. http://www.ibm.com/developerworks/web/library/wa-memleak

...

jQuery UI Sortable 使用介绍

29 Nov 2013

什么是Sortable

Enable a group of DOM elements to be sortable. Click on and drag an element to a new spot within the list, and the other items will adjust to fit.

允许一组DOM元素可排序. 点击并拖动元素到列表中的新位置, 而其他元素将调整以适应.

先来一个简单的示例 - 一个任务列表

  1. 页面内容

    <h4>重要</h4>
    <div class="list-group" data-priority="2">
        <div data-id="" ng-repeat="item in items | filter : {priority : 2}" class="row alert alert-warning">
            <div class="col-xs-8 item-content"></div>
            <div class="col-xs-4 text-right text-muted small"></div>
        </div>
    </div>
    <h4>普通</h4>
    <div class="list-group" data-priority="1">
        <div data-id="" ng-repeat="item in items | filter : {priority : 1}" class="row alert alert-success">
            <div class="col-xs-8"></div>
            <div class="col-xs-4 text-right text-muted small"></div>
        </div>
    </div>
    
  2. 脚本

    $('.list-group').sortable({
        connectWith: '.list-group', // 可以拖动其他的列表
        cursor: 'move', // 鼠标指针
        placeholder: 'alert alert-info', // 放置位置的样式
        revert: 20, // 松开后的动画时长
        handle: '.item-content', // 可以拖动的元素, 这里要注意, 上面第二个列表没有包含.item-content, 这就导致第二个列表不能拖动了.
    })
    // 禁用一下选择, 避免拖动时把文本也选中了.
    .disableSelection();
    

简单解析

  1. connectWith

    A selector of other sortable elements that the items from this list should be connected to.

    如果需要将当前列表的元素, 拖动到其他列表去, 则需要用这个选项来指定"其他列表" 的selector. 取值为:selector. 例如上面的:

    connectWith: '.list-group'
    

    需要注意的是, 这个只是单向的, 也就是说如果"其他列表" 的元素要能够拖放到当前列表, "其他列表" 也需要进行这样的设置才行.

  2. cursor

    Defines the cursor that is being shown while sorting.

    拖动时的鼠标指针, 这里直接是样式值, 而不是css class. 默认是auto(也就是普通指针). 比如下面将鼠标指针改为"移动" 的样式:

    cursor: 'move'
    
  3. placeholder

    A class name that gets applied to the otherwise white space.

    在拖动时, 元素可以放置的位置的样式. 比如:

    placeholder: 'alert alert-info'
    
  4. revert

    Whether the sortable items should revert to their new positions using a smooth animation.

    定义拖动的元素在松开后, 到达新位置的时长. 可能的取值是:

    + false (默认值), 则没有时长, 直接到达新位置.
    
    + true, 则使用默认的时长.
    
    + 数值, 使用指定的时长, 单位是毫秒(milliseconds)
    

    例如:

    revert: 20
    
  5. handle

    Restricts sort start click to the specified element.

    限制可以拖动的位置, 限定为指定的元素. 取值为: selector / element. 比如:

    handle: '.item-content'
    

    这里要注意的是, 如果可以排序的元素里面没有这个限定的元素(比如上面示例中的第二个列表), 则没办法再进行排序了.

保存结果

上面的例子, 只是可以进行列表的排序. 但是我们往往需要将排序结果保存起来. 这个可以通过toArray 方法进行处理. 直接上代码:

$('.list-group').on('sortstop', function(event, ui) {
    var $this = $(this);
    // 由于在上面的列表, 通过data-id 来指定id 的, 所以要传递参数
    // 如果是使用id, 则直接可以使用: $(this).sortable('toArray')
    var idArr = $this.sortable('toArray', {attribute : 'data-id'});

    $.post(saveOrderUrl, {
        idArr: idArr
    }, function(result) {
        // ...
        if (result.isOk == false) {
            // 保存失败了, 还原吧...
            $this.sortable('cancel');
            // 可能还需要提示一下, 因为还原的效果很不明显.
            // ...
        }
    });
});

这里, 直接在stop 事件进行保存的, 并且在保存失败时, 取消当前的排序操作, 把拖动的元素还原回原来的位置.

再来看一下这里面的东东

  1. stop 事件

    排序已经停止时触发的事件. 方法的参数

    在jQuery UI, 事件的绑定, 可以使用上面那种方式, 在初始化之后进行设置. 也可以直接在初始化的时候进行, 比如上面的stop 事件也可以这样绑定:

    $('.list-group').sortable({
        // ...
        stop: function(event, ui) {
            // ...
        },
    }
    
  2. toArray 方法

    Serializes the sortable's item id's into an array of string.

    返回一个字符串的数组, 保存可排序的元素的id(默认是获取id), 比如:

    $('.list-group').sortable('toArray')
    // 由于上面没有id 属性, 则会返回类似这样的数组: ['', '', '']
    

    如果要获取其他属性, 则可通过参数来指定. 例如上面例子中用来获取data-id 属性.

    另外, 需要注意 的是, jQuery UI 的方法调用, 是通过下面的形式进行的:

    $('.list-group').sortable('方法名', '方法参数')
    

    而不是通常的:

    $('.list-group').方法名('方法参数')
    
  3. cancel 方法

    Cancels a change in the current sortable and reverts it to the state prior to when the current sort was started. Useful in the stop and receive callback functions.

    取消当前的排序操作, 并将元素还原到拖动之前的位置. 例如上面例子中保存失败时的处理.

再来扩展一下

现在在上面的例子基础上, 扩展一下, 想限制一些元素的排序, 比如第一个列表中符合一定条件(这里假定是data-id < 10 ) 的元素不能放到第二个列表.

首先很快就可以想到, 完全可以用上面的方法, 在stop 事件里面, 增加判断, 如果符合条件, 则调用cancel 方法取消排序操作.

但能不能直接就限制这样的元素, 不给在第二个列表放置呢?

首先再来看一下Sortable 的选项, 看有没有选项能达成限制的需求. 翻阅文档, 有两个和可排序的元素有关:

  • items: 指定列表中可以拖动排序的元素, 取值为selector, 默认是> *(列表的所有直接子元素)
  • cancel: 限制一些元素的拖动, 取值为selector, 默认为input,textarea,button,select,option. 当有一些元素是和可以拖动的元素是同一级别的DOM 时, 通过这个选项来排除这些元素的拖动.

但仔细分析一下, 这些都不是我们想要的(这些都是针对整个列表的). 那只能自己写代码控制了.

从前面的介绍也知道, connectWith 是设置排序可以放到其他列表去的. 现在要限制第一个列表的元素, 那么很自然的思路就是, 我在开始拖动的时候, 判断拖动的元素, 是否可以放到第二个列表, 如果不能, 则将第二个列表从connectWith 中排除掉:

$('.list-group:eq(0)').on('sortstart', function(event, ui) {
    var $this = $(this);
    var dataId = ui.item.data('id');
    var oldConnectWith = $this.data('connectWith');
    if (dataId < 10 && oldConnectWith == null) {
        var connectWith = $this.sortable('option', 'connectWith');
        $this.data('connectWith', connectWith);
        $this.sortable('option', 'connectWith', $(connectWith).not('[data-priority="1"]'));
    } else if (dataId >= 10 && oldConnectWith) {
        $this.sortable('option', 'connectWith', oldConnectWith);
        $this.removeData('connectWith');
    }
});

先来看一下

暂时先不管这段代码是否可以达到要求, 先看看这个例子里面的东东. 首先就是

  1. option 方法

    这个或者可以说是最重要的一个方法了. 这个方法类似jQuery 的css 等方法, 有下面这样几种调用方式:

    • option(optionName): 获取指定option 的值.

    • option(optionName, optionValue): 设置option 的值.

    • option(options): 和上面的类似, 都是设置option 值,

      但是这个方式可以支持多个option 的设置, 例如:

      $('.list-group').sortable('option', {
          disable : true // 禁用排序
      });
      
    • option(): 获取所有的option, 返回一个很长串的对象. 实际意义并不是很大, 调试的时候会有些用.
  2. start 事件

    拖动开始时触发的事件.

    有问题啊

    再回过头运行上面的代码, 会发现有问题, 设置connectWith 的效果要到下一个元素的排序才有用, 当前的拖动不起作用.

    经过一番分析, 查看源代码, 发现在start 事件里面, 元素可以放置的列表已经通过connectWith 属性设置计算好了. 这个时候再进行设置, 当然就只有下一次才起作用了.

    不过幸好还有补救的办法, 先来看一下修改后的代码:

    $('.list-group:eq(0)' ).on('sortstart', function(event, ui) {
        var $this = $(this);
        var dataId = ui.item.data('id');
        var oldConnectWith = $this.data('connectWith');
        if (dataId < 10 && oldConnectWith == null) {
            var connectWith = $this.sortable('option', 'connectWith');
            $this.data('connectWith', connectWith);
            $this.sortable('option', 'connectWith', $(connectWith).not('[data-priority="1"]'));
            // 增加这个方法的调用
            $this.sortable('refresh');
        } else if (dataId >= 10 && oldConnectWith) {
            $this.sortable('option', 'connectWith', oldConnectWith);
            // 增加这个方法的调用
            $this.sortable('refresh');
            $this.removeData('connectWith');
        }
    });
    

    这里主要是增加对refresh 方法的调用. 这个效果可以再http://embed.plnkr.co/OHXmZ3/preview 看到

  3. refresh

    Refresh the sortable items. Triggers the reloading of all sortable items, causing new items to be recognized.

    刷新可排序的元素, 重新加载所有可排序的元素, 使得新的元素可以被识别. 这个方法没有参数.

    查看源代码发现这个方法会重新计算可以排序的元素, 也会计算可以放置的列表. 在上面调用这个方法, 重新刷新一下.

OK, 差不多就这样了. 末了, 再补充一点点

  1. 其他一些选项

    • disabled: 取值为boolean, 如果设置为true, 则直接禁用整个列表的排序功能.

    • helper: 在拖动时, 跟随鼠标移动的元素, 取值为: original (默认值, 直接拖动元素), clone(复制一个新的元素), function(){...}(使用函数返回的元素).

    • axis: 可以指定为x 或y, 来限定只能进行一个方向上的拖动

    • zIndex: 设置拖动元素的z-index, 取值为数值, 默认是1000

  2. 从选项可以看出的一点东西

    从上面的选项, 可以看出, Sortable 在进行拖动排序时:

    • 根据items / cancel / handle 属性的设置, 给列表的元素绑定拖动事件.

    • 在进行拖动时, 根据helper 设置处理跟随鼠标拖动的元素. 并根据当前鼠标的位置, 以及connectWith 设置, 计算可以进行放置的位置, 并在该位置创建placeholder, 如果有设置placeholder 样式, 则会将该样式应用上去.

    • 松开鼠标时, 将元素放到当前placeholder 的位置.

  3. 关于事件

    Sortable 有不少事件, 前面也介绍了其中的start 和stop. 不过Sortable 还有不少其他事件.

    通过测试, 如果是在列表内容拖动, 则是这样一个过程:

    • start --> 拖动开始时触发.

    • activate --> 在start 事件之后, 拖动的元素所有可以放置的列表都会触发这个事件. 这里所有可以放置的列表: 首先是元素本身所在的列表, 另外, 如果指定了connectWith 参数, 则还包括connectWith 参数指定的所有列表.

    • sort --> 在拖动时会不停触发, 意义不大.

    • change --> 当元素的位置发生变化时触发, 会有多次产生(只要放置的位置变了就触发, 也就是说会触发多次, 感觉实际意义不大).

    • beforeStop --> 停止之前, 在helper /placeholder 还存在时触发(我的理解是在刚刚松开鼠标时触发的).

    • update --> 在停止排序, 并且元素位置已经改变之后触发.

    • deactivate --> 和activate 类似, 所有可以放置的列表都会触发这个事件.

    • stop --> 排序已经停止时触发, 可以通过cancel 方法取消当前的排序操作.

    如果在在多个列表之间拖动, 则还会再触发over (进入到另外一个列表), out(从一个列表离开), remove /receive(元素从一个列表放到另一个列表时, 这两个列表分别触发的事件) 这些事件.

再其他的, 自己看API 了.

...

Flickr 首页效果分析

07 Nov 2013

Flickr(http://www.flickr.com),雅虎旗下图片分享网站。为一家提供免费及付费数位照片储存、分享方案之线上服务,也提供网络社群服务的平台。其重要特点就是基于社会网络的人际关系的拓展与内容的组织。这个网站的功能之强大,已超出了一般的图片服务,比如 图片服务、联系人服务、组群服务。

首页截图1

首页截图1

首页截图2

首页截图1

首页截图3

首页截图1

这里有几个有趣的功能:

  1. 截图3中的弹出的注册提示框,这个是在页面滚动到一定程度后才会出现的。

    function checkCTA() {
        // show or hide CTA, depending on whether user is looking at the Justified view.
        var frameContentInView = frameContent.vis.isVisible();
    
        if (!ctaVisible && !frameContentInView) {
                ctaWrapper.addClass('transitioning');
                window.setTimeout(function() {
                    ctaWrapper.removeClass('hidden');
                    ctaWrapper.addClass('visible');
                    ctaVisible = true;
                }, 20);
    
                window.setTimeout(function() {
                    ctaWrapper.removeClass('transitioning');
                }, 300);
    
        } else if (ctaVisible && frameContentInView) {
            ctaWrapper.addClass('transitioning');
            ctaWrapper.removeClass('visible');
            window.setTimeout(function() {
                // additional CSS to actually hide element, post-transition
                if (!ctaVisible) {
                    ctaWrapper.addClass('hidden');
                }
    
                // in either event, remove transitioning.
                ctaWrapper.removeClass('transitioning');
            }, 300);
    
            ctaVisible = false;
        }
    }
    

    注意到显示和消失的过程中,有一个很短的动画,这是通过下面的CSS来实现的。

    #huge-cta-wrapper .bd {
        position: relative;
        display: inline-block;
    
        /* lazy transition-all */
        -moz-transition: all 0.225s ease-out;
        -ms-transition: all 0.225s ease-out;
        -webkit-transition: all 0.225s ease-out;
        transition: all 0.225s ease-out;
    
        /* specifics */
        -moz-transition-property: -moz-transform, opacity;
        -ms-transition-property: -ms-transform, opacity;
        -webkit-transition-property: -webkit-transform, opacity;
    
        /* default state */
        -webkit-transform: scale(1.1);
        -ms-transform: scale(1.1);
        -moz-transform: scale(1.1);
        transform: scale(1.1);
        opacity: 0;
    }
    
  2. 截图1中的背景,在页面滚动时会有相应的变化,比如图中的相机镜头,会上下移动。

    function refresh(docScrollY) {
        var i, j, scroll, transformParam;
        i = 0;
        bgElements.each(function(bgElement) {
            scroll = -Math.round(((docScrollY - offset) / containerHeight)
                    * maxScrolls[i]);
            scroll = Math.max(scroll, maxScrolls[i]);
            var transformParam;
            if (!use2DTransform) {
                transformParam = 'translate3d(0px,' + scroll + 'px, 0px)';
            } else {
                transformParam = 'translateY(' + scroll + 'px)';
            }
            if (transform && transformParam) {
                bgElement.setStyle(transform, transformParam);
                bgElement.setStyle(prefixes.w3c, transformParam);
            } else {
                bgElement.setStyle('marginTop', scroll + 'px');
            }
            i++;
        });
    }
    
  3. 截图3中的图片,会自动根据浏览器的大小进行排列。这里在浏览器缩放时,根据一定的算法计算图片的大小(同一行的高度相等,宽度的和等于浏览器的宽度,图片按比例缩放),然后进行重排(图片墙):

+ 从第一个张图片开始遍历图片。遍历到图片的时候,先把它等比例缩放到高度为一个特定的高度ROWHEIGHT,也就是让所有图片的高度统一下来,宽度根据高度的缩放比例来缩放;
+ 把宽度累加起来(包括margin)。直到宽度超出容器的宽度时候,这样,就有了一行的数据;
+ 这时候要判断,是否应该就把超出的部分在每张图片中等比缩小回去,还是去掉这行的最后一张图片之后把每这行张图片等比放大,这两个状态哪个更接近满行就用哪个。这个操作,还需要记录这一行与满行的差,这个数据在计算图片需要缩放多少时候会用到;
+ 得到一行的数据之后,就要去计算超出的部分在高度中应该调整多少。可以把图片看作是一个高度为自变量,宽度为因变量的函数。这样只要把这一整行的所有图片的导数相加就可以得到整行的导数了。然后把上面记录的行与满行的差除以这个数字就可以得到高度需要变化的量了;
+ 在ROWHEIGHT中修正这个变化量,再计算等比缩放的宽度就可以得到图片最终的显示宽度了。

[这里](/data/imagewall.js)是一个简单的实现,基本实现功能。但是对于最后一行,如果只分配到1、2 张图片, 那么就有可能被拉得很高.
...

jQuery UI

31 Oct 2013

  1. jQuery UI 是什么

    jQuery UI 是以 jQuery 为基础的开源 JavaScript 网页用户界面代码库。包含底层用户交互、动画、特效和可更换主题的可视控件。

  2. jQuery UI 有什么

    jQuery UI 主要分为3个部分:Interactions、Widgets和Effects。

+ Interactions

    一些与鼠标交互相关的内容,包括Draggable(拖动)、Droppable(放置)、Resizable(缩放)、Selectable(可选)、Sortable(排序)等。

+ Widgets

    主要是一些界面的扩展,包括Accordion(手风琴堆叠)、AutoComplete(自动完成)、Button(按钮)、Datepicker(日期选择)、Dialog(对话框)、Menu(菜单)、ProgressBar(进度条)、Slider(滑条)、Spinner(数字微调)、Tabs、Tooltip(提示)等。

    jQuery UI有丰富的主题,可以非常方便地更换widget主题,也可以自己自定义主题。

+ Effects

    用于提供丰富的动画效果,让动画不再局限于jQuery的animate()方法。
  1. jQuery UI 没有什么

    jQuery UI 没有Datagrid、没有Tree、没有Layout、没有ComboBox、没有RichTextEditor、更没有Mobile支持、……。

  2. 一个简单示例

+  引用

```
<link rel="Stylesheet" type="text/css" href="themes/base/jquery.ui.all.css" />
<script type="text/javascript" src="ui/jquery.ui.core.js"></script>
<script type="text/javascript" src="ui/jquery.ui.widget.js"></script>
<script type="text/javascript" src="ui/jquery.ui.accordion.js"></script>
```

+ 实现代码

```
<div id="accordion" class="ui-widget-content"   style="height: 80%;">
     <h6><a href="#">用户管理</a></h6>
     <div>...</div>
     <h6><a href="#">商品管理</a></h6>
     <div>...</div>
     <h6><a href="#">订单管理</a></h6>
     <div>
       <ul>
           <li><a href="#">订单管理</a></li>
           <li><a href="#">收货地址管理</a></li>
           <li><a href="#">退货管理</a></li>
        </ul>
     </div>
     <h6><a href="#">统计</a></h6>
     <div>...</div>
</div>
```

```
//初始化手风琴
function initAccordion(){
    $('#accordion').accordion({
        collapsible: true
    });
}

$(document).ready(function() {
    //手风琴
    initAccordion();
});
```

+ 实现效果

    ![jQuery UI Tab](/img/jQuery UI/jQueryUI-accordion.png)

+ 方法调用

```
// getter
var active = $('#accordion').accordion('option', 'active');

// setter
$('#accordion').accordion('option', 'active', 2);
```
  1. jQuery UI可以用来做什么
+ Interactions

    通用的鼠标交互效果,可以简化页面的交互的实现方式,比如拖动、排序。

+ Widgets

    只是一些界面的小组件,提供的组件比较有限,只能用于页面部分效果的处理,无法像Dojo、ExtJs那样代替整个页面的实现。另外,大部分组件和Bootstrap重复了。

    jQuery UI提供丰富的主题,并可以进行自定义。

    不过jQuery UI提供了扩展自定义widget的方式,另外也有一些开源的扩展widget实现,比如http://quasipartikel.at/multiselect 中的MultiSelect

    PS:个人感觉jQuery UI 的界面效果做得不怎么样,自带的theme效果也不是很好,例如:

    jQuery UI

    ![jQuery UI Tab](/img/jQuery UI/jQueryUI-tab.png)

    Bootstrape

    ![Bootstrap Tab](/img/jQuery UI/bootstrap-tab.png)

+ Effects

    对jQuery动画的扩展,实际使用中并不是很必要,也不是很有意义。

综上,jQuery UI 的widget和Effect意义并不是很大。反倒是Interactions更实用一些。

...

Hibernate Search (Part 1)

01 Jul 2013

今天參加了一個面試, 聊到了差不多去年這個時候做的全文搜索功能, 感覺自己總結得實在是太少了, 明明知道的一些東西, 問起來卻總是一下答不上來...

業務場景其實不算複雜, 就是要做到類似Google 搜索那樣, 通過一個輸入框來搜索系統中多個表/ 多個字段中包含了所輸入內容的記錄集, 也就是在幾個表的字段中, 如果包含了輸入的內容, 那麼對於的記錄就是符合的.

因爲主要的業務對象是數據庫, 而不是文檔, 所以技術選型的結果就是使用Hibernate Search 來實現這個全文搜索功能, 主要是基於下面幾點考慮:

  1. 主要的業務對象並不是文檔, 而是數據庫

  2. Hibernate Search 跟Hibernate 結合得更緊密, 可以直接通過Hibernate 實體類來操作Lucene 的索引, 只需要實體類中添加Annotation, 然後在Hibernate 配置文件中添加listener, 即可以在數據新增/ 修改/ 刪除時自動更新索引.

  3. 數據查詢同樣可以借助Hibernate, 減少了從索引中獲取數據後再轉折去查詢數據庫的操作.

由於內容較多, 這一節先只介紹索引的處理.

  • 首先當然就是添加Hibernate Search 的依賴了

  • 在要創建Lucene 索引的實體類中添加Annotation, 如下:

 1 @Indexed
 2     public class Device {
 3         @DocumentId
 4         private long id;
 5         
 6         @Field(name = "deviceNname", index = Index.TOKENIZED, store = Store.YES)
 7         private String name;
 8         
 9         private String type;
10         
11         //... getter setter
12     }
 1 @Indexed
 2     public class Alarm {
 3         @DocumentId
 4         private long id;
 5         
 6         @Field(name = "alarmName", index = Index.TOKENIZED, store = Store.YES)
 7         private String name;
 8         
 9         @IndexedEmbedded
10         private Device device;
11         
12         //... getter setter
13     }
這裏, 主要是@Field 的Annotation, 其中的name 屬性即是生成的Lucene 字段名稱
  • 接下來自然是Hibernate 的配置文件了, 和正常的配置文件差不多, 除了要添加Lucene 的索引保存路徑, 如果要在新增/ 修改/ 刪除實體時自動更新索引, 還需要添加listener.
1 <!-- 配置索引的保存方式, 這裡配置為使用文件進行保存 -->
2     <property name="hibernate.search.default.directory_provider">org.hibernate.search.store.FSDirectoryProvider</property>
3     <!-- 配置索引的保存路徑 -->
4     <property name="hibernate.search.default.indexBase">./var/lucene/indexes</property>
5     <!-- 是否使用listener 監聽實體的改變 -->
6     <property name="hibernate.search.autoregister_listeners">true</property>
 1 <!-- 這裡沒有直接使用hibernate 的配置方式了, 如果使用spring 進行配置, 則更加簡單 -->
 2     <event type="post-update">
 3         <listener class="org.hibernate.search.event.FullTextIndexEventListener" />
 4     </event>
 5     <event type="post-insert">
 6         <listener class="org.hibernate.search.event.FullTextIndexEventListener"/>
 7     </event>
 8     <event type="post-delete">
 9         <listener class="org.hibernate.search.event.FullTextIndexEventListener"/>
10     </event>
11     
12     <event type="post-collection-recreate">
13         <listener class="org.hibernate.search.event.FullTextIndexEventListener"/>
14     </event>
15     <event type="post-collection-remove">
16         <listener class="org.hibernate.search.event.FullTextIndexEventListener"/>
17     </event>
18     <event type="post-collection-update">
19         <listener class="org.hibernate.search.event.FullTextIndexEventListener"/>
20     </event>
21     <event type="flush">
22         <listener class="org.hibernate.event.def.DefaultFlushEventListener"/>
23     </event>
  • 如果像上面那樣配置了listener, 那麼這樣就可以了, 在實體內容發生變化時, 會自動更新對於的索引. 如果不自動監聽實體的改變, 那麼就需要自己在實體改變的時候去更新索引.
1 Session session = null; // ... get session;
2     FullTextSession fullTextSession = Search.getFullTextSession(session);
3     Transaction tx = fullTextSession.beginTransaction();
4     // 新建/ 更新索引
5     fullTextSession.index(po);
6     // 刪除索引
7     // fullTextSession.purge(po.getClass(), po.getId());
8     tx.commit();

至此, 創建索引的操作就算是完成了, 這比直接使用Lucene 要簡單很多.

...