59 votes

plugin jQuery modèle de meilleure pratique, de la convention, la performance et l'impact de mémoire

J'ai commencé à écrire quelques plugins jQuery et pensé qu'il serait bien pour l'installation de mon IDE avec un plugin jQuery template.

J'ai lu quelques articles et de posts sur ce site plugin convention, la conception, etc.. et j'ai pensé essayer et de consolider tout cela.

Ci-dessous est mon modèle, je suis à la recherche de l'utiliser fréquemment, a tenu à s'assurer qu'il est généralement conforme aux plugin jQuery conception de la convention et si l'idée d'avoir plusieurs méthodes internes (ou même à sa conception générale) aurait un impact sur les performances et être sujettes à des problèmes de mémoire.

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

Merci à l'avance.

28voto

Kees C. Bakker Points 7504

Un temps, j'ai un plugin générateur basé sur un article de blog que j'ai lu: http://jsfiddle.net/KeesCBakker/QkPBF/. Il pourrait être utile. Il est assez basique et simple. Tout commentaire serait le bienvenu.

Vous pouvez fourche votre propre générateur et de le modifier pour vos besoins.

Ps. C'est le générés corps:

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

26voto

Raynos Points 82706

[Edit] 7 mois plus tard

Citant le projet github

jQuery n'est pas bon, et les plugins jQuery est pas la façon de faire du code modulaire.

Au sérieux "plugins jQuery" ne sont pas un bruit de l'architecture de la stratégie. L'écriture de code avec une dépendance matérielle sur jQuery est aussi absurde.

[Original]

Depuis que j'ai donné critique sur ce modèle, je vais proposer une alternative.

Pour rendre plus facile de vivre cela dépend de l' jQuery 1.6+ et ES5 (utiliser l' ES5 Cale).

J'ai passer un peu de temps de re-concevoir le plugin modèle que vous avez donné et lancé mon propre.

Liens:

Comparaison:

J'ai refait le modèle de sorte qu'elle est divisée en standard (85%) et de l'échafaudage code (15%). L'intention est que vous n'avez qu'à éditer les échafaudages de code et vous pouvez garder le congé de code réutilisable intacte. Pour réaliser cela, j'ai utilisé

  • l'héritage var self = Object.create(Base) Plutôt d'édition de l' Internal classe que vous avez directement, vous devriez être en train de modifier une sous-classe. Tous les modèles / par défaut la fonctionnalité doit être dans une classe de base (appelés Base dans mon code).
  • convention - self[PLUGIN_NAME] = main; , Par convention, le plugin défini sur jQuery qui fera appel à la méthode de définir sur self[PLUGIN_NAME] par défaut. Ceci est considéré comme l' main plugin méthode et a une autre méthode externe pour plus de clarté.
  • monkey patching $.fn.bind = function _bind ... Utilisation de monkey patching signifie que l'événement namespacing est fait automatiquement pour vous sous le capot. Cette fonctionnalité est gratuite et ne pas se faire au détriment de la lisibilité (appelant getEventNS tout le temps).

OO Techniques

Il est préférable de s'en tenir à la bonne JavaScript OO plutôt classique OO émulation. Pour ce faire, vous devez utiliser Object.create. (ES5 suffit d'utiliser la cale de mise à niveau de vieux navigateurs).

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

C'est différent de la norme new et .prototype basé OO les gens sont habitués. Cette approche est privilégiée parce qu'elle renforce l'idée qu'il y a seulement des Objets en JavaScript et c'est un prototype OO approche.

[getEventNs]

Comme mentionné cette méthode a été reconstruit à l'écart par des raisons impérieuses .bind et .unbind pour injecter automatiquement les espaces de noms. Ces méthodes sont écrasés sur le privé, la version de jQuery $.sub(). L'écrasé méthodes se comportent de la même manière que votre namespacing. Il espaces de noms événements uniquement basé sur le plugin et l'exemple d'un plugin wrapper autour d'une HTMLElement (à l'Aide d' .ns.

[getData]

Cette méthode a été remplacée par un .data méthode qui a la même API que jQuery.fn.data. Le fait que c'est la même API rend plus facile à utiliser, c'est en fait un wrapper mince autour d' jQuery.fn.data avec namespacing. Cela vous permet de définir une paire clé/valeur de données qui est immédiatement stockée pour ce plugin. Plusieurs plugins peuvent utiliser cette méthode en parallèle, sans aucun conflit.

[publicMethods]

Le publicMethods objet a été remplacé par n'importe quelle méthode étant définie en Wrap automatiquement public. Vous pouvez appeler n'importe quelle méthode sur un objet Enveloppé directement, mais vous n'avez pas réellement avoir accès à l'objet enveloppé.

[$.fn[PLUGIN_NAME]]

Cela a été refait donc, il expose plus uniformes et de l'API. Cette api est

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

les éléments du sélecteur sont automatiquement enveloppé dans l' Wrap de l'objet, la méthode est appelée ou chaque élément sélectionné dans le sélecteur et la valeur de retour est toujours un $.Deferred élément.

Cette normalise l'API et le type de retour. Vous pouvez ensuite appeler .then sur le revenu reportés pour obtenir les données qui vous intéressent. L'utilisation de reportées ici est très puissant pour l'abstraction loin si le plugin est synchrone ou asynchrone.

