web-dev-qa-db-fra.com

modèle de plug-in jQuery - Meilleures pratiques, convention, performances et impact sur la mémoire

J'ai commencé à écrire quelques plugins jQuery et je me suis dit que ce serait bien de configurer mon IDE avec un template de plugin jQuery. 

Je lisais sur ce site des articles et des articles sur la convention des plugins, le design, etc. et je pensais essayer de consolider tout cela.

Ci-dessous, mon modèle, je cherche à l’utiliser fréquemment, donc je souhaitais qu’il soit conforme à la convention de conception du plug-in jQuery et si l’idée de disposer de plusieurs méthodes internes (ou même de sa conception générale) aurait un impact sur les performances et serait sujette aux 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 d'avance.

59
Ed .

[Edit] 7 mois plus tard

Citant le projet github

jQuery n'est pas bon, et les plugins jQuery ne sont pas comment faire du code modulaire.

Sérieusement, les "plugins jQuery" ne constituent pas une stratégie d'architecture judicieuse. Écrire du code avec une dépendance forte sur jQuery est également idiot.

[Original]

Puisque j'ai critiqué ce modèle, je proposerai une alternative.

Pour rendre la vie plus facile, cela dépend de jQuery 1.6+ et ES5 (utilisez le ES5 Shim ).

J'ai passé un certain temps à repenser le modèle de plug-in que vous avez donné et à déployer le mien.

Liens:

Comparaison:

J'ai restructuré le modèle afin qu'il soit divisé en code général (85%) et en code d'échafaudage (15%). L'intention est que vous n'ayez qu'à modifier le code d'échafaudage et que vous puissiez laisser le code passe-partout intact. Pour y parvenir, j'ai utilisé 

  • héritage _ ​​ [FUNCTION] Au lieu de modifier la classe Internal que vous avez directement, vous devriez modifier une sous-classe. Toutes les fonctionnalités de votre modèle/par défaut doivent être dans une classe de base (appelée Base dans mon code).
  • convention _ ​​ self[PLUGIN_NAME] = main; Par convention, le plugin défini sur jQuery appellera la méthode define sur self[PLUGIN_NAME] par défaut. Ceci est considéré comme la méthode du plugin main et possède une méthode externe distincte pour plus de clarté. 
  • correction de singe _ ​​ $.fn.bind = function _bind ... L'utilisation de la correction de singe signifie que le contournement des événements se fait automatiquement pour vous sous le capot. Cette fonctionnalité est gratuite et n'entraîne pas une perte de lisibilité (appel getEventNS tout le temps).

OO Techniques

Il vaut mieux s'en tenir au bon JavaScript OO plutôt qu'au classique OO émulation. Pour ce faire, vous devez utiliser Object.create . (lequel ES5 utilise simplement la cale pour mettre à niveau les anciens 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);

Ceci est différent de la norme new et .prototype OO les gens sont habitués. Cette approche est préférée car elle renforce le concept selon lequel il n'y a que des objets en JavaScript et qu'il s'agit d'une approche prototype OO.

[getEventNs]

