jQuery插件模板 – 最佳实践,惯例,性能和内存影响
我已经开始编写几个jQuery插件,并认为使用jQuery插件模板设置我的IDE会很好。
我一直在阅读有关插件公约,devise等相关的一些文章和post,并认为我会尝试和巩固所有这一切。
下面是我的模板,我期待着经常使用它,所以我很想确保它通常符合jQuery插件devise惯例,以及是否有多个内部方法(甚至是其一般devise)的想法会影响性能,并容易出现内存问题。
(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, eg 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);
提前致谢。
[编辑] 7个月后
从github项目引用
jQuery是不好的,jQuery插件是不是如何做模块化的代码。
认真的“jQuery插件”不是一个健全的架构战略。 编写对jQuery的依赖性很强的代码也很愚蠢。
[原版的]
既然我对这个模板给予了批评,我会提出一个替代scheme。
为了让生活更轻松,这依赖于jQuery
1.6+和ES5(使用ES5 Shim )。
我花了一些时间重新devise你给出的插件模板,并推出了我自己的。
链接:
- Github上
- 文档
- unit testing确认通过FF4,Chrome和IE9(IE8和OP11死亡。已知错误 )。
- 注释的源代码
- PlaceKitten示例插件
比较:
我重构了模板,以便将其分成样板(85%)和脚手架代码(15%)。 意图是你只需要编辑脚手架代码,你可以保持不变的代码。 为了达到这个我用过了
- inheritance
var self = Object.create(Base)
而不是直接编辑Internal
类,你应该编辑一个子类。 所有您的模板/默认function应该在一个基类(在我的代码中称为Base
)。 - 会议
self[PLUGIN_NAME] = main;
按照惯例,在jQuery上定义的插件默认会调用self[PLUGIN_NAME]
上的方法define。 这被认为是main
插件方法,并且为了清楚而具有单独的外部方法。 - 猴子补丁
$.fn.bind = function _bind ...
猴子补丁的使用意味着事件命名空间是自动为你做的。 这个function是免费的,不会以可读性为代价(始终调用getEventNS
)。
OO技术
坚持正确的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
]
这个方法已经被replace为一个.data
方法,它和jQuery.fn.data
具有相同的API。 事实上,它是相同的API使它更容易使用,它基本上是jQuery.fn.data
与命名空间的薄包装。 这使您可以设置立即为该插件存储的键/值对数据。 多个插件可以并行使用这个方法,没有任何冲突。
[ publicMethods
]
publicMethods对象已被自动公开定义的任何方法取代。 您可以直接调用Wrapped对象上的任何方法,但实际上并没有访问包装对象的权限。
[ $.fn[PLUGIN_NAME]
]
这已被重构,所以它暴露了一个更标准化的API。 这个API是
$(selector).PLUGIN_NAME("methodName", {/* object hash */}); // OR $(selector).PLUGIN_NAME({/* object hash */}); // methodName defaults to PLUGIN_NAME
select器中的元素被自动包装在Wrap
对象中,方法被调用,或者从select器中select每个元素,并且返回值总是$.Deferred
元素。
这标准化了API和返回types。 然后你可以调用返回的延迟来获取你关心的实际数据。 这里延迟的使用对抽象是非常强大的,不pipe插件是同步的还是asynchronous的。
_create
已添加caching创buildfunction。 这被称为将一个HTMLElement
转换成一个Wrapped元素,每个HTMLElement只会被包装一次。 这个caching可以让你的内存减less很多。
$.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
。 这允许即挂钩到您的插件方法。 所有这三个在他们的名字前都标有_
,所以他们是内部的,使用它们打破了你的插件工作的garantuee。
内部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. ie 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()
调用的主函数,并且应该包含您的主逻辑。
一段时间后,我已经build立了一个基于博客文章的插件生成器我已经阅读: 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);
我一直在谷歌search,所以我必须发表一些想法:首先我同意@Raynos。
那里试图build立一个jQuery插件的代码实际上…不是一个插件! 它只是一个存储在内存中的对象,它是由一个节点/元素的数据属性引用的。 这是因为应该将jQuery看作是一个与类库并排使用的工具(为了弥补OO体系结构中的js不一致性)来构build更好的代码,而且这一点都不错!
如果你不喜欢古典的OO行为,坚持像克隆这样的原型库。
那么我们的select是什么?
- 使用JQueryUI / Widget或类似的库来隐藏技术并提供抽象
- 不要因为复杂而使用它们,学习曲线和上帝知道未来的变化
- 不要使用它们,因为你想坚持模块化devise,以后再做小幅增加
- 不要使用它们,因为您可能需要将代码移植到不同的库中。
假设以下场景解决的问题(请参阅这个问题的复杂性: 我应该使用哪个jQuery插件devise模式? ):
我们有节点A,B和C,将对象引用存储到它们的
data
属性中其中一些将信息存储在公共和私人可访问的内部对象中 ,这些对象的一些类与inheritance关联,所有这些节点也需要一些私有和公共单例来最好地工作。
我们会做什么? 看画面:
classes : | ABC ------------------case 1---------- members | | | | of | vvv 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 | vvv 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 | vvv an object | var d= D.getInstance() (private), at | e= E.getInstance() (public) node Z : | D, E : Singletons
正如你所看到的,每个节点都指向一个对象 – 一个jQuery的方法 – 但是这些对象变化无常; 它们包含存储在或中的不同数据的对象属性,甚至是应该单独存储在内存中的单例,比如对象的原型函数。 我们不希望每个对象的属于class A
的函数在每个节点的对象的内存中被重复地复制 !
在我的回答之前,我看到了我在jQuery插件中看到的一种常见方法 – 其中一些非常stream行,但我不说名字:
(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));
我正在看一些幻灯片,来自Ben Alman的http://www.slideshare.net/benalman/jquery-plugin-creation ,他在幻灯片13中提到将单词作为单例来对待,而这只是把我敲了过来:这就是上面的插件呢,它创造了一个单身, 没有机会改变它的内部状态!
而且,在jQuery部分,它存储了每个节点的公共引用 !
我的解决scheme使用一个工厂来保持内部状态并返回一个对象,另外它可以用一个类库扩展并分割成不同的文件:
;(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 Templet (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 what ever 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