2012年4月17日星期二

如何开发jQuery 插件


如何开发jQuery 插件

基于 jQuery 官方网站Plugins/Authoring页面的说明改编
发表时间:2012年4月3日

jQuery是最流行的JavaScript库,很多网站都使用jQuery。jQuery常被用来做动态效果或实现Ajax的功能。但是,相对而言,很少开发者会去深入研究插件的开发。

这篇文章将会概述关于插件编写的基础知识,最佳实践(Best Practices),以及刚开始编写插件时需要注意的常见陷阱。




目录
1.新手入门
2.背景介绍
3.基础知识
4.维护chainability(jQuery魔术)
5.默认和选项
6.命名空间
a) 插件方法
b) 事件
c) 数据
7.总结和最佳实践

新手入门


通过扩展jQuery,可以创建在任何页面上都能使用的可重用组件。代码是已封装的,你可以在别的地方使用相同名字给函数命名,风险较小。
要编写一个jQuery插件,从给jQuery.fn对象添加新的函数属性开始,新属性的名字应取为插件的名字。

// jQuery.fn = jQuery.prototype = ... 
jQuery.fn.myPlugin = function () {
    // 把你厉害的插件代码写在这里  
};

但是,为了确保编写的插件与其他可能使用美元符号的库不冲突,将jQuery变成立即调用的函数表达式IIFEImmediately Invoked Function Expression)是最优方案,IIFE将插件映射给美元符号,以免在执行代码期间插件被另一个库覆盖。

