Шаблон плагина jQuery - лучшие практики, соглашения, производительность и влияние на память - PullRequest
59 голосов
/ 12 мая 2011

Я начал писать несколько плагинов 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);

Заранее спасибо.

Ответы [ 4 ]

28 голосов
/ 09 июня 2011

Некоторое время назад я построил генератор плагинов на основе статьи в блоге, которую я прочитал: http://jsfiddle.net/KeesCBakker/QkPBF/. Это может быть полезно.Это довольно простой и прямой.Любые комментарии будут очень приветствоваться.

Вы можете раскошелиться на собственный генератор и изменить его под свои нужды.

Ps.Это сгенерированное тело:

(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);
26 голосов
/ 08 июня 2011

[Изменить] 7 месяцев спустя

Цитата из проекта github

jQuery не годится, а плагины jQuery - не то, что делает модульный код.

Серьезно, «плагины jQuery» не являются стратегией разумной архитектуры. Написание кода с жесткой зависимостью от jQuery также глупо.

[оригинал]

Поскольку я дал критическую оценку этому шаблону, я предложу альтернативу.

Чтобы упростить жизнь, используются jQuery 1.6+ и ES5 (используйте ES5 Shim ).

Я потратил некоторое время на переработку шаблона плагина, который вы дали, и выкатил свой собственный.

Ссылки:

Сравнение:

Я реорганизовал шаблон так, чтобы он был разбит на шаблон (85%) и код лесов (15%). Предполагается, что вам нужно всего лишь отредактировать код скаффолдинга, и вы можете оставить стандартный код без изменений. Для этого я использовал

  • наследование var self = Object.create(Base) Вместо того, чтобы редактировать класс Internal, который у вас есть, вы должны редактировать подкласс. Все ваши функции шаблона / по умолчанию должны быть в базовом классе (в моем коде он называется Base).
  • Convention self[PLUGIN_NAME] = main; По соглашению плагин, определенный в jQuery, будет вызывать метод define по self[PLUGIN_NAME] по умолчанию. Это рассматривается как метод плагина main и имеет отдельный внешний метод для ясности.
  • исправление обезьян $.fn.bind = function _bind ... Использование исправлений обезьян означает, что пространство имен событий выполняется автоматически для вас. Эта функциональность бесплатна и не достигается за счет читабельности (звоните getEventNS все время).

OO Techniques

Лучше придерживаться правильного JavaScript OO, чем классической OO-эмуляции. Для этого вы должны использовать Object.create. (какие ES5 просто используют прокладку для обновления старых браузеров).

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

Это отличается от стандартных OO, к которым привыкли люди на основе new и .prototype. Этот подход предпочтителен, потому что он подтверждает концепцию, что в JavaScript есть только объекты, и это прототип ОО-подхода.

[getEventNs]

Как уже упоминалось, этот метод был подвергнут рефакторингу путем переопределения .bind и .unbind для автоматического внедрения пространств имен. Эти методы перезаписываются в приватной версии jQuery $.sub(). Перезаписанные методы ведут себя так же, как ваше пространство имен. Он именует события уникальным образом на основе плагина и экземпляра оболочки плагина вокруг HTMLElement (с использованием .ns.

[getData]

Этот метод был заменен методом .data, который имеет тот же API, что и jQuery.fn.data. Тот факт, что это тот же API, облегчает его использование, в основном это тонкая оболочка jQuery.fn.data с пространством имен. Это позволяет вам установить данные пары ключ / значение, которые сразу же сохраняются только для этого плагина. Несколько плагинов могут использовать этот метод параллельно без каких-либо конфликтов.

[publicMethods]

Объект publicMethods был заменен любым методом, определяемым в Wrap, который автоматически становится открытым. Вы можете напрямую вызывать любой метод для объекта Wrapped, но у вас нет доступа к этому объекту.

[$.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 для возвращенной отсрочки, чтобы получить фактические данные, которые вам нужны. Использование отсроченного здесь очень эффективно для абстракции вне зависимости от того, является ли плагин синхронным или асинхронным.

_create

Добавлена ​​функция создания кэширования. Это вызывается для превращения HTMLElement в элемент Wrapped, и каждый элемент HTMLE будет упакован только один раз. Это кэширование дает вам существенное сокращение памяти.

$.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 перегрузками методов!) И достаточно стандартным, чтобы привыкнуть к syntnax для каждого метода, который будет выставлен вашим плагином.

Общественное облучение

Объекты Wrap, create и $ выставляются глобально. Это позволит продвинутым пользователям плагинов максимально гибко работать с вашим плагином. Они могут использовать create и модифицированное подразделение $ в своей разработке, и они также могут использовать обезьяну патч 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(), и должна содержать вашу основную логику.

0 голосов
/ 16 января 2016

Как насчет этого?Это гораздо понятнее, но опять же было бы приятно услышать от вас, если вы можете улучшить его, не усложняя его простоту.

// 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 голосов
/ 06 декабря 2013

Я гуглил и приземлился здесь, поэтому я должен опубликовать несколько идей: сначала я согласен с @Raynos.

Большая часть кода, который пытается создать плагин jQuery на самом деле ... это не плагин!Это просто объект, хранящийся в памяти, на который ссылается свойство data узла / элемента.Это потому, что jQuery следует рассматривать и использовать в качестве инструмента рядом с библиотекой классов (чтобы исправить несоответствия js в архитектуре OO) для создания лучшего кода, и да, это совсем не плохо!

Если вы неКак классическое поведение ОО, придерживайтесь прототипной библиотеки, такой как clone .

Так что же на самом деле наши варианты?

  • использовать JQueryUI / Widget или подобную библиотеку, которая скрываеттехнические детали и обеспечивает абстракцию
  • не используйте их из-за сложностей, кривой обучения и бог знает будущие изменения
  • не используйте их, потому что вы хотите настаивать на модульном дизайне, строить небольшие увеличенияпозже
  • не используйте их, потому что вы, возможно, захотите перенести / соединить ваш код с разными библиотеками.

Предположим, что проблемы решены в следующем сценарии (см. сложности этого вопроса: Какой шаблон проектирования плагинов jQuery мне следует использовать? ):

у нас есть узлы A, B и C, которые хранят объектВ свою data собственность

некоторые из них хранят информацию в общедоступных и частных доступных внутренних объектах , некоторые классыэти объекты связаны с наследованием , все эти узлы также нуждаются в некоторых частных и public singletons для лучшей работы.

Что бы мы делали?Посмотрите на рисунок:

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

Я смотрел несколько слайдов по адресу: http://www.slideshare.net/benalman/jquery-plugin-creation от Бена Алмана, где он ссылается на слайде 13 на литералы объекта как синглеты , и это просто сбивает меня с толку: этоэто то, что делает вышеупомянутый плагин, он создает один синглтон с без шансов вообще , чтобы изменить его внутреннее состояние !!!

Кроме того, в части jQuery он хранит общая ссылка на каждый узел!

Мое решение использует factory для поддержания внутреннего состояния и возврата объектаплюс его можно расширить с помощью библиотеки class и разделить на несколько файлов:

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...