jQuery插件模板 - 最佳实践、惯例、性能和内存影响

59

我开始编写一些jQuery插件,并想要为我的IDE设置一个jQuery插件模板。

我已经阅读了这个网站上与插件约定、设计等相关的一些文章和帖子,想要将它们整合起来。

以下是我的模板,我希望经常使用它,因此我很乐意确保它基本符合jQuery插件设计约定,并且是否拥有多个内部方法(甚至其总体设计)会影响性能并容易出现内存问题。

(function($)
{
    var PLUGIN_NAME = "myPlugin"; // TODO: Plugin name goes here.
    var DEFAULT_OPTIONS =
    {
        // TODO: Default options for plugin.
    };
    var pluginInstanceIdCount = 0;

    var I = function(/*HTMLElement*/ element)
    {
        return new Internal(element);
    };

    var Internal = function(/*HTMLElement*/ element)
    {
        this.$elem = $(element);
        this.elem = element;
        this.data = this.getData();

        // Shorthand accessors to data entries:
        this.id = this.data.id;
        this.options = this.data.options;
    };

    /**
     * Initialises the plugin.
     */
    Internal.prototype.init = function(/*Object*/ customOptions)
    {
        var data = this.getData();

        if (!data.initialised)
        {
            data.initialised = true;
            data.options = $.extend(DEFAULT_OPTIONS, customOptions);

            // TODO: Set default data plugin variables.
            // TODO: Call custom internal methods to intialise your plugin.
        }
    };

    /**
     * Returns the data for relevant for this plugin
     * while also setting the ID for this plugin instance
     * if this is a new instance.
     */
    Internal.prototype.getData = function()
    {
        if (!this.$elem.data(PLUGIN_NAME))
        {
            this.$elem.data(PLUGIN_NAME, {
                id : pluginInstanceIdCount++,
                initialised : false
            });
        }

        return this.$elem.data(PLUGIN_NAME);
    };

    // TODO: Add additional internal methods here, e.g. Internal.prototype.<myPrivMethod> = function(){...}

    /**
     * Returns the event namespace for this widget.
     * The returned namespace is unique for this widget
     * since it could bind listeners to other elements
     * on the page or the window.
     */
    Internal.prototype.getEventNs = function(/*boolean*/ includeDot)
    {
        return (includeDot !== false ? "." : "") + PLUGIN_NAME + "_" + this.id;
    };

    /**
     * Removes all event listeners, data and
     * HTML elements automatically created.
     */
    Internal.prototype.destroy = function()
    {
        this.$elem.unbind(this.getEventNs());
        this.$elem.removeData(PLUGIN_NAME);

        // TODO: Unbind listeners attached to other elements of the page and window.
    };

    var publicMethods =
    {
        init : function(/*Object*/ customOptions)
        {
            return this.each(function()
            {
                I(this).init(customOptions);
            });
        },

        destroy : function()
        {
            return this.each(function()
            {
                I(this).destroy();
            });
        }

        // TODO: Add additional public methods here.
    };

    $.fn[PLUGIN_NAME] = function(/*String|Object*/ methodOrOptions)
    {
        if (!methodOrOptions || typeof methodOrOptions == "object")
        {
            return publicMethods.init.call(this, methodOrOptions);
        }
        else if (publicMethods[methodOrOptions])
        {
            var args = Array.prototype.slice.call(arguments, 1);

            return publicMethods[methodOrOptions].apply(this, args);
        }
        else
        {
            $.error("Method '" + methodOrOptions + "' doesn't exist for " + PLUGIN_NAME + " plugin");
        }
    };
})(jQuery);

提前感谢您。


8
个人认为这样的模板过度工程化。我真的不认为你需要在JavaScript中添加这么多杂乱无章的内容。感觉有点过于企业化了。保持简单(KISS)。 - Raynos
4
感觉这个东西有点像 C# / Java,不太像 JavaScript。我想尝试给出一个不同的模板。 - Raynos
4
这是许多 jQuery 插件中使用的技术组合,我认为这是最好的通用实现之一。查看任何 jQuery 插件,你会看到他正在做的子集。所以每个人都做错了吗?唯一我不喜欢的是内部对象名称,个人而言,我会用 VS 中的片段替换它。我不太理解你关于企业/C#/Java 的抱怨。 - mattmanser
2
我从未编写过jQuery插件,但当我开始这个旅程时,我会关注它的。 - Sonny
3
你需要在每行的末尾加上花括号(即k&r风格)。因为在JavaScript中,分号是可选的,如果不加花括号可能会导致代码出错。请参考http://www.codeproject.com/KB/scripting/javascript-gotchas.aspx#semicolons和http://encosia.com/in-javascript-curly-brace-placement-matters-an-example/。(我知道,我也是喜欢在新行上放置花括号的人。) - Jason
显示剩余3条评论
4个回答