(function ($) {
     $.fn.myPlugin = function () {
         // 把你厉害的插件代码写在这里     
        // 变量范围     
         //   闭包    
         //     $: function (selector, context) {...   };
})(jQuery);

在闭包里面,可以任意使用美元符号来代替jQuery。

背景介绍

在掌握了一些基本知识后,我们可以开始编写自己的插件代码了。在那之前,先介绍一下相关背景。在插件函数的即时范围(immediate scope)里,关键字this指的是jQuery对象,插件会被立即调用。这是一个常见误区因为在其他实例中,jQury是接受回调的,而this关键字指的是回调函数中源生DOM element。这常常会让开发者们将this关键字(再度)打包在jQuery函数中,这其实是不必要的。

(function ($) {
    $.fn.myPlugin = function () {
        // 这里没有必要使用 $(this)     
        // 因为"this" 已经是jquery的一个对象了    
        // $(this) 其实就是 $($('#element'));      
       // 本地   
        //   this: jQuery.fn.jQuery.init[1]     
        //    0: HTMLDivElement     
        //    context: HTMLDocument     
        //    length: 1     
        //    selector: "#element"    
        this.fadeIn('normal', function () {
            // this关键字是DOM element        
           // 本地     
            //   this: HTMLDivElement       
            //     id: "element"       var that = this;              
            // 本地     
            //   $this: jQuery.fn.jQuery.init[1]       
            //     0: HTMLDivElement       
            //     context : HTMLDivElement       
            //     length: 1      
            var $this = $(this);
        });
    };
})(jQuery);
$('#element').myPlugin();

基础知识

在了解了jQuery插件的相关背景之后,现在来练练手,编写个能实现某种功能的插件吧。
(function ($) {
    $.fn.maxHeight = function () {
        var max = 0;
        this.each(function () {
           // 闭包 : max      
            max = Math.max(max, $(this).height());
        });
        return max;
    };
})(jQuery);
var tallest = $('div').maxHeight(); // 返回最高的div的高度
这是一个简单的插件,使用.height() 返回该页面最高div的高度。


维持Chainability

之前的例子返回的是页面上最高div的整数值。但是,插件常被用来改变集合中的元素,然后将他们传递给链中的下一个方法。这就是jQuery设计的美妙之处,也是jQuery如此盛行的原因之一。因此为了维护插件中的chainability,必须得确保插件返回了this关键字。

(function ($) {
    $.fn.lockDimensions = function (type) {
       // .each(function(index, Element))     
        // 对每个匹配的元素,jQuery对象重复执行函数
        // 返回jQuery    
        return this.each(function () {
            var $this = $(this);
            if (!type || type == 'width') {
                $this.width($this.width());
            }
            if (!type || type == 'height') {
                $this.height($this.height());
            }
        });
    };
})(jQuery);
$('div').lockDimensions('width').css('color', 'red');

由于插件在即时范围中返回了this关键词,因此它维持了chainability,并且jQuery方法可以继续操作jQeury集合,例如.css。所以,如果插件没有返回一个本身的值,就需要在插件函数的即时范围内一直返回关键字this。同样的,在插件调用中传递的参数可能被传递给插件函数的即使范围。因此在之前的例子中,字符串“width”变为插件函数的参数。

默认和选项 

对于可以提供很多选项的复杂和可定制的插件,在插件被调用时,默认设置能被扩展(使用$.extend)是最优方案。因此,无须调用具有大量参数的插件,可以只调用想要覆盖的对象字面量(object literal)设置作为参数。以下是使用方法。

(function ($) {
    $.fn.tooltip = function (options) {
        // 创建一些默认参数,使用提供的选项扩展它们    
       // var defaults = {...}; var settings = $.extend({}, defaults, options); 也可以做到
        var settings = $.extend({
            'location': 'top',
            'background-color': 'blue'
        }, options);
        return this.each(function () {
           // 此处为tooltip插件代码     
        });
    };
})(jQuery);
$('div').tooltip({   'location' : 'left' });

在这个例子当中,在用给定选项调用tooltip插件后,默认的location设定被覆盖为“left”,而bakground-color的设定仍然是默认的“blue”。因此最终的设置对象应该是这样:

{  'location'  : 'left',   'background-color' : 'blue' }

高度可配置插件的实用性之一就是它不要求开发者定义所有可用的选项。

命名空间

插件开发中,合适地命名空间(namespace)十分重要。正确地命名空间可以降低插件被其他插件或者同页面上代码覆盖的可能性。命名空间同样也会让你的生活更容易,因为它能更好地跟踪方法,事件以及数据。

· 插件方法 Plugin Methods

jQuery.fn对象中单单一个插件是不可能有多个命名空间的。

(function ($) {
    $.fn.tooltip = function (options) {
        //  
    };
    $.fn.tooltipShow = function () {
        //    
    };
    $.fn.tooltipHide = function () {
        // 糟糕  
    };
    $.fn.tooltipUpdate = function (content) {
        // !!    
    };
})(jQuery);

这是一个不好的例子,因为它堆满了$.fn 命名空间。为了补救,应该将所有插件方法放在同一个对象字面量里,通过将方法名传递给插件来调用方法。

(function ($) {
    var methods = {
        init: function (options) {
            //      
        },
        show: function () {
            //     
        },
        hide: function () {
            //      
        },
        update: function (content) {
            // !!!      
        }
    };
    $.fn.tooltip = function (method) {
        // 写方法需要逻辑    
        if (methods[method]) {
            return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
        } else if (typeof method === 'object' || !method) {
            return methods.init.apply(this, arguments);
        } else {
            $.error('Method ' + method + ' does not exist on jQuery.tooltip');
        }
    };
})(jQuery);
// 调用init 方法 $('div').tooltip();   
// 调用init 方法 $('div').tooltip({   foo : 'bar' });
// 调用hide 方法 $('div').tooltip('hide');  
// 调用update 方法 $('div').tooltip('update', 'This is the new tooltip content!');

这种插件结构允许将你所有的方法封装在插件的父闭包里,并首先通过传递方法名来调用该方法,然后将所需的额外参数传递给该方法。这种方法的封装和结构在jQuery插件社区中是一种标准,数不清的插件都使用这种标准,包括jQueryUI里面的插件和小工具。

· 事件

Bind方法具有更不为人所知的另一特性,就是它允许为已绑定事件命名空间。如果插件绑定了事件,将其命名空间是很好的方法。这样的话,如果之后需要将其解除绑定,并不会干预到其他可能已经绑定给同类事件的其他事件。可以通过添加 ".<namespace>"到正在绑定事件的类型,就可以命名空间事件了。

(function ($) {
     var methods = {
         init: function (options) {
             return this.each(function () {
                 $(window).bind('resize.tooltip', methods.reposition);
             });
         },
         destroy: function () {
             return this.each(function () {
                 $(window).unbind('.tooltip');
             })
         },
         reposition: function () {
             // ...       
         },
         show: function () {
             // ...       
         },
         hide: function () {
             // ...       
         },
         update: function (content) {
             // ...      
         }
     };
     $.fn.tooltip = function (method) {
         if (methods[method]) {
             return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
         } else if (typeof method === 'object' || !method) {
             return methods.init.apply(this, arguments);
         } else {
             $.error('Method ' + method + ' does not exist on jQuery.tooltip');
         }
     };
 })(jQuery);
$('#fun').tooltip(); // 一段时间以后... $('#fun').tooltip('destroy');

在这个例子当中,当用init方法初始化tooltip时,在空间命名'tooltip'下,它将reposition方法绑定给窗口的resize事件。之后,如果开发者需要销毁tooltip,就可以将通过传递命名空间被插件绑定的事件解除绑定。这样可以安全地解除绑定,而不会无端将在插件外部绑定的事件解除绑定。

· 数据

在插件开发中,可能常常需要维持状态或者检查在给定的元素上的插件是否已经初始化了。在每个元素的基础之上,使用jQuery的data方法跟踪变量是一个很好的方法。尽管如此,最好使用单一一个对象字面量来储存所有变量,继而通过单一的数据命名空间读取该对象,而不是用不同名字跟踪一大把分散的数据调用。

(function ($) {
    var methods = {
        init: function (options) {
            return this.each(function () {
                // .data(key, value) 返回 : jQuery          
                // 储存任意与匹配元素关联的数据。     
                var $this = $(this),
                    data = $this.data('tooltip'),
                    tooltip = $('<div />', {
                        text: $this.attr('title')
                    });
                // 如果插件还没被初始化          
                if (!data) {
                    /*      这里做更多的搭建工作    */
                    $(this).data('tooltip', {
                        target: $this,
                        tooltip: tooltip
                    });
                }
            });
        },
        destroy: function () {
            return this.each(function () {
                var $this = $(this),
                    data = $this.data('tooltip');
                // 命名空间啦         
                $(window).unbind('.tooltip');
                data.tooltip.remove();
                $this.removeData('tooltip');
            })
        },
        reposition: function () {
            // ... 
        },
        show: function () {
            // ... 
        },
        hide: function () {
            // ... 
        },
        update: function (content) {
            // ...
        }
    };
    $.fn.tooltip = function (method) {
        if (methods[method]) {
            return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
        } else if (typeof method === 'object' || !method) {
            return methods.init.apply(this, arguments);
        } else {
            $.error('Method ' + method + ' does not exist on jQuery.tooltip');
        }
    };
})(jQuery);

从插件中使用数据帮助跟踪变量和状态,不需要调用方法。命名空间数据为对象字面量,可以让中心位置访问插件的属性更为容易,同样地,可以减少数据命名空间,使得在需要删除的时候变得容易。

总结和最佳练习

编写jQuery插件可以最大地利用库,并且将最好用的方法抽象化为可重用代码,既可以节省时间,又可以让开发更有效率。以下是该帖的简短总结,在开发第一个或者下一个jQuery插件的时候要谨记在心:
· 将插件打包在闭包中:  (function( $ ){ /* 插件在这里 */ })( jQuery );
· 不要太多地将this关键字打包在插件函数的即时范围之内;
· 除非从插件中返回一个内部值,插件函数都要返回this关键字以保持chainability;
· 不要传递一长串参数,将插件设置放在对象字面量中,这样就可以扩展插件默认设置;
· 不要乱堆jQuery.fn对象,一个插件不可以有多个命名空间;
· 方法,事件和数据一定要有命名空间.
· 让插件可以自己初始化

使用jQuery插件模板

以下是根据该帖内容,为开发新插件所写的模板代码:
(function ($) {
    // 私有成员或函数     
    //...TODO      
    // 公有函数 
    var methods = {
        init: function (options) {
            var defaults = {
                //...TODO            
            };
            var opts = $.extend({}, defaults, options);
            return this.each(function () {
                //...TODO             
            });
        },
        destroy: function () {
            //...TODO         
        },
        update: function () {
            //...TODO         
        }
        // 在这里可以添加其他方法,但是别忘了逗号
    };
    // PLUGIN_NAME更换为自己插件的名字
    $.fn.PLUGIN_NAME = function (method) {
        if (methods[method]) {
            return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
        } else if (typeof method === 'object' || !method) {
            return methods.init.apply(this, arguments);
        } else {
            $.error('Method ' + method + ' does not exist on jQuery.PLUGIN_NAME');
        }
    };
})(jQuery);

还有,你可以使用一个更简单的模板,由 geetarista创建的简单情况下该模板工作地不错,但是并不坚固;

 /**
 * jQuery PLUGIN_NAME Plugin
 * Version: x.x.x
 * URL: URL
 * Description: DESCRIPTION
 * Requires: JQUERY_VERSION, OTHER_PLUGIN(S), ETC. 
 * Author: AUTHOR (AUTHOR_URL)
 * Copyright: Copyright 2010 YOUR_NAME
 * License: LICENSE_INFO
 */
// 插件闭包wrapper
(function ($) {
    // 主要的插件函数
    // Replace PLUGIN with the name of your desired function
    $.fn.PLUGIN = function (options) {
        // 用插件默认设置覆盖用户选项
        var opts = $.extend({}, $.fn.PLUGIN.defaults, options);
        // 通过DOM elements 重复
        return this.each(function () {
            // 为方便使用,将现在的对象赋值给一个变量
            var $this = $(this);
            // 这就是大部分插件功能所在
        }); // 结束返回this.each
    }; // 结束$.fn.PLUGIN
    // 公有插件函数
    // 用自己的插件函数名替换PLUGIN
    // 用公有函数名替换FUNCT
    $.fn.PLUGIN.FUNCT = function () {
        // 很酷的JS行为
    }; // 结束$.fn.PLUGIN.FUNCT
    // 为插件默认设置
    $.fn.PLUGIN.defaults = {
        property: "value",
        anotherProperty: 10
    };
    // 在插件里面使用私有函数
    function privateFunction() {
        // 很酷的JS行为
    }
})(jQuery); // 结束闭包wrapper

在结束该帖之前,我将Raynos 创建的一个很厉害的jQuery 插件介绍给你们,该插件有最佳实践,惯例,性能和记忆影响。但是我认为你们需要有坚实的js基础来理解以下代码:

(function ($, jQuery, window, document, undefined) {
    var PLUGIN_NAME = "Identity";
    // default options hash.
    var defaults = {
        // TODO: 添加默认参数
    };
    // -------------------------------
    // -------- 样板文件 ----------
    // -------------------------------
    var toString = Object.prototype.toString,
        // 元素的uid 
        uuid = 0,
        Wrap, Base, create, main;
    (function _boilerplate() {
        // 覆盖bind,使用默认的命名空间
        // 命名空间是 PLUGIN_NAME_<uid>
        $.fn.bind = function _bind(type, data, fn, nsKey) {
            if (typeof type === "object") {
                for (var key in type) {
                    nsKey = key + this.data(PLUGIN_NAME)._ns;
                    this.bind(nsKey, data, type[key], fn);
                }
                return this;
            }
            nsKey = type + this.data(PLUGIN_NAME)._ns;
            return jQuery.fn.bind.call(this, nsKey, data, fn);
        };
        // 覆盖unbind 使用默认命名空间.
        // 添加新的重写. .unbind() 不需要参数来解除绑定所有的方法
        // 对于元素和插件而言例如 calls .unbind(_ns)
        $.fn.unbind = function _unbind(type, fn, nsKey) {
            // 处理对象字面量
            if (typeof type === "object" && !type.preventDefault) {
                for (var key in type) {
                    nsKey = key + this.data(PLUGIN_NAME)._ns;
                    this.unbind(nsKey, type[key]);
                }
            } else if (arguments.length === 0) {
                return jQuery.fn.unbind.call(this, this.data(PLUGIN_NAME)._ns);
            } else {
                nsKey = type + this.data(PLUGIN_NAME)._ns;
                return jQuery.fn.unbind.call(this, nsKey, fn);
            }
            return this;
        };
        // 创建一个新的Wrapped元素。这在缓存里一个已打包元素 
        // 每个HTMLElement. 使用data-PLUGIN_NAME-cache 作为关键字
        // 如果不存在就创建一个.
        create = (function _cache_create() {
            function _factory(elem) {
                return Object.create(Wrap, {
                    "elem": {
                        value: elem
                    },
                    "$elem": {
                        value: $(elem)
                    },
                    "uid": {
                        value: ++uuid
                    }
                });
            }
            var uid = 0;
            var cache = {};
            return function _cache(elem) {
                var key = "";
                for (var k in cache) {
                    if (cache[k].elem == elem) {
                        key = k;
                        break;
                    }
                }
                if (key === "") {
                    cache[PLUGIN_NAME + "_" + ++uid] = _factory(elem);
                    key = PLUGIN_NAME + "_" + uid;
                }
                return cache[key]._init();
            };
        }());
        // 基础对象,每个Wrap都是从该基础对象继承而来
        Base = (function _Base() {
            var self = Object.create({});
            // destroy方法解除锁定,删除数据
            self.destroy = function _destroy() {
                if (this._alive) {
                    this.$elem.unbind();
                    this.$elem.removeData(PLUGIN_NAME);
                    this._alive = false;
                }
            };
            // 初始化命名空间,将其储存在elem.
            self._init = function _init() {
                if (!this._alive) {
                    this._ns = "." + PLUGIN_NAME + "_" + this.uid;
                    this.data("_ns", this._ns);
                    this._alive = true;
                }
                return this;
            };
            // 在插件下返回elem中储存的数据.
            self.data = function _data(name, value) {
                var $elem = this.$elem,
                    data;
                if (name === undefined) {
                    return $elem.data(PLUGIN_NAME);
                } else if (typeof name === "object") {
                    data = $elem.data(PLUGIN_NAME) || {};
                    for (var k in name) {
                        data[k] = name[k];
                    }
                    $elem.data(PLUGIN_NAME, data);
                } else if (arguments.length === 1) {
                    return ($elem.data(PLUGIN_NAME) || {})[name];
                } else {
                    data = $elem.data(PLUGIN_NAME) || {};
                    data[name] = value;
                    $elem.data(PLUGIN_NAME, data);
                }
            };
            return self;
        })();
        // 直接调用方法. $.PLUGIN_NAME(elem, "method", option_hash)
        var methods = jQuery[PLUGIN_NAME] = function _methods(elem, op, hash) {
                if (typeof elem === "string") {
                    hash = op || {};
                    op = elem;
                    elem = hash.elem;
                } else if ((elem && elem.nodeType) || Array.isArray(elem)) {
                    if (typeof op !== "string") {
                        hash = op;
                        op = null;
                    }
                } else {
                    hash = elem || {};
                    elem = hash.elem;
                }
                hash = hash || {}
                op = op || PLUGIN_NAME;
                elem = elem || document.body;
                if (Array.isArray(elem)) {
                    var defs = elem.map(function (val) {
                        return create(val)[op](hash);
                    });
                } else {
                    var defs = [create(elem)[op](hash)];
                }
                return $.when.apply($, defs).then(hash.cb);
            };
        // 公有化暴露.
        Object.defineProperties(methods, {
            "_Wrap": {
                "get": function () {
                    return Wrap;
                },
                "set": function (v) {
                    Wrap = v;
                }
            },
            "_create": {
                value: create
            },
            "_$": {
                value: $
            },
            "global": {
                "get": function () {
                    return defaults;
                },
                "set": function (v) {
                    defaults = v;
                }
            }
        });
        // 主要插件. $(selector).PLUGIN_NAME("method", option_hash)
        jQuery.fn[PLUGIN_NAME] = function _main(op, hash) {
            if (typeof op === "object" || !op) {
                hash = op;
                op = null;
            }
            op = op || PLUGIN_NAME;
            hash = hash || {};
            // 将元素映射给deferreds.
            var defs = this.map(function _map() {
                return create(this)[op](hash);
            }).toArray();
            //调用cb,返回deffered.
            return $.when.apply($, defs).then(hash.cb);
        };
    }());
    // -------------------------------
    // --------- 你的代码 -----------
    // -------------------------------
    main = function _main(options) {
        this.options = options = $.extend(true, defaults, options);
        var def = $.Deferred();
        // Identity 返回this & $elem.
        // TODO: 用自定义逻辑代码代替
        def.resolve([this, this.elem]);
        return def;
    }
    Wrap = (function () {
        var self = Object.create(Base);
        var $destroy = self.destroy;
        self.destroy = function _destroy() {
            delete this.options;
            // custom destruction logic
            // 删除元素和其他事件 / 数据不储存在.$elem
            $destroy.apply(this, arguments);
        };
        // PLUGIN_NAME 主函数设为主函数.
        self[PLUGIN_NAME] = main;
        // TODO: 为公有方法添加自定义逻辑代码
        return self;
    }());
})(jQuery.sub(), jQuery, this, document);

转载请注明出处

没有评论:

发表评论