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



Я начал писать несколько плагинов jQuery и решил, что было бы неплохо настроить мою IDE с помощью шаблона плагина jQuery.



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



Ниже приведен мой шаблон, я ищу, чтобы использовать его часто так стремился, чтобы убедиться, что он в целом соответствует конвенции jQuery plugin design и ли идея иметь несколько внутренних методы (или даже его общий дизайн) будут влиять на производительность и склонны к проблемам с памятью.



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


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

444   4  

4 ответов:

[Edit] 7 месяцев

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

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

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

[оригинальный]

так как я дал критику об этом шаблоне я предложу альтернатива.

чтобы сделать жизнь проще, это зависит от jQuery 1.6+ и ES5 (используйте ES5 Shim).

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

ссылки:

для сравнения:

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

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

методы ОО

лучше придерживаться правильного 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);

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

[getEventNs]

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

[getData]

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

[publicMethods]

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

[$.fn[PLUGIN_NAME]]

это было рефакторинг, поэтому он предоставляет более стандартизированный API. Этот api является

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

элементы селектора автоматически завернуты в Wrap объект, вызывается метод или каждый выбранный элемент из селектора и возвращаемое значение всегда а $.Deferred элемент.

это стандартное API и тип возвращаемого значения. Затем вы можете позвонить .then на возвращенном отложенном, чтобы получить фактические данные, о которых вы заботитесь. Использование отложенного здесь очень мощно для абстрагирования от того, является ли плагин синхронным или асинхронным.

_create

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

$.PLUGIN_NAME

добавил еще один публичный метод для плагина (всего два!).

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

все параметры являются необязательными. elem по умолчанию <body>,"methodName" по умолчанию "PLUGIN_NAME" и {/* options */} по умолчанию {}.

этот API очень гибкий (с 14 перегрузками метода!) и достаточно стандартный, чтобы привыкнуть к syntnax для каждого метода, который ваш плагин будет выставлять.

общественного воздействия

The Wrap,create и $ объекты подвергаются во всем мире. Это позволит продвинутым пользователям плагинов максимально гибко работать с вашим плагином. Они могут использовать create и модифицированная подложка $ в их развитии, и они также могут обезьяна патч Wrap. Это позволяет т. е. подключаться к вашим методам плагина. Все три из них отмечены знаком _ перед их именем, так что они являются внутренними и с их помощью ломает гарантию, что ваш плагин работает.

внутренние defaults объект также отображается как $.PLUGIN_NAME.global. Это позволяет пользователям переопределить настройки по умолчанию и установить плагин 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 объект действует аналогично вашему

некоторое время назад я построил генератор плагинов на основе статьи в блоге, которую я прочитал: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);

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

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

Если вам не нравится классическое поведение OO, придерживайтесь прототипной библиотеки, такой как клон.

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

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

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

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

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

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

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

как вы можете видеть каждый узел ссылается на объект - подход jQuery - но эти объекты изменяются дико; они содержат свойства объекта с различными данными, хранящимися в или, даже синглетах, которые должны быть...один в памяти как прототип функции объектов. Мы не хотим, чтобы функция каждого объекта принадлежала class A неоднократно дублируется в в объекте каждого узла!

перед моим ответом смотрите общий подход, который я видел в плагинах jQuery - некоторые из них очень популярны, но я не произноси имен:

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

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

}(jQuery, window, document));

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

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

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

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

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

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

Comments

    Ничего не найдено.