使用jQuery UI widget编写有状态的插件

jQuery最好的设计在于两个方面,一是它的API,另外一个是它的插件系统。这也是jQuery这些年异军突起的原因,虽然现在遇到了新的危机,Vue,Angular,React的出现给在现代的浏览器开放带来了新的开发体验。

这两年前端库或者框架更新太快,但是如果你是一个真正的实践者,时间长了你慢慢发现其实很多的前端开发框架都是当时编程思想的不同实现。

本人的JavaScript编程能力,也从最初的面向过程的面条式代码,转变到面向对象和使用一些MVC方式来组织代码。废话不多说,进入正题。

[TOC]

定义插件的步骤

首先插件定义时,为了防止外部影响插件,一般插件定义全部放在闭包内部。

  • 定义插件的闭包
1
2
3
(function () {
// define you plugin
})()
  • 基于jQuery来定义插件
1
2
3
4
5
6
7
8
9
10
11
12
// 以step插件为例
;(function ($) {
$.fn.step = function (element, options) {
// do something
}
$.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
// 以Alert为例
+function ($) {
'use strict';

// ALERT CLASS DEFINITION
// ======================
// Alert构造函数
var Alert = function (el) {
}
// 公共属性
Alert.VERSION = '3.3.7'
Alert.TRANSITION_DURATION = 150

Alert.prototype.close = function (e) {
}

// 备份jQuery的prototype上已经有的alert
// 为了后面防冲突处理使用.
var old = $.fn.alert
$.fn.alert = Plugin
$.fn.alert.Constructor = Alert


// ALERT NO CONFLICT
// =================
// 防冲突处理
$.fn.alert.noConflict = function () {
$.fn.alert = old
return this
}


// ALERT PLUGIN DEFINITION
// =======================
// DOM-TO-Object的桥接模式
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);

从上面的例子,可以看出定义插件需要如下几个步骤:

  1. 定义插件的构造函数。
  2. 定义插件的参数(包含公共属性)插件可以通过初始化参数,或者动态的改变参数,来改变自己的状态和行为。
  3. 定义插件的行为方法,来具体的控制插件的状态。
  4. 插件的公共部分(所有的插件都会涉及,例如:防冲突处理,一般提供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
// Step插件
(function ($) {
/**
*@param {Object} options 动态参数
*/
function Step (options) {
// do something
}

// 公共属性
Step.VERSION = '0.0.1';
Step.NAME = 'step';

// 原型定义,所有的实例(instance)都会拥有的方法
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
 // ALERT PLUGIN DEFINITION
// =======================
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)
});
}

// 这段代码之所以会重复出现,是因为bootstrap的设计者,想减少bootstrap对其它库的依赖。
// 这段代码在$.widget.bridge中有非常巧妙的实现(jquery.ui.widget.js或者widget.js)。
  • 使用$.Widget对象扩展控件

    jQuery widget factory 帮我们做了很多事情,尤其一些插件公共部分的生命周期管理,都帮我们实现了,所以创建一个jQuery UI组件从widget factory开始。

    jQueryUI的widget公共模块,只依赖jQuery,没有和jQueryUI的core模块耦合,是一个纯粹的工厂函数实现,很方便定义自己的ui组件。

    举个栗子:

    1
    2
    3
    $.widget('repay.step', {
    _create: function () {}
    })
  • 命名空间(namespace): ‘repay.step’,其中repay是namespace;step是控件名称。ui 命名空间默认留给jquery.ui使用了。

1
2
// 调用
$('table').step();
  • 继续step的例子
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
/**
* @date 2017-05-08
* @author lijunnan
*/
(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
<!-- 模版1 --> 
<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
// 控件的初始化调用
$('table').step(); // 这样就创建了step控件实例
  • 控件的方法调用
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
// 触发的start
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控件,整个生命周期的管理。

一个控件最终要的就是几个部分:

  1. 初始化渲染(create)
  2. 控件的事件(event)
  3. 控件的销毁(destroy)
  4. 控件的扩展性(extend)
  5. 可配置性(configurable):通过option来动态改变控件的状态。

参考资料

https://www.smashingmagazine.com/2011/10/essential-jquery-plugin-patterns/

http://www.cnblogs.com/timy/archive/2011/04/01/2001871.html