众所周知,微信自从提出开发小程序后,就成为了一个可以比肩Goggle、Apple的具有平台性质的超级app。同样的,jQuery最成功的地方莫过于:它的可扩展性吸引了众多开发者为其开发插件,从而建立起了一个生态系统。
学会使用jQuery并不难,因为它简单易学,我们在使用jQuery时肯定使用或熟悉了不少其插件。如果我们希望提升自己的编程水平,编写一个属于自己的jQuery插件是个不错的选择。
jQuery插件的开发模式
软件开发过程中是需要一定的设计模式来指导开发的,有了模式,我们就能更好地组织我们的代码,并且从这些前人总结出来的模式中学到很多好的实践。
根据《jQuery高级编程》的描述,jQuery插件开发方式主要有三种:
1.通过$.extend()来扩展jQuery
2.通过$.fn 向jQuery添加新的方法
3.通过$.widget()应用jQuery UI的部件工厂方式创建
第三种方式是用来开发更高级jQuery部件的,该模式开发出来的部件带有很多jQuery内建的特性,比如插件的状态信息自动保存,各种关于插件的常用方法等,本篇文章暂不涉及该开发模式。
再来比较第一种和第二种开发模式:
很显然,第一种开发模式是直接在关键字$、jQuery上挂载相应的方法,使jQuery具有静态方法,也可以理解为使用$.extend()在jQuery的命名空间里直接扩展对应的全局方法。这种方法的声明和使用都比较简单,示例代码如下所示:
$.extend({
Alert: function(message) {
alert('app:' + message);
}
});
$.Alert('hello world!');
效果如下:
上述代码标识:声明一个自定义Alert方法,通过$.Alert('hello world!')调用,弹出"app:hello world"。
这种方式一般适用于一些辅助方法,比如上述重写alert方法,或者在console.log()时加入打印的时间等。
但这种方式无法利用jQuery强大的选择器带来的便利,要处理DOM元素以及将插件更好地运用于所选择的元素身上,还是需要使用第二种开发方式。你所见到或使用的插件也大多是通过此种方式开发。
插件开发
基本方法
声明:
$.fn.myPlugin= function() {
//to do list
}
在$.fn上挂载一个方法,方法名就是我们的插件名,然后具体逻辑在这个方法内展开。
调用
$('#app a').myPlugin();
例如:做一个插件,将页面上所有的a标签的字体颜色改为'red',那么代码如下:
//html
<div id="app">
<a href="javascript:void(0);">谷歌</a>
<a>href="javascript:void(0);">百度</a>
<a>href="javascript:void(0);">网易</a>
</div>
//javascript(需要引入jQuery)
$.fn.myPlugin = function() {
//this的解释及注意事项见下文
this.css('color', 'red');
}
$('#app a').myPlugin();
注意:this代表的是当前调用的对象,上述代码调用myPlugin的是$('#app a'),那么this代表的就是$('#app a'),所以方法体内的this.css('color', 'red')相当于$('#app a').css('color', 'red'),同理,如果使用$('#app')那么this代表的就是$('#app')。那么就有同学问:为什么方法体内部不直接使用$('#app a')呢?
答:书写插件的目的就是为了最大限度的复用代码,我们不仅希望可以将id="app"内的a标签字体颜色全部改为'red',还希望可以将id="store"内的a标签字体颜色全部改为'red',如果我们在方法体内将this改为$('#app a'),那么id="stroe"的方法是不是也要改为$('#store a').css('color', 'red')?这不就违背我们开发jQuery插件的初衷了吗?
那么有人还会问,为什么直接使用this而不是使用$(this)?
答:在插件名字定义的这个函数内部,this指代的是我们在调用该插件时,用jQuery选择器选中的元素,一般是一个jQuery类型的集合。比如$('a')返回的是页面上所有a标签的集合,且这个集合已经是jQuery包装类型了,也就是说,在对其进行操作的时候可以直接调用jQuery的其他方法而不需要再用美元符号来包装一下。
效果如下:
支持链式调用
在jQuery官方文档中,有以下类似的例子:
$('#d1').css('color', 'red').addClass('classA');
这段代码表示:将id="d1"的元素的字体改为'red',同时添加一个'classA'的类。这是一个典型的"链式调用"的例子。那么jQuery在开发插件时,我们也可以实现链式调用。具体方法如下:
$.fn.myPlugin = function() {
//this的解释及注意事项见下文
return this.css('color', 'red');
}
$('#app a').myPlugin().addClass('classA');
效果如下:
如果插件内不使用 return this,那么链式调用的时候虽然a标签的字体变成了red,但是在addClass的时候会报错:
接收参数
在使用jQuery系列插件的时候,往往会有一个该插件的官网文档,在这个文档中会介绍相应的参数配置,那么我们接下来学习书写插件时如何传递参数,以使我们插件更加健壮。
现在有一个需求:在调用插件时,可以自定义字体颜色,如果在初始化的时候设定了自定义字体颜色,则设置为该颜色;如果未在初始化时自定义,则默认显示为红色。
通常,我们声明一个函数,在使用函数的参数时是这样的:
function sub(a, b, step) {
return a + b + (typeof step === 'undefined' ? 0 : step);
}
这段代码表示:在调用sub时,如果参数step存在那么返回结果为a+b+step;反之,则返回a+b。
同样,我们在插件内也是可以这样去声明的:
$.fn.myPlugin(color) {
return this.css('color', typeof color === 'undefined' ? 'red' : color);
}
但是这仅仅是一个配置项,如果我们的插件足够大,配置项足够多,那么这种在参数会罗列成color、href、width、 height、 bgImg等等,这样非常不便于维护和升级。
那么为了避免参数越来越长,我们采用一个传递一个对象的方式:
$.fn.myPlugin(opts) {
this.css('color', typeof opts.color === 'undefined' ? 'red' : opts.color);
this.attr('href', typeof opts.href=== 'undefined' ? '#' : opts.href );
return this;
}
这样看起来好多了,看似解决了我们参数过多的问题。
但是,这样写有一个弊端:每逢用到一个参数我们都要通过一个三目运算(当然if else也可以)来判断这个参数是不是在传了进来,这样代码显得臃肿不堪。好在jQuery提供了一个$.extend()的方法(不清楚这个方法是干什么用的的同学请出门左拐,百度一下),将插件内的默认项defaults与自定义参数opts进行合并。
$.fn.myPlugin = function(opts) {
var defaults = {
color: 'red',
href: '#'
};
var settings = $.extend(defaults, opts);
this.css('color', settings.color);
this.attr('href', settings.href);
return this;
}
$('#app a').myPlugin({ color: 'green' }).addClass('classA');
效果如下:
我们发现:字体颜色被换成"green",href属性仍然是使用默认值"#"。
到此,插件可以接收和处理参数后,就可以编写出更健壮而灵活的插件了。若要编写一个复杂的插件,代码量会很大,如何组织代码就成了一个需要面临的问题,没有一个好的方式来组织这些代码,整体感觉会杂乱无章,同时也不好维护,所以将插件的所有方法属性包装到一个对象上,用面向对象的思维来进行开发,无疑会使工作轻松很多。
面向对象的插件开发
为什么要通过面向对象进行jQuery插件开发?核心原因还是为了便于维护。
例如:我们在一个项目中,会开发不止一个jQuery插件,那么如果采用$.fn.XXX的方法,就会出现N多个挂载方法,很容易出现命名冲突的情况。另外一个原因是,如果我们的所有属性、方法都放在$.fn.XXX这个作用域里,那么我们这个插件会随着功能的增加变得越来越臃肿,属性、方法之间的耦合度会越来越大,那么后期的维护工作量之大可想而知;如若把属性、方法放在插件作用域之外,那简直就是一个灾难----如何把控全局变量名和方法名?
面向对象的jQuery插件开发思想简单来讲,就是在函数的作用域里将属性、方法私有化,与外界进行"隔离",使函数内部的属性、方法在"自己的圈子里"专注于"干自己的事情"。
以下代码,我们通过一个具体的业务进行开发navsilde,插件实现目的:
(1)配置导航data,插件自动渲染html结构。
(2)鼠标放在某个导航选项上,下方的activeLine(一条蓝色的线条)会向左或向右地滑动到该选项下,鼠标离开后会回复到active选项上。
(3)点击某个选项,那么会将当前选项设置为active选项。
(4)插件提供一个方法,用来获取配置的data。
下面我们由浅入深的讲解面向对象的开发方法,最终实现这个插件。
首先,我们新建一个函数,命名为Navslide,将Navslide绑定到$.fn上,代码如下:
var Navslide = function(element, options) {
this.default = {
data: [],
active: null
};
this.element = element;
this.settings = $.extend({}, this.default, options);
this.init();
}
//插件入口函数
Navslide.prototype.init = function() {
this.$nav = this.initNav();
}
//根据data渲染html
Navslide.prototype. initNav = function() {
var html = [],
datas = this.settings.data,
len = datas.length;
html.push('< ul class="nav-slide-wrapper">');
for (var i = 0; i < len; i++) {
html.push(
' <li class="nav-slide-item',
datas[i].isActive ? ' active':'',
'">',
datas[i].text, '
'</li>'
);
}
html.push('<i class="active-line"></i>');
html.push('</ul>');
return $(html.join('')).appendTo(this.element);
}
//在插件中调用对象
$.fn.navslide = function(options) {
return new Navslide(this, options );
}
//调用
$('#app'). navslide({
data: [
{'text': '新闻', 'value': 'xinwen', 'isActive': true},
{'text': '社会趣闻', 'value': 'shehuiquwen'},
{'text': '视频', 'value': 'shipin'},
{'text': '新农村', 'value': 'xinongcun'},
]
});
css代码在本文省略,如有需要,请根据文末提供的Github地址下载完整代码。
效果如下:
通过上面这样一改造,我们的代码变得更面向对象了,也更好维护和理解,以后要加新功能新方法,只需向对象添加新变量及方法即可,然后在插件里实例化后即可调用新添加的东西。
命名空间
不仅仅是jQuery插件的开发,我们在写任何JS代码时都应该注意的一点是不要污染全局命名空间。因为随着你代码的增多,如果有意无意在全局范围内定义一些变量的话,最后很难维护,也容易跟别人写的代码有冲突。
比如你在代码中向全局window对象添加了一个变量status用于存放状态,同时页面中引用了另一个别人写的库,也向全局添加了这样一个同名变量,最后的结果肯定不是你想要的。所以不到万不得已,一般我们不会将变量定义成全局的。
一个好的做法是始终用自调用匿名函数包裹你的代码,这样就可以完全放心,安全地将它用于任何地方了,绝对没有冲突。还有一个好处就是,自调用匿名函数里面的代码会在第一时间执行,页面准备好过后,上面的代码就将插件准备好了,以方便在后面的代码中使用插件。
; //这里多个分号?看下边的讲解
(function() {
var Navslide = function() {...}
...
//这里的属性名、方法名不会污染全局
})();
另外需要注意一个问题,在书写javascript时,并不强制需要分号结尾,我们在使用自调用匿名函数时要注意格式问题。所以为了避免其他人的不规范代码,我们在开发插件时,第一行首先使用一个分号强制结束其他语句,这样我们的插件在初始化时就不会报错了!
命名空间传参
细心的同学在阅读一些优秀的jQuery插件时,往往会看到这样的格式:
;
(function($, window, document, undefined) {
//code here
}(jQuery, window, document));
自调用匿名函数传了jQuery、window、documnet三个参数,这是将系统变量传入到函数内部,我们在内部调用的时候可以使用别名。
这里其实是有一个小知识点:很多有同学认为$代表的就是jQuery,其实不然,$只是jQuery的一个简写,我们不仅可以把jQuery.js起一个别名叫$,同样可以将Zepto.js起一个别用叫$。那么问题来了:当我们不把jQuery传入到自定义匿名函数中,而且我们项目组同时用到了jQuery.js和Zepto.js时,函数内部的$代表的是jQuery还是Zepto呢?当然了,如果不怕麻烦,我们不需要传入jQuery,将函数内部使用到$的地方全部改为jQuery(selector)也是可以的,但是不提倡这样做。
而至于这个undefined,比较有意思,为了得到没有被修改的undefined,我们并没有传递这个参数,但却在接收时接收了它,因为实际并没有传,所以"undefined"那个位置接收到的就是真实的"undefined"了。有一点hack的感觉,也是从其他大牛总结到的经验。
最终模板
;
(function($, window, document, undefined) {
var pluginName = 'navslide';
var Navslide = function(element, option) {
//初始化参数
...
//调用入口函数
this.init();
}
//入口方法
Navslide.prototype.init = function() {...}
//业务方法
Navslide.prototype.XXX = function() {...}
//绑定函数
$.fn[pluginName] = function(options) {
return new Navslide(this, options);
}
}(jQuery, window, document));
至此,我们搭建了一个通用的jQuery插件模板。由于篇幅有限,这个模板只能算是比较基础的,在我们的实际业务中,还要进行插件自定义方法、多语言、多主题、插件发布等设置,我会在之后的文章继续推出。
滑动导航条完整示例代码:https://github.com/zhiyuanMain/jQuery-plugins/tree/master/how-to-init