29

我之前基于一篇博客文章构建了一个插件生成器: http://jsfiddle.net/KeesCBakker/QkPBF/。它可能有用。它相当基础和直接。欢迎提出任何意见。

您可以 fork 自己的生成器并根据自己的需要进行更改。

附:这是生成的主体内容:

(function($){

    //My description
    function MyPluginClassName(el, options) {

        //Defaults:
        this.defaults = {
            defaultStringSetting: 'Hello World',
            defaultIntSetting: 1
        };

        //Extending options:
        this.opts = $.extend({}, this.defaults, options);

        //Privates:
        this.$el = $(el);
    }

    // Separate functionality from object creation
    MyPluginClassName.prototype = {

        init: function() {
            var _this = this;
        },

        //My method description
        myMethod: function() {
            var _this = this;
        }
    };

    // The actual plugin
    $.fn.myPluginClassName = function(options) {
        if(this.length) {
            this.each(function() {
                var rev = new MyPluginClassName(this, options);
                rev.init();
                $(this).data('myPluginClassName', rev);
            });
        }
    };
})(jQuery);

6
我觉得有趣的是生成插件的代码本身不是插件。 - Raynos
@ErickPetru 我不知道,大多数时候我使用它来为我的类制作基本框架,实际验证并不是真正需要的。 - Kees C. Bakker
不要在意我上次的评论——你的工作看起来很棒,这个答案可以让人们很快地提高生产力,向你点赞。Raynos 的回答需要在开始构建之前进行太多的思维解构。 - whitneyland
1
美丽、简单而优雅。这正是我需要的,以开始进行适当的插件开发。 - nitech
是的,在工作中有多个。公司一直使用这种格式。你在找什么?jsFiddle 的东西生成得非常好,你需要什么? - Kees C. Bakker
显示剩余6条评论

26

[编辑] 7个月后

引用自GitHub项目

jQuery不好用,而且jQuery插件不能实现模块化编程。

说真的,“jQuery插件”不是一个合理的架构策略。写代码时强依赖于jQuery也是很傻的。

[原文]

由于我对这个模板提出了批评,因此我会提出一种替代方案。

为了让生活更轻松,这个方案依赖于jQuery 1.6+和ES5(使用ES5 Shim)。

我花了一些时间重新设计了你给出的插件模板,并推出了我的版本。

链接:

Comparison(比较):

我已将模板重构为85%的样板代码和15%的脚手架代码。意图是您只需编辑脚手架代码,即可保持样板代码不变。为此,我使用了

  • 继承 var self = Object.create(Base):不要直接编辑Internal类,而应该编辑子类。所有模板/默认功能都应该在一个基类中(我的代码中称为Base)。
  • 约定 self[PLUGIN_NAME] = main;:按照约定,在jQuery上定义的插件默认会调用self[PLUGIN_NAME]上的方法。这被认为是main插件方法,并有一个单独的外部方法以增加清晰度。
  • 猴子补丁 $.fn.bind = function _bind ...:使用猴子补丁意味着事件命名空间在幕后自动完成。这个功能是免费的,不会影响可读性(不需要一直调用getEventNS)。

面向对象技术

最好坚持使用适当的JavaScript面向对象编程,而不是传统的面向对象模拟。要实现这一点,你应该使用Object.create(ES5只需使用shim来升级旧浏览器)。

var Base = (function _Base() {
    var self = Object.create({}); 
    /* ... */
    return self;
})();

var Wrap = (function _Wrap() {
    var self = Object.create(Base);
    /* ...  */
    return self;
})();

var w = Object.create(Wrap);

这与人们所习惯的基于new.prototype的面向对象不同。这种方法更受欢迎,因为它强调了JavaScript中只有对象的概念,是一种原型面向对象的方法。

[getEventNs]