Comme mentionné précédemment, cette méthode a été refondue en surchargeant .bind et .unbind pour injecter automatiquement des espaces de noms. Ces méthodes sont écrasées sur la version privée de jQuery [FUNCTION] . Les méthodes écrasées se comportent de la même manière que votre espacement de noms. Il nomme des événements d'espaces de noms uniquement basés sur le plugin et l'instance d'un plugin wrapper autour d'un HTMLElement (Using .ns .

[getData]

Cette méthode a été remplacée par une méthode .data ayant la même API que jQuery.fn.data. Le fait qu'il s'agisse de la même API facilite son utilisation. Il s'agit essentiellement d'un wrapper fin autour de jQuery.fn.data avec un espacement de nom. Cela vous permet de définir des données de paire clé/valeur qui sont immédiatement stockées pour ce plugin uniquement. Plusieurs plugins peuvent utiliser cette méthode en parallèle sans aucun conflit.

[publicMethods]

L'objet publicMethods a été remplacé par une méthode définie sur Wrap étant automatiquement publique. Vous pouvez appeler n'importe quelle méthode sur un objet Wrapped directement, mais vous n'avez pas réellement accès à cet objet.

[$.fn[PLUGIN_NAME]]

Cela a été refondu pour exposer une API plus standardisée. 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 encapsulés dans l'objet Wrap, la méthode est appelée ou chaque élément sélectionné dans le sélecteur et la valeur de retour est toujours un élément $.Deferred

Cela normalise l'API et le type de retour. Vous pouvez ensuite appeler .then sur le différé renvoyé pour obtenir les données qui vous intéressent. L'utilisation de différé ici est très puissante pour l'abstraction, que le plugin soit synchrone ou asynchrone.

_create

Une fonction de création de cache a été ajoutée. Ceci est appelé pour transformer une - variable - en un élément Wrapped et chaque HTMLElement ne sera enveloppé qu'une fois. Cette mise en cache vous permet de réduire considérablement la mémoire.

$.PLUGIN_NAME

Ajout d'une autre méthode publique pour le plugin (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. HTMLElement par défaut à <body>, "methodName" par défaut à "PLUGIN_NAME" et {/* options */} par défaut à {}.

Cette API est très flexible (avec 14 surcharges de méthodes!) Et suffisamment standard pour s’habituer à la syntnax pour chaque méthode exposée par votre plugin. Exposition publique .

Les objets elem, Wrap et $ sont exposés globalement. Cela permettra aux utilisateurs avancés de plugins de bénéficier d'une flexibilité maximale avec votre plugin. Ils peuvent utiliser create et le sous-code modifié $ dans leur développement et ils peuvent également utiliser le patch singe create. Cela permet d’être connecté à vos méthodes de plug-in. Tous les trois sont marqués d'un _ devant leur nom, ils sont donc internes et leur utilisation annule la garantie du bon fonctionnement de votre plugin.

L'objet interne Wrap est également exposé en tant que $.PLUGIN_NAME.global. Cela permet aux utilisateurs de remplacer vos valeurs par défaut et de définir le plugin global defaults. Dans ce plugin, configurez tous les hachages passés aux méthodes lorsque les objets sont fusionnés avec les valeurs par défaut. Cela permet donc aux utilisateurs de définir des valeurs globales par défaut pour toutes vos méthodes.

Code actuel

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

La fonction Internal est la fonction principale appelée avec var self = Object.create(Base) ou $.sub() et doit contenir votre logique principale. 

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

26
Raynos

Il y a quelque temps, j'ai créé un générateur de plug-in basé sur un article de blog que j'ai lu: http://jsfiddle.net/KeesCBakker/QkPBF/ . Cela pourrait être utile. C'est assez basique et simple. Tous les commentaires seraient les bienvenus. 

Vous pouvez créer votre propre générateur et le modifier selon vos besoins.

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

(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);
28
Kees C. Bakker

Je suis allé googler et atterrir ici donc, je dois poster quelques idées: je suis d’abord d’accord avec @Raynos. 

La plupart des codes qui essaient de créer un plugin jQuery ne sont pas des plugins! C'est juste un objet stocké en mémoire qui est référencé par la propriété data d'un nœud/élément. C’est parce que jQuery devrait être vu et utilisé comme un outil côte à côte avec une bibliothèque de classes (pour remédier aux incohérences de js de l’architecture OO) afin de construire un meilleur code et oui c’est pas mal du tout!

Si vous n'aimez pas le comportement classique OO, utilisez une bibliothèque de prototypes comme clone .

Alors, quelles sont vraiment nos options?

  • utilisez JQueryUI/Widget ou une bibliothèque similaire qui cache des détails techniques et fournit une abstraction
  • ne les utilisez pas à cause de la complexité, de la courbe d'apprentissage et dieu sait les changements à venir
  • ne les utilisez pas parce que vous voulez insister sur la conception modulaire, construisez une petite augmentation plus tard
  • ne les utilisez pas car vous voudrez peut-être porter/connecter votre code avec différentes bibliothèques.

Supposons les problèmes traités par le scénario suivant (voir les complexités de cette question: Quel modèle de conception de plug-in jQuery dois-je utiliser? ):

nous avons des nœuds A, B et C qui stockent une référence d'objet dans leur propriété data

certains d'entre eux stockent des informations en public et privé accessible objets internes, Certaines classes de ces objets sont liées à inheritance, tous ces noeuds ont également besoin de certains et public singletons privés pour fonctionner au mieux. 

Que ferions nous? Voir le tirage au sort:

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 constater, chaque nœud fait référence à un objet - une approche jQuery - mais ces objets changent brutalement. ils contiennent des propriétés d'objet avec différentes données stockées dans ou même des singletons qui devraient être ... uniques en mémoire, comme les fonctions prototypes des objets. Nous ne voulons pas que chaque fonction appartenant à class A soit répétée dupliquée en mémoire dans l'objet de chaque nœud!

Avant ma réponse voyez une approche commune que j'ai vue dans les plugins jQuery - certains d'entre eux sont très populaires mais je ne dis pas de noms:

(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 des diapositives sur: http://www.slideshare.net/benalman/jquery-plugin-creation de Ben Alman où il fait référence à la diapositive 13 à littéraux d'objet comme singletons et cela me renverse: c'est ce que le plugin ci-dessus fait, il crée un singleton avec aucune chance que ce soit pour modifier son état interne !!!

De plus, dans la partie jQuery, il stocke un référence commune à chaque nœud!

Ma solution utilise une fabrique pour conserver l'état interne et renvoyer un objet. Il peut également être développé avec une bibliothèque classe et scindé 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/

0
centurian

Que diriez-vous quelque chose comme ça ? C'est beaucoup plus clair, mais encore une fois, il serait agréable d'entendre vos commentaires si vous pouvez l'améliorer sans trop compliquer sa simplicité.

// 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
0
DevWL