_create

Une mise en cache de créer la fonction a été ajoutée. Voilà ce qui s'appelle tourner un HTMLElement Enveloppé dans un élément et de chaque HTMLElement ne seront enveloppés d'une fois. Cette mise en cache vous donne une solide réduction de la mémoire.

$.PLUGIN_NAME

A ajouté une autre méthode publique pour le plugin (pour Un total de deux!).

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

Tous les paramètres sont facultatifs. elem par défaut est <body>, "methodName" par défaut est "PLUGIN_NAME" et {/* options */} par défaut est {}.

Cette API est très souple (14 surcharges de méthode!) et standard suffisant pour s'habituer à la syntnax pour chaque méthode de votre plugin pour exposer.

L'exposition du Public

L' Wrap, create et $ des objets sont exposés dans le monde. Ceci permettra à l'avancée du plugin utilisateurs un maximum de flexibilité avec votre plugin. Ils peuvent utiliser create et de la modification de la subbed $ de leur développement, et ils peuvent également monkey patch Wrap. Cela permet par exemple d'accrochage dans votre plugin méthodes. Tous les trois de ceux-ci sont marqués avec un _ en face de leur nom, de sorte qu'ils sont à l'intérieur et à l'aide de leur casse la garantuee que votre plugin fonctionne.

L'interne defaults objet est également exposées en $.PLUGIN_NAME.global. Cela permet aux utilisateurs de modifier vos paramètres par défaut et définir plugin global defaults. Dans ce plugin installation de tous les hachages passé dans les méthodes que les objets sont fusionnées avec les valeurs par défaut, ce qui permet aux utilisateurs de définir des valeurs globales par défaut pour toutes vos méthodes.

Code

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

Comme peut être vu le code de votre soi-disant à modifier est en dessous de la YOUR CODE ligne de. L' Wrap objet agit de même pour votre Internal objet.

La fonction main est la principale fonction est appelée avec $.PLUGIN_NAME() ou $(selector).PLUGIN_NAME() et devrait contenir votre logique principale.

-1voto

centurian Points 131

J'ai cherché sur google et a atterri ici, donc, j'ai poster quelques idées: tout d'abord je suis d'accord avec @Raynos.

La plupart du code qui tente de créer un plugin jQuery en fait...ce n'est pas un plugin! C'est juste un objet stocké dans la mémoire qui est référencé par la propriété de données d'un nœud/élément. C'est parce que jQuery doit être considéré et utilisé comme un outil à côté d'une bibliothèque de classe (pour remédier à js incohérences d'architecture OO) pour construire un meilleur code et oui ce n'est pas mal du tout!

Si vous n'aimez pas classique OO comportement s'en tenir à un prototypes de la bibliothèque comme clone.

Donc, ce que nos options vraiment?

  • l'utilisation de JQueryUI/Widget ou une bibliothèque semblable, qui cache des aspects techniques et des fournit l'abstraction
  • ne pas utiliser en raison de la complexité, de la courbe d'apprentissage et dieu sait que les changements futurs
  • ne pas les utiliser parce que vous voulez insister sur la conception modulaire, construire des petits-augmenter plus tard
  • ne pas les utiliser parce que vous pourriez portage/connexion de votre code avec les différentes bibliothèques.

Supposons que les questions traitées par le scénario suivant (voir la complexité de cette question: Qui plugin jQuery design pattern dois-je utiliser?):

nous avons les nœuds A, B et C que de stocker un objet de référence dans leur data de la propriété

certains d'entre eux de stocker les infos dans le public et le privé accessible objets internes, certaines classes de ces objets sont connectés avec l'héritage, l'ensemble de ces nœuds ont aussi besoin d'être privé et public singletons la meilleure.

Que ferions-nous? Voir la drawning:

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

comme vous pouvez le voir, chaque nœud se réfère à un objet jQuery approche - mais ces objets changent de wildely; ils contiennent des objets-propriétés différentes des données stockées dans les ou de, même des singletons qui devrait être...seul dans la mémoire comme le prototype des fonctions des objets. Nous ne voulons pas que chaque objet en fonction de l'appartenance à l' class A à plusieurs reprises dupliqué en mémoire dans chaque noeud de l'objet!

Avant ma réponse voir une approche commune que j'ai vu dans les plugins jQuery, très populaire, mais je ne dis pas les noms de:

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

Je regardais quelques diapositives à: http://www.slideshare.net/benalman/jquery-plugin-creation de Ben Alman où il se réfère à la diapositive 13 pour objet de littéraux comme des singletons et qui vient de me frapper en plus: c'est ce que le plugin ci-dessus, il crée un singleton avec aucune chance que ce soit pour modifier son état interne!!!

En outre, à l'jQuery partie il stocke une référence commune à chaque nœud unique!

Ma solution utilise une usine à maintenir en interne de l'état et de retourner un objet, plus il peut être étendue avec une classe de la bibliothèque et de la séparer en différents fichiers:

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

Mon projet: https://github.com/centurianii/jsplugin

Voir: http://jsfiddle.net/centurianii/s4J2H/1/

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X