如上所述,此方法已通过覆盖.bind.unbind自动注入命名空间而被重构。这些方法在jQuery的私有版本$.sub()上被重写。被重写的方法的行为与您的命名空间相同。它根据插件和HTMLElement的包装器实例(使用.ns)独特地为事件命名空间进行命名。

[getData]

此方法已被替换为具有与jQuery.fn.data相同API的.data方法。它与jQuery.fn.data具有相同的API,使其更易于使用。这基本上是jQuery.fn.data的一个薄包装器,带有命名空间。这允许您设置仅对该插件立即存储的键/值对数据。多个插件可以并行使用此方法,而不会发生冲突。

[publicMethods]

publicMethods对象已被替换,任何定义在Wrap上的方法都会自动成为公共方法。您可以直接调用包装对象上的任何方法,但实际上您无法访问包装对象本身。

[$.fn[PLUGIN_NAME]]

这个已经重构,以便公开更标准化的API。这个API是

$(selector).PLUGIN_NAME("methodName", {/* object hash */}); // OR
$(selector).PLUGIN_NAME({/* object hash */}); // methodName defaults to PLUGIN_NAME

选择器中的元素会自动包装在Wrap对象中,每个选定元素都会调用该方法,返回值始终是$.Deferred元素,从而标准化了API和返回类型。然后你可以调用.then在返回的deferred上获取你关心的实际数据。在此使用deferred非常强大,可以抽象出插件是同步还是异步的细节。 _create
添加了一个缓存创建函数,它用于将HTMLElement转换为包装元素,每个HTMLElement只会被包装一次。这种缓存可以显著减少内存占用。

$.PLUGIN_NAME

为插件添加了另一个公共方法(总共两个!)。

$.PLUGIN_NAME(elem, "methodName", {/* options */});
$.PLUGIN_NAME([elem, elem2, ...], "methodName", {/* options */});
$.PLUGIN_NAME("methodName", { 
  elem: elem, /* [elem, elem2, ...] */
  cb: function() { /* success callback */ }
  /* further options */
});

所有参数都是可选的。 elem 默认为 <body>"methodName" 默认为 "PLUGIN_NAME"{/* options */} 默认为 {}

这个 API 非常灵活(有 14 种方法重载!)并且标准化,足以让您熟悉每种方法插件将公开的语法。

公共曝光

Wrapcreate$ 对象被全局公开。这将允许高级插件用户最大限度地使用您的插件。他们可以在开发中使用 create 和修改后的子代 $,也可以 monkey patch Wrap。这允许例如钩入您的插件方法。这三个都在其名称前面标有 _,因此它们是内部的,使用它们会破坏您的插件工作的保证。

内部的 defaults 对象也公开为 $.PLUGIN_NAME.global。这允许用户覆盖您的默认值并设置插件全局 defaults。在此插件设置中,传递到方法的所有哈希作为对象合并到默认值中,因此这允许用户为所有方法设置全局默认值。

实际代码

(function($, jQuery, window, document, undefined) {
    var PLUGIN_NAME = "Identity";
    // default options hash.
    var defaults = {
        // TODO: Add defaults
    };

    // -------------------------------
    // -------- BOILERPLATE ----------
    // -------------------------------

    var toString = Object.prototype.toString,
        // uid for elements
        uuid = 0,
        Wrap, Base, create, main;

    (function _boilerplate() {
        // over-ride bind so it uses a namespace by default
        // namespace is 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);
        };

        // override unbind so it uses a namespace by default.
        // add new override. .unbind() with 0 arguments unbinds all methods
        // for that element for this plugin. i.e. calls .unbind(_ns)
        $.fn.unbind = function _unbind(type, fn, nsKey) {
            // Handle object literals
            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;
        };

        // Creates a new Wrapped element. This is cached. One wrapped element 
        // per HTMLElement. Uses data-PLUGIN_NAME-cache as key and 
        // creates one if not exists.
        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();
            };
        }());

        // Base object which every Wrap inherits from
        Base = (function _Base() {
            var self = Object.create({});
            // destroy method. unbinds, removes data
            self.destroy = function _destroy() {
                if (this._alive) {
                    this.$elem.unbind();
                    this.$elem.removeData(PLUGIN_NAME);
                    this._alive = false;    
                }
            };

            // initializes the namespace and stores it on the elem.
            self._init = function _init() {
                if (!this._alive) {
                    this._ns = "." + PLUGIN_NAME + "_" + this.uid;
                    this.data("_ns", this._ns);    
                    this._alive = true;
                }
                return this;
            };

            // returns data thats stored on the elem under the plugin.
            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;
        })();

        // Call methods directly. $.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);
        };

        // expose publicly.
        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; }
             }
        });

        // main plugin. $(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 || {};

            // map the elements to deferreds.
            var defs = this.map(function _map() {
                return create(this)[op](hash);
            }).toArray();

            // call the cb when were done and return the deffered.
            return $.when.apply($, defs).then(hash.cb);

        };
    }());

    // -------------------------------
    // --------- YOUR CODE -----------
    // -------------------------------

    main = function _main(options) {
        this.options = options = $.extend(true, defaults, options); 
        var def = $.Deferred();

        // Identity returns this & the $elem.
        // TODO: Replace with custom logic
        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
            // remove elements and other events / data not stored on .$elem

            $destroy.apply(this, arguments);
        };

        // set the main PLUGIN_NAME method to be main.
        self[PLUGIN_NAME] = main;

        // TODO: Add custom logic for public methods

        return self;
    }());

})(jQuery.sub(), jQuery, this, document);

