我们在做web的时候都会用到很多jQuery插件,这些插件可以很方便的使用。但对于初学者来说想要修改插件中的一些功能,或者想要自定义插件却不是容易的事情。自己也刚好在学习这部分的知识,这里用一个案例来介绍jQuery插件的使用和写法。
就拿单页导航这个插件来举例吧,one-page-nav写的比较规范代码也比较短。其他jQuery插件大同小异,学会结构和规范之后自己也可以自定义插件,后面也会讲解对该插件功能的改写,从 会使用-》看懂-》改写-》自定义插件有一个循序渐进的过程。
地址:github:https://github.com/davist11/jQuery-One-Page-Nav
原插件内容:
;(function($, window, document, undefined){
// our plugin constructor
var OnePageNav = function(elem, options){
this.elem = elem;
this.$elem = $(elem);
this.options = options;
this.metadata = this.$elem.data('plugin-options');
this.$win = $(window);
this.sections = {};
this.didScroll = false;
this.$doc = $(document);
this.docHeight = this.$doc.height();
};
// the plugin prototype
OnePageNav.prototype = {
defaults: {
navItems: 'a',
currentClass: 'current',
changeHash: false,
easing: 'swing',
filter: '',
scrollSpeed: 750,
scrollThreshold: 0.5,
begin: false,
end: false,
scrollChange: false
},
init: function() {
// Introduce defaults that can be extended either
// globally or using an object literal.
this.config = $.extend({}, this.defaults, this.options, this.metadata);
this.$nav = this.$elem.find(this.config.navItems);
//Filter any links out of the nav
if(this.config.filter !== '') {
this.$nav = this.$nav.filter(this.config.filter);
}
//Handle clicks on the nav
this.$nav.on('click.onePageNav', $.proxy(this.handleClick, this));
//Get the section positions
this.getPositions();
//Handle scroll changes
this.bindInterval();
//Update the positions on resize too
this.$win.on('resize.onePageNav', $.proxy(this.getPositions, this));
return this;
},
adjustNav: function(self, $parent) {
self.$elem.find('.' + self.config.currentClass).removeClass(self.config.currentClass);
$parent.addClass(self.config.currentClass);
},
bindInterval: function() {
var self = this;
var docHeight;
self.$win.on('scroll.onePageNav', function() {
self.didScroll = true;
});
self.t = setInterval(function() {
docHeight = self.$doc.height();
//If it was scrolled
if(self.didScroll) {
self.didScroll = false;
self.scrollChange();
}
//If the document height changes
if(docHeight !== self.docHeight) {
self.docHeight = docHeight;
self.getPositions();
}
}, 250);
},
getHash: function($link) {
return $link.attr('href').split('#')[1];
},
getPositions: function() {
var self = this;
var linkHref;
var topPos;
var $target;
self.$nav.each(function() {
linkHref = self.getHash($(this));
$target = $('#' + linkHref);
if($target.length) {
topPos = $target.offset().top;
self.sections[linkHref] = Math.round(topPos);
}
});
},
getSection: function(windowPos) {
var returnValue = null;
var windowHeight = Math.round(this.$win.height() * this.config.scrollThreshold);
for(var section in this.sections) {
if((this.sections[section] - windowHeight) < windowPos) {
returnValue = section;
}
}
return returnValue;
},
handleClick: function(e) {
var self = this;
var $link = $(e.currentTarget);
var $parent = $link.parent();
var newLoc = '#' + self.getHash($link);
if(!$parent.hasClass(self.config.currentClass)) {
//Start callback
if(self.config.begin) {
self.config.begin();
}
//Change the highlighted nav item
self.adjustNav(self, $parent);
//Removing the auto-adjust on scroll
self.unbindInterval();
//Scroll to the correct position
self.scrollTo(newLoc, function() {
//Do we need to change the hash?
if(self.config.changeHash) {
window.location.hash = newLoc;
}
//Add the auto-adjust on scroll back in
self.bindInterval();
//End callback
if(self.config.end) {
self.config.end();
}
});
}
e.preventDefault();
},
scrollChange: function() {
var windowTop = this.$win.scrollTop();
var position = this.getSection(windowTop);
var $parent;
//If the position is set
if(position !== null) {
$parent = this.$elem.find('a[href$="#' + position + '"]').parent();
//If it's not already the current section
if(!$parent.hasClass(this.config.currentClass)) {
//Change the highlighted nav item
this.adjustNav(this, $parent);
//If there is a scrollChange callback
if(this.config.scrollChange) {
this.config.scrollChange($parent);
}
}
}
},
scrollTo: function(target, callback) {
var offset = $(target).offset().top;
$('html, body').animate({
scrollTop: offset
}, this.config.scrollSpeed, this.config.easing, callback);
},
unbindInterval: function() {
clearInterval(this.t);
this.$win.unbind('scroll.onePageNav');
}
};
OnePageNav.defaults = OnePageNav.prototype.defaults;
$.fn.onePageNav = function(options) {
return this.each(function() {
new OnePageNav(this, options).init();
});
};
})( jQuery, window , document );
先来看该插件实现的功能有,接下来再讲它是如何实现的。
1:点击导航栏的某一项,内容导航到相应项。当前项的<li>中增加class="current"。
2:鼠标滚动到相应内容时,导航栏中class="current"会切换。
3:其他功能比如说滚动速度和回调函数等。
插件的封装
我们去掉内容,只看这部分。有很多的括号和参数,最开始看的时候内心是拒绝的,完全不知道这么多括号是干什么的,慢慢学下去会发现还是很有意思的。这里是利用了闭包的特性,既可以避免内部临时变量影响全局空间,又可以在插件内部继续使用$作为jQuery的别名。
为了更好的兼容性,开始有一个分号,否则压缩的时候可能出现问题。首先定义一个匿名函数function(){},然后用括号括起来,最后通过()这个运算符来执行。js是以function为作用域,这样定义了一个自调用匿名函数,全局空间就不能访问其中定义的局部变量了。其中
jQuery,window,document
作为实参传递给匿名函数,插件内部就可以使用$作为jQuery的别名了。
;(function($, window, document, undefined){ //这里放置代码 })( jQuery, window , document );
向jQuery的命名空间添加新的方法
jQuery.fn即$.fn是指jQuery的命名空间,所有的对象方法应当附加到就jQuery.fn对象上。这样写之后外部就可以通过这种方式调用它$('#nav').onePageNav(option)我们遵循jQuery的规范,插件应该返回一个jQuery对象,以保证插件的可链式操作。假设$('.nav')可能是一个数组,可以通过this.each来遍历所有的元素。这种情况可以用于一个页面有多个导航的时候。$.fn.onePageNav = function(options) { return this.each(function() { new OnePageNav(this, options).init(); }); };
定义OnePageNav对象
这里定义一个对象OnePageNav,在调用插件的时候用new可以创建一个原对象的实例对象。我们注意到参数加了$符号的表示的是jQuery对象,没有加$符号表示的是DOM对象,这是一个 很好的习惯,在使用它们的时候就不需要做DOM对象和jQuery对象之间的转换。
var OnePageNav = function(elem, options){ this.elem = elem; this.$elem = $(elem); this.options = options; this.metadata = this.$elem.data('plugin-options'); this.$win = $(window); this.sections = {}; this.didScroll = false; this.$doc = $(document); this.docHeight = this.$doc.height(); };
设置默认参数
所有的插件几乎都会有自己的默认参数,所以在调用的时候即使不传递option也可以,在OnePageNav 的原型中的默认参数如下。
defaults: { navItems: 'a', //默认<a>标签 currentClass: 'current', //默认当前标签的样式名 changeHash: false, easing: 'swing', filter: '', //标签过滤 scrollSpeed: 750, //速度 scrollThreshold: 0.5, //占面积比 begin: false, //开始回调函数 end: false, //结束回调函数 scrollChange: false //市场改变的回调函数 },
初始化
注意到 $.fn.onePageNav方法中的这句话new OnePageNav(this, options).init(); 调用了OnePageNav的init方法进行初始化,现在来看看初始化都做了什么工作。可以概括为这几部分1、extend 函数用于将一个或多个对象的内容合并到目标对象,把默认设置和自定义设置合并起来。2、将标签进行 过滤 ,比如像下面这样我们要过滤掉最后一个标签,可以在option中加入 filter: ':not(.exception)', 3、给过滤后的标签绑定click方法,点击click可以scroll到对应的内容 。4、获取标签对应内容的位置,并都放在this.sections中 。5、添加一个间隔性触发定时器,目的是为了在滑动鼠标滚轮的时候根据当前显示内容,更改导航标签的class=current。6、每次窗体大小改变时重新获取标签对应内容的位置。<ul id="nav"> <li><a href="#nr">内容一</a></li> <li><a href="#nt">内容二</a></li> <li><a href="#ny">内容三</a></li> <li><a href="#nu">内容四</a></li> <li><a class="exception" href="#top">返回顶部</a></li> </ul>init: function() { this.config = $.extend({}, this.defaults, this.options, this.metadata); this.$nav = this.$elem.find(this.config.navItems); if(this.config.filter !== '') { this.$nav = this.$nav.filter(this.config.filter); } this.$nav.on('click.onePageNav', $.proxy(this.handleClick, this)); this.getPositions(); this.bindInterval(); this.$win.on('resize.onePageNav', $.proxy(this.getPositions, this)); return this; },
功能的实现
其他的方法都是添加到OnePageNav.prototype上的,比较好理解,可以自己看代码。这里特别说一下回调函数。比如说下面这句话,默认值是 begin: false,
如果调用的时候option设置了
begin: function() {
//I get fired when the animation is starting
},
//I get fired when the animation is starting
},
每次单击导航标签的时候就会调用这个方法。如果没有设置begin,自然也不会出问题。
if(self.config.begin) {
self.config.begin();
}