jQuery最好的设计在于两个方面,一是它的API,另外一个是它的插件系统。这也是jQuery这些年异军突起的原因,虽然现在遇到了新的危机,Vue,Angular,React的出现给在现代的浏览器开放带来了新的开发体验。
这两年前端库或者框架更新太快,但是如果你是一个真正的实践者,时间长了你慢慢发现其实很多的前端开发框架都是当时编程思想的不同实现。
本人的JavaScript编程能力,也从最初的面向过程的面条式代码,转变到面向对象和使用一些MVC方式来组织代码。废话不多说,进入正题。
[TOC]
定义插件的步骤
首先插件定义时,为了防止外部影响插件,一般插件定义全部放在闭包内部。
1 2 3 4 5 6 7 8 9 10 11 12 ;(function ($ ) { $.fn.step = function (element, options ) { } $.fn.step.prototype = { options: { version: '0.0.1' , name: 'step' } }; })(jQuery)
使用extend扩展jQuery对象(jQuery也是这样扩展而成的)
1 2 3 4 5 6 7 8 9 ;(function ($ ) { $.extend($.fn, { options: { version: '0.0.1' , name: 'step' } }); })(jQuery)
举一个bootstrap插件Alert的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 +function ($ ) { 'use strict' ; var Alert = function (el ) { } Alert.VERSION = '3.3.7' Alert.TRANSITION_DURATION = 150 Alert.prototype.close = function (e ) { } var old = $.fn.alert $.fn.alert = Plugin $.fn.alert.Constructor = Alert $.fn.alert.noConflict = function ( ) { $.fn.alert = old return this } function Plugin (option ) { return this .each(function ( ) { var $this = $(this ) var data = $this .data('bs.alert' ) if (!data) $this .data('bs.alert' , (data = new Alert(this ))) if (typeof option == 'string' ) data[option].call($this ) }) } }(jQuery);
从上面的例子,可以看出定义插件需要如下几个步骤:
定义插件的构造函数。
定义插件的参数(包含公共属性)插件可以通过初始化参数,或者动态的改变参数,来改变自己的状态和行为。
定义插件的行为方法,来具体的控制插件的状态。
插件的公共部分(所有的插件都会涉及,例如:防冲突处理,一般提供noConflict方法;使用DOM-to-Object的桥接模式等)。
栗子:Step插件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 (function ($ ) { function Step (options ) { } Step.VERSION = '0.0.1' ; Step.NAME = 'step' ; Step.prototype = { constructor : Step, // 修正构造函数属性的指向 // 插件参数 options : { }, _init: function (options ) {} }; })(jQuery)
jQuery UI插件编写的方式
上面总结的定义插件步骤其实还不完整,定义一个插件,我们就要想到,插件实例从无到有,还要从有到无。我们定义的插件,需要有一个生命周期管理,而这个生命周期的管理,其实是所有插件都要处理的,其实它是一个公共部分。既然是公共部分(DRY原则),必须是能够重用,而不用重复这部分代码。
如果你多看几个bootstrap的插件,你会发现几乎每一个插件都有如下的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function Plugin (option ) { return this .each(function ( ) { var $this = $(this ) var data = $this .data('plug.name' ) if (!data) $this .data('plug.name' , (data = new Step(this ))) if (typeof option == 'string' ) data[option].call($this ) }); }
1 2 3 4 5 6 7 8 <table name ="step" class ="step" > <tbody > <tr > <td class ="text-center" > 第一步</td > </tr > </tbody > </table >
控件的每一步有三个状态:原始状态,活动状态,焦点状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 (function ($ ) { var baseClass = 'step' , textClass = 'text-center' , activeClass = 'active' , focusClass = 'js-focus' ; $.widget('repay.step' , { options: { VERSION: '0.0.1' , NAME: 'step' , template: '<td></td>' }, _create: function ( ) { if (!this ._haveContainer()) { this ._addContainer(); } this .refresh(); this .$container = this .element.find('tr' ); }, add: function (name ) { $(this .options.template).appendTo(this .$container).text(name); this .refresh(); }, refresh: function ( ) { this .element.addClass(baseClass); this .element.find('td' ).addClass(textClass); }, activate: function (index, onFocus ) { onFocus = typeof onFocus === 'boolean' ? onFocus : true ; var classes = onFocus ? activeClass + " " + focusClass : activeClass; this .element.find('td' ).eq(this ._constrainedIndex(index)) .addClass(classes); }, isFocus: function (index ) { return this .element.find('td' ).eq(this ._constrainedIndex(index)) .hasClass(focusClass); }, isActive: function (index ) { return this .element.find('td' ).eq(this ._constrainedIndex(index)) .hasClass(activeClass); }, _haveContainer: function ( ) { return this .element.find('tr' ).length > 0 ; }, _addContainer: function ( ) { this .element.append('<tbody><tr></tr></tbody>' ); }, _constrainedIndex: function (index ) { var $allSteps = this .element.find('td' ), lastIndex = $allSteps.length - 1 ; if (typeof index !== 'number' ) { index = 0 ; } index = index < 0 ? 0 : index; index = index > lastIndex ? lastIndex : index; return index; }, _destroy: function ( ) { this .element.remove(); }, next: function ( ) { var activeSelector = '.' + activeClass, focusSelector = '.' + focusClass, focusIndex, $allSteps = this .element.find('td' ), maxIndex = $allSteps.length - 1 ; var $focusNode = $allSteps.filter(activeSelector + focusSelector); if ($focusNode.length === 0 ) { this .activate(0 ); this ._trigger('start' , null , { name: $allSteps.first().text(), index: 0 }); } else { focusIndex = this ._constrainedIndex($focusNode.next().index()); $focusNode.removeClass(focusClass) .next('td' ).addClass(activeClass + ' ' + focusClass); if (focusIndex === maxIndex) { this ._trigger('done' , null , { name: $focusNode.text(), index: focusIndex }); } } } }); })(jQuery);
上面的代码控件已经完成了。
1 2 3 4 5 6 7 8 9 10 <table name ="step" class ="step" > <tbody > <tr > <td class ="text-center" > 第1步</td > <td class ="text-center" > 第2步</td > <td class ="text-center" > 第3步</td > </tr > </tbody > </table >
1 2 3 4 5 6 7 8 $('table' ).step('next' ); $('table' ).step('isActive' , 0 ); $('table' ).step('destroy' );
事件
事件是控件的很重要的一部分,事件是控件和外界其它控件解耦的关键。
栗子中next方法:
1 2 3 4 5 6 7 8 9 this ._trigger('start' , null , { name: $allSteps.first().text(), index: 0 }); this ._trigger('done' , null , { name: $focusNode.text(), index: focusIndex });
this._trigger方法是从jQuery.Widget继承的 , 此方法对触发事件做了特殊处理,所以控件对外发布的事件会变成控件名+事件名 。所以start和done事件,对外发布的事件是stepstart和stepdone。
监听事件
事件对外发布之后,我们可以根据控件发布的事件,做一些其它的业务处理。
1 2 3 4 5 6 7 $(document ).on('stepstart' , function (evt, data ) { var args = arguments ; console .log('stepstart' ); }).on('stepdone' , function ( ) { var args = arguments ; console .log('stepdone' ); });
小结
step已经完成了一个完整的控件例子的展示,从create控件建立控件到destory控件,整个生命周期的管理。
一个控件最终要的就是几个部分:
初始化渲染(create)
控件的事件(event)
控件的销毁(destroy)
控件的扩展性(extend)
可配置性(configurable):通过option来动态改变控件的状态。
参考资料 https://www.smashingmagazine.com/2011/10/essential-jquery-plugin-patterns/
http://www.cnblogs.com/timy/archive/2011/04/01/2001871.html