jQuery插件模板 - 最佳实践,约定,性能和内存影响 [英] jQuery plugin template - best practice, convention, performance and memory impact

查看:128
本文介绍了jQuery插件模板 - 最佳实践,约定,性能和内存影响的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我已经开始编写一些jQuery插件,并认为使用jQuery插件模板设置我的IDE会很不错。

I've started to write few jQuery plugins and figured it'd be nice to setup my IDE with a jQuery plugin template.

我一直在阅读本网站上与插件约定,设计等相关的一些文章和帖子。并且我认为我会尝试并巩固所有这些。

I have been reading some articles and posts on this site related to plugin convention, design, etc.. and thought I'd try and consolidate all of that.

下面是我的模板,我希望经常使用它,所以很想确保它通常符合jQuery插件设计约定以及是否有多个内部方法的想法(甚至它的一般设计会影响性能并容易出现内存问题。

Below is my template, I am looking to use it frequently so was keen to ensure it generally conforms to jQuery plugin design convention and whether the idea of having multiple internal methods (or even its general design) would impact performance and be prone to memory issues.

(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);

提前致谢。

推荐答案

7个月后

从github项目报价

Quoting from the github project


jQuery是不行的,jQuery插件不是模块化代码怎么做。

jQuery is no good, and jQuery plugins is not how do modular code.

严重的jQuery插件不是一种健全的架构策略。编写对jQuery具有硬依赖性的代码也很愚蠢。

Seriously "jQuery plugins" are not a sound architecture strategy. Writing code with a hard dependency on jQuery is also silly.

[原创]

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

Since I gave critique about this template I will propose an alternative.

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

To make live easier this relies on jQuery 1.6+ and ES5 (use the ES5 Shim).

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

I've spend some time re-designing the plugin template you've given and rolled out my own.

链接:

  • Github
  • Documentation
  • Unit tests Confirmed to pass in FF4, Chrome and IE9 (IE8 & OP11 dies. known bug).
  • Annotated Source Code
  • The PlaceKitten example plugin

比较:

我重构了模板,使其分为样板(85%)和脚手架代码(15%)。目的是您只需编辑脚手架代码,并且可以保持样板代码不变。为实现这一点,我使用了

I've refactored the template so that it's split into boilerplate (85%) and scaffolding code (15%). The intention is that you only have to edit the scaffolding code and you can keep leave boilerplate code untouched. To achieve this I've used


  • 继承 var self = Object.create(Base) 而不是编辑你直接拥有的内部类,你应该编辑一个子类。您的所有模板/默认功能都应该在基类中(在我的代码中称为 Base )。

  • 约定 self [PLUGIN_NAME] = main; 按照惯例,jQuery上定义的插件将在 self [PLUGIN_NAME] 上调用方法define默认。这被认为是 main 插件方法,为清晰起见,它有一个单独的外部方法。

  • 猴子修补 $。fn.bind = function _bind ... 使用猴子修补意味着事件命名空间在引擎盖下自动完成。此功能是免费的,不会以可读性为代价(始终调用 getEventNS )。

  • inheritance var self = Object.create(Base) Rather then editing the Internal class you have directly you should be editing a sub class. All your template / default functionality should be in a base class (called Base in my code).
  • convention self[PLUGIN_NAME] = main; By convention the plugin defined on jQuery will call the method define on self[PLUGIN_NAME] by default. This is considered the main plugin method and has a seperate external method for clarity.
  • monkey patching $.fn.bind = function _bind ... Use of monkey patching means that the event namespacing is done automatically for you under the hood. This functionality is free and does not come at the cost of readability (calling getEventNS all the time).

OO技巧

最好坚持使用正确的JavaScript OO而不是经典的OO仿真。要实现此目的,您应该使用 Object.create 。 (ES5只使用垫片升级旧浏览器)。

It's better to stick to proper JavaScript OO rather then classical OO emulation. To achieve this you should use Object.create. (which ES5 just use the shim to upgrade old browsers).

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 基于OO的人习惯了。这种方法是首选,因为它重新强化了JavaScript中只有对象的概念,它是一种典型的面向对象方法。

This is different from the standard new and .prototype based OO people are used to. This approach is preferred because it re-inforces the concept that there are only Objects in JavaScript and it's a prototypical OO approach.

[ getEventNs ]

[getEventNs]

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

As mentioned this method has been refactored away by overriding .bind and .unbind to automatically inject namespaces. These methods are overwritten on the private version of jQuery $.sub(). The overwritten methods behave the same way as your namespacing does. It namespaces events uniquely based on plugin and instance of a plugin wrapper around a HTMLElement (Using .ns.

[ getData ]

[getData]

此方法已替换为 .data jQuery.fn.data 具有相同API的方法。事实上它是相同的API使它更容易使用,它基本上是一个薄的包装器 jQuery.fn.data with namespacing。这允许您设置仅为该插件存储的键/值对数据。多个插件可以并行使用此方法而不会发生任何冲突。

This method has been replaced with a .data method that has the same API as jQuery.fn.data. The fact that it's the same API makes it easier to use, its basically a thin wrapper around jQuery.fn.data with namespacing. This allows you to set key/value pair data that is immediatley stored for that plugin only. Multiple plugins can use this method in parallel without any conflicts.

[ publicMethods ]

[publicMethods]

publicMethods对象一直是rep由 Wrap 上定义的任何方法自动公开。您可以直接调用Wrapped对象上的任何方法,但实际上您无权访问包装对象。

The publicMethods object has been replaced by any method being defined on Wrap being automatically public. You can call any method on a Wrapped object directly but you do not actually have access to the wrapped object.

[ $。fn [PLUGIN_NAME] ]

[$.fn[PLUGIN_NAME]]

这已经过重构,因此它会公开更标准化的API。这个api是

This has been refactored so it exposes a more standardized API. This api is

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

选择器中的元素自动包装在 Wrap 对象,调用方法或选择器中的每个选定元素,返回值始终为 $。延期 元素。

the elements in the selector are automatically wrapped in the Wrap object, the method is called or each selected element from the selector and the return value is always a $.Deferred element.

这标准化了API和返回类型。然后,您可以在返回的延期时调用 .then 来获取您关心的实际数据。在这里使用deferred非常强大,可以抽象出插件是同步还是异步。

This standardizes the API and the return type. You can then call .then on the returned deferred to get out the actual data you care about. The use of deferred here is very powerful for abstraction away whether the plugin is synchronous or asynchronous.

_create

添加了缓存创建功能。这被称为将 HTMLElement 转换为Wrapped元素,每个HTMLElement只会被包装一次。这种缓存可以大大减少内存。

A caching create function has been added. This is called to turn a HTMLElement into a Wrapped element and each HTMLElement will only be wrapped once. This caching gives you a solid reduction in memory.

$ .PLUGIN_NAME

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

Added another public method for the plugin (A total of two!).

$.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 * /} 默认为 {}

All parameters are optional. elem defaults to <body>, "methodName" defaults to "PLUGIN_NAME" and {/* options */} defaults to {}.

这个API非常灵活(有14个方法重载!)和足够的标准,可以适应你的插件将暴露的每个方法的syntnax 。

This API is very flexible (with 14 method overloads!) and standard enough to get used to the syntnax for every method your plugin will expose.

公开曝光

Wrap create $ 对象全局公开。这将允许高级插件用户使用您的插件获得最大的灵活性。他们可以在开发中使用 create 和修改后的subbed $ ,他们也可以使用涡卷。这允许挂钩你的插件方法。所有这三个都在他们的名字前面标有 _ ,所以他们是内部的,并使用它们打破了你的插件工作的garantuee。

The Wrap, create and $ objects are exposed globally. This will allow advanced plugin users maximum flexibility with your plugin. They can use create and the modified subbed $ in their development and they can also monkey patch Wrap. This allows for i.e. hooking into your plugin methods. All three of these are marked with a _ in front of their name so they are internal and using them breaks the garantuee that your plugin works.

内部默认对象也公开为 $ .PLUGIN_NAME.global 。这允许用户覆盖您的默认值并设置插件全局默认值。在此插件设置中,当对象与默认值合并时,所有哈希都会进入方法,因此这允许用户为所有方法设置全局默认值。

The internal defaults object is also exposed as $.PLUGIN_NAME.global. This allows users to override your defaults and set plugin global defaults. In this plugin setup all hashes past into methods as objects are merged with the defaults, so this allows users to set global defaults for all your methods.

实际代码

(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);

可以看出你应编辑的代码低于你的代码行。 Wrap 对象的行为类似于内部对象。

As can be seen the code your supposed to edit is below the YOUR CODE line. The Wrap object acts similarly to your Internal object.

函数 main 是使用 $调用的主函数.PLUGIN_NAME() $(选择器).PLUGIN_NAME()并且应包含您的主逻辑。

The function main is the main function called with $.PLUGIN_NAME() or $(selector).PLUGIN_NAME() and should contain your main logic.

这篇关于jQuery插件模板 - 最佳实践,约定,性能和内存影响的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