可以看到,您需要编辑的代码位于YOUR CODE行以下。 Wrap对象类似于您的Internal对象。 main函数是通过$.PLUGIN_NAME()$(selector).PLUGIN_NAME()调用的主函数,应包含您的主要逻辑。

6
如果您正在阅读这篇答案,可能不知道ES5是什么。它是ECMAScript标准的第五个版本:http://en.wikipedia.org/wiki/ECMAScript#Versions - whitneyland
2
@Raynos,我正在尝试学习jQuery插件开发(同时也在学习面向对象的JavaScript)。您认为让您的代码更易于初学者阅读并用于学习和生产(包括.min.js)怎么样?例如,对于主要插件,也许您可以过度地冗长?即将参数op重命名为operation_to_call,或许在第180行包括一个关于else条件的注释描述。cbcallback_function,对吗? - blong
1
@BrianL 我两个月前写了这个。如果我要动它,我会完全重写它。实际上,现在我建议不要使用jQuery,但那是另外一个故事了。然而,如果你想谈论模块化JS开发,请随时与我们聊天 - Raynos
3
jQuery插件非常棒,为什么它们不是?我在构建任何网站时都会包含jQuery,它的API、选择器支持以及简单的过程可以使常见任务变得轻松无比,因此,由于我和大多数人总是有这个依赖关系,为什么不为模块化组件制作jQuery插件呢?例如,如果我要制作一个可重复使用的自动完成系统,你为什么不建议将其制作为jQuery插件?你还会使用jQuery来使其中的操作更加简单吗? - GONeale
1
@Raynos 我也开始了一个问题,如果你愿意回答,请看一下。它并不是要表现出轻蔑的态度,而是为了我和其他人的学习。http://stackoverflow.com/questions/18626842/could-somebody-tell-me-the-benefits-of-making-a-jquery-plugin-this-verbose - GONeale
显示剩余4条评论

0
这样怎么样?它更清晰易懂,但如果您能在不过度复杂化其简洁性的情况下改进它,我们很乐意听取您的意见。
// jQuery plugin Template
(function($){
    $.myPlugin = function(options) { //or use "$.fn.myPlugin" or "$.myPlugin" to call it globaly directly from $.myPlugin();
        var defaults = {
            target: ".box",
            buttons: "li a"             
        };

        options = $.extend(defaults, options);

        function logic(){
            // ... code goes here
        }

        //DEFINE WHEN TO RUN THIS PLUGIN
        $(window).on('load resize', function () { // Load and resize as example ... use whatever you like
            logic();
        });

        // RETURN OBJECT FOR CHAINING
        // return this;

        // OR FOR FOR MULTIPLE OBJECTS
        // return this.each(function() {
        //    // Your code ...
        // });

    };
})(jQuery);


// USE EXAMPLE with default settings
$.myPlugin(); // or run plugin with default settings like so.

// USE EXAMPLE with overwriten settings
var options = {
    target: "div.box", // define custom options
    buttons: ".something li a" // define custom options
}     
$.myPlugin(options); //or run plugin with overwriten default settings

-1

我一直在谷歌上搜索,最终来到这里,所以我必须发表一些想法:首先,我同意@Raynos的观点。

