шаблон плагина 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);
спасибо заранее.
4 ответов:
[Edit] 7 месяцев
цитата из проекта github
jQuery не годится, и плагины jQuery-это не то, как сделать модульный код.
серьезно "Плагины jQuery" не являются надежной стратегией архитектуры. Написание кода с жесткой зависимостью от jQuery также глупо.
[оригинальный]
так как я дал критику об этом шаблоне я предложу альтернатива.
чтобы сделать жизнь проще, это зависит от
jQuery1.6+ и ES5 (используйте ES5 Shim).я потратил некоторое время на перепроектирование шаблона плагина, который вы дали, и развернул свой собственный.
ссылки:
- Github
- документация
- тесты подтверждено, чтобы пройти в FF4, Chrome и IE9 (IE8 & OP11 умирает. известный ошибка).
- Аннотированный Исходный Код
- плагин PlaceKitten example
для сравнения:
я переработал шаблон так, что он разделен на шаблонный (85%) и строительный код (15%). Намерение состоит в том, что вам нужно только отредактировать код лесов, и вы можете оставить код шаблона нетронутым. Для достижения этой цели я использовал
- наследование
var self = Object.create(Base)вместо того, чтобы редактироватьInternalкласс у вас есть непосредственно вы должны редактировать подкласс. Все ваши функции шаблона / по умолчанию должны быть в базовом классе (называетсяBaseв моем коде).self[PLUGIN_NAME] = main;по соглашению плагин, определенный на jQuery, вызовет метод define onself[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на возвращенном отложенном, чтобы получить фактические данные, о которых вы заботитесь. Использование отложенного здесь очень мощно для абстрагирования от того, является ли плагин синхронным или асинхронным.добавлена функция создания кэширования. Это называется повернуть
HTMLElementв завернутый элемент и каждый HTMLElement будут обернуты только один раз. Это кэширование дает вам значительное сокращение памяти.добавил еще один публичный метод для плагина (всего два!).
$.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. Это позволяет пользователям переопределить настройки по умолчанию и установить плагин globaldefaults. В этой настройке плагина все хэши проходят в методы, поскольку объекты объединяются с настройками по умолчанию, поэтому это позволяет пользователям устанавливать глобальные значения по умолчанию для всех ваших методов.(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