大多数试图构建jQuery插件的代码实际上...并不是插件!它只是存储在内存中的一个对象,由节点/元素的数据属性引用。这是因为jQuery应该被视为与类库并排使用的工具(以弥补从OO架构中的js不一致性),以构建更好的代码,是非常好的选择!

如果您不喜欢经典的OO行为,请使用原型库,例如clone

那么我们真正的选择是什么?

  • 使用JQueryUI/Widget或类似的库,隐藏技术细节并提供抽象
  • 由于复杂性、学习曲线和未来的变化,不使用它们
  • 不使用它们,因为您想坚持模块化设计,构建小型增量后期
  • 不使用它们,因为您可能希望将您的代码移植/连接到不同的库中。

假设以下情景中涉及的问题(请参见此问题的复杂性:我应该使用哪种jQuery插件设计模式?):

我们有节点A、B和C,它们将对象引用存储在其data属性中

其中一些在公共私有可访问的内部对象中存储信息, 这些对象的某些类与继承相关联, 所有这些节点还需要一些私有的公共的单例才能发挥最佳作用。

我们该怎么办?请看下图:

classes : |  A        B         C
------------------case 1----------
members   |  |        |         |
  of      |  v        v         v
an object | var a=new A, b=new B,  c=new C
  at      |     B extends A
node X :  |  a, b, c : private
------------------case 2---------
members   |  |        |         |
  of      |  v        v         v
an object | var aa=new A, bb=new B, cc=new C
  at      |     BB extends AA
node Y :  |  aa, bb, cc : public
-------------------case 3--------
members   |  |        |         |
  of      |  v        v         v
an object | var d= D.getInstance() (private),
  at      |     e= E.getInstance() (public)
node Z :  |     D, E : Singletons

正如您所看到的,每个节点都指向一个对象 - 一种jQuery方法 - 但这些对象会发生很大变化;它们包含具有不同数据存储的对象属性,甚至是应该在内存中单独存在的单例,就像对象的原型函数一样。我们不希望每个对象的函数都属于 class A 并且在每个节点的对象中重复内存中复制

在我的回答之前,请看我在jQuery插件中看到的常见方法 - 其中一些非常受欢迎,但我不说名字:

(function($, window, document, undefined){
   var x = '...', y = '...', z = '...',
       container, $container, options;
   var myPlugin = (function(){ //<----the game is lost!
      var defaults = {

      };
      function init(elem, options) {
         container = elem;
         $container = $(elem);
         options = $.extend({}, defaults, options);
      }
      return {
         pluginName: 'superPlugin',
         init: function(elem, options) {
            init(elem, options);
         }
      };
   })();
   //extend jquery
   $.fn.superPlugin = function(options) {
      return this.each(function() {
         var obj = Object.create(myPlugin); //<---lose, lose, lose!
         obj.init(this, options);
         $(this).data(obj.pluginName, obj);
      });
   };

}(jQuery, window, document));

我正在观看Ben Alman的幻灯片,链接为http://www.slideshare.net/benalman/jquery-plugin-creation,他在第13张幻灯片中将对象字面量称为单例,这让我大吃一惊:上述插件就是这样做的,它创建了一个无法更改内部状态的单例!!!
此外,在jQuery部分,它存储了每个节点的公共引用
我的解决方案使用工厂来保持内部状态并返回一个对象,还可以通过类库扩展并拆分成不同的文件:
;(function($, window, document, undefined){
   var myPluginFactory = function(elem, options){
   ........
   var modelState = {
      options: null //collects data from user + default
   };
   ........
   function modeler(elem){
      modelState.options.a = new $$.A(elem.href);
      modelState.options.b = $$.B.getInstance();
   };
   ........
   return {
         pluginName: 'myPlugin',
         init: function(elem, options) {
            init(elem, options);
         },
         get_a: function(){return modelState.options.a.href;},
         get_b: function(){return modelState.options.b.toString();}
      };
   };
   //extend jquery
   $.fn.myPlugin = function(options) {
      return this.each(function() {
         var plugin = myPluginFactory(this, options);
         $(this).data(plugin.pluginName, plugin);
      });
   };
}(jQuery, window, document));

我的项目:https://github.com/centurianii/jsplugin 查看:http://jsfiddle.net/centurianii/s4J2H/1/

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接