jQuery插件开发方式主要有三种:

  1. 通过$.extend()来扩展jQuery
  2. 通过$.fn 向jQuery添加新的方法
  3. 通过$.widget()应用jquery UI的部件工厂方式创建

第一种方式很简单,仅仅是在jQuery命名空间上添加一个静态方法而已,所以我们调用通过$.extend()添加的函数是直接通过$符号调用($.myfunction())而不需要选中DOM元素($(‘#example’).myfunction()),看下面例子:

  1. $.extend({
  2. sayHello: function(name){
  3. console.log('Hello, '+(name ? name: 'default name')+' !');
  4. }
  5. })
  6. $.sayHello(); // print "Hello, default name !"
  7. $.sayHello('eric'); // print "Hello, eric !"

上面代码通过$.extend()向jQuery添加了一个sayHello的函数,然后通过$直接调用。
但是这种方式用来定义一些辅助方法还是比较方便的。比如一个自定义的console,输出特定格式的信息,定义一次后可以通过jQuery在程序中任何需要的地方调用它:

$.extend({
            log: function(message){
                var now = new Date(),
                    y = now.getFullYear(),
                    m = now.getMonth() + 1,
                    d = now.getDate(),
                    h = now.getHours(),
                    min = now.getMinutes(),
                    s = now.getSeconds(),
                    time = y + '/'+ m + '/' + d + ' '+h+':'+min+':'+s;
                console.log(time+'My App: '+message);
            }
        })

        $.log('initializing...');

结果: jQuery插件开发 - 图1但是这种方式无法利用jQuery强大的选择器带来的便利,要处理DOM元素以及将插件更好的运用在所选择的元素身上,还是需要使用第二种开发方式。

$.fn 插件开发

基本格式:

$.fn.pluginName = function(){
    // your code goes here
}

比如我们将页面上所有链接颜色转成红色,则可以这样写这个插件:

$.fn.myPlugin = function(){
//this 表示jQuery选中的元素
    this.css('color','red');
}
//调用插件方式:
$('a').myPlugin();

在插件名字定义的函数内部,this指代的是我们在调用该插件时,用jQuery选择器选中的元素,一般是一个jQuery类型的集合。比如$('a')返回的是页面上所有的a标签集合,且这个集合已经是jQuery包装类型了。也就是在对其进行操作的时候可以直接调用jQuery的其他方法而不需要再用$来包装一下。
所以上面插件代码中,我们在this上调用jQuery的css()方法, 也就相当于在调用$(‘a’).css();
理解this在这个地方的含义很重要,这样你才知道为什么直接调用jQuery方法同时在其他地方this指代不同时我们又需要用jQuery重新包装才能调用。

让插件支持链式调用:

我们都知道jQuery有一个优雅的特性是支持链式调用,选择好DOM元素后就可以不断调用其他方法。
要让插件不打破这种链式调用,只需要return一下即可。

$.fn.myPlugin = function(){
    this.css('color','red');
    return this.each(function(){
        //对每一个元素进行不同的操作
        $(this).append(' ' +$(this).attr('href'));
})
}

让插件接收参数:

一个强劲的插件时可以让使用者随意定制的,这要求我们提供在编写插件时就要考虑得全面些,尽量提供合适的参数。
比如现在我们不想让链接只变成红色,我们让插件的使用者自己定义显示什么颜色,要做到这一点很方便,只需要使用者在调用的时候传入一个参数即可,同时我们在插件的代码里接收这个参数。另一方面,为了灵活,使用者可以不传递参数,插件提供参数的默认值。
在插件处理参数的接收上,通常使用jQuery的extend()方法,extend()方法有下面两种用法:

  1. 如果给extend传递单个对象的情况下,这个对象会合并到jQuery上

  2. 如果给extend传递一个以上的参数时,它会将所有参数对象合并到第一个参数里面,同时如果对象中有同名属性时,后面的会覆盖前面的。例如:$.extend(defaults,options)- 这个会把两个参数合并到defaults里面。$.extend({},defaults,options) - 这个会把两个参数合并到一个新的空对象{}里面,这种用法可以用来保护defaults的值不被修改。

利用这一点,我们可以在插件里定义一个保存插件参数默认值的对象,同时将接收来的参数对象合并到默认对象上,最后就实现了用户指定了值得参数使用指定的值,未指定的参数使用插件默认值。
在下面的例子中,我们再指定一个参数fontsize,允许调用插件的时候设置字体大小:

$.fn.myPlugin = function(options){
            var defaults = {
                'color':'red',
                'fontSize':'12px'
            };
            var settings = $.extend(defaults,options);
            return this.css({
                'color': settings.color,
                'fontSize': settings.fontSize
            });
        }

现在调用插件, 在用户不指定fontSize时,fontSize使用默认的12px:

$('a').myPlugin({
    'color': 'green'
    }
);

同时指定颜色和字体大小:

$('a').myPlugin({
    'color': 'green',
    'fontSize': '14px'
    }
);

保护好默认参数值:

上面的代码调用extend时会将defaults的值改变,这样不好,因为它作为插件自有的东西应该维持原样,另外如果你在后续代码中还要使用这些默认值得话,当你再次访问它时就已经被用户传进来的参数更改了。

jQuery插件开发 - 图2一个好的做法是将一个新的空对象作为$.extend的第一个参数,defaults和用户传递的参数对象放在后面,这样做的好处是所有值被合并到空对象上,保护了插件的默认值。

$.fn.myPlugin = function(options){
            var defaults = {
                'color':'red',
                'fontSize':'12px'
            };
            var settings = $.extend({},defaults,options);
            return this.css({
                'color': settings.color,
                'fontSize': settings.fontSize
            });
        }

到此,插件可以接受和处理参数后,就可以编写出更健壮灵活的插件了。若要编写一个复杂的插件,代码了会很大,如何组织就成了一个需要面临的问题,没有一个好的方式来组织这些代码,整体感觉会杂乱无章,同时也不好维护,所以将插件的所有方法属性包装到一个对象上,用面向对象的思维来进行开发。

面向对象的插件开发

为什么要有面向对象的思维,如果不这样,你可能需要一个方法的时候就去定义一个function,当需要另外一个方法的时候,再去随便定义一个function,同样,需要一个变量的时候,毫无规则地定义一些散落在代码各处的变量。不方便维护,也不够清晰。
如果将需要的重要变量定义到对象的属性上,函数变成对象的方法,当我们需要的时候通过对象来获取,一来方便管理,二来不会影响外部命名空间,因为所有这些变量名还有方法名都是在对象内部。
下面我们可以开发一个日历本插件:
效果图:

jQuery插件开发 - 图3首先我们要定义一个Calendar对象来封装这个日历相关的信息:

var MyCalendar = function(ele,opt){
        this.$element = ele,
            this.defaults = {
                'color':'#6ac13c',
                'async':false,
                'url':'',
                'rootpath':''
            },
            this.options = $.extend({},this.defaults,opt)
    }

接下来要初始化数据

var my_date = new Date();

MyCalendar.prototype={
    my_date : my_date,
    my_year : my_date.getFullYear(),
    my_month : my_date.getMonth(),
    my_day : my_date.getDate(),
    month_olympic : [31,29,31,30,31,30,31,31,30,31,30,31],
    month_normal : [31,28,31,30,31,30,31,31,30,31,30,31],
    month_name : ["January","Febrary","March","April","May","June","July","Auguest","September","October","November","December"],
    init: function(){
        $(this.$element).addClass('mycalendar');
        this.$element.append(this.buildCalHeader());
        this.$element.append(this.buildCalBody());
        this.loadingImg = document.createElement('img');
        $(this.loadingImg).addClass('loading').attr('src','images/ajax-loader-1.gif').css('display','none');
        this.$element.append(this.loadingImg);
        $(document.body).append(this.buildModal());
        var _self = this; // 保存calendar对象
        $("#prev").click(function(evt){
            evt.preventDefault();
            _self.prev(); //这里this对象指向的是$("#prev"), 但是prev对象是定义在calendar对象上的
        });
        $("#next").click(function(evt){
            evt.preventDefault();
            _self.next();
        });
    },
    prev: function(){ //上一个月按钮触发
        this.my_month--;
        if(this.my_month<0){
            this.my_year--;
            this.my_month=11;
        }
        this.showCalendar();
    },
    next: function(){ //下一个月按钮触发
        this.my_month++;
        if(this.my_month>11){
            this.my_year++;
            this.my_month = 0;
        }
        this.showCalendar();
    },
    showCalendar: function(){ //调用入口,显示calendar
        this.refreshDate();
    },
    refreshDate: function(){ //主要逻辑
        var _self = this;
        var str = "";
        var totalDay = this.daysMonth(this.my_month,this.my_year);
        var firstDay = this.dayStart(this.my_month, this.my_year); //获取该月第一天是星期几
        var myClass;
        for(var i=1;i<firstDay;i++){
            str += "<li></li>"; //为起始日之前的日期创建空白节点
        }
        for(var i=1;i<=totalDay;i++){
            if((i<this.my_day && this.my_year==this.my_date.getFullYear() && this.my_month==this.my_date.getMonth()) ||
                this.my_year<this.my_date.getFullYear() || 
                (this.my_year==this.my_date.getFullYear() && this.my_month<this.my_date.getMonth())){
                 //当该日期在今天之前时,以浅灰色字体显示
                myClass = " class='lightgrey'";
            }else if(i==my_day && my_year==my_date.getFullYear() && my_month==my_date.getMonth()){
                myClass = " class='green greenbox'";//当天日期以绿色背景突出显示
            }else{
                myClass = " class='darkgrey'";//当该日期在今天之后时,以深灰字体显示
            }
            str += "<li"+myClass+">"+i+"</li>"; // 创建日期节点
        }
        $("#days").html(str);
        $("#calendar-title").html(this.month_name[this.my_month]);
        $("#calendar-year").html(this.my_year);

    },
    $.fn.mycalendar=function(options){
            var calendar = new MyCalendar(this,options);
            calendar.init();
            calendar.showCalendar();
        }
}

调用插件:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Calendar Demo</title>
    <link rel="stylesheet" href="css/calendar-plugin.css">
    <link rel="stylesheet" href="bootstrap/3.3.7/css/bootstrap.css">
    <script type="text/javascript" src="jquery/3.3.1/jquery.js"></script>
    <script type="text/javascript" src="bootstrap/3.3.7/js/bootstrap.js"></script>
    <script type="text/javascript" src="js/calendar-plugin.js"></script>
</head>
<body>
        <div id="mycalendar">
        </div>
        <script type="text/javascript">
            $("#mycalendar").mycalendar();
        </script>
</body>
</html>

关于命名空间:

不仅仅是jQuery插件的开发,我们在写任何JS代码时都应该注意不要污染全局命名空间。因为随着你的代码的增多,如果有意无意的全局范围内定义一些变量的话,最后很难维护,也容易跟别人写的代码有冲突。
比如你在代码中向全局window对象添加了一个变量status用于存放状态,同时页面中引用了另一个别人写的库,也向全局添加了这样一个同名变量,最后的结果肯定不是你想要。所以不到万不得已,一般我们不会讲变量定义成全局的。
一个号的做法是始终用 自调用匿名函数 包裹你的代码,这样就可以完全放心,安全的将它用于任何地方了。

用自调用匿名函数包裹你的代码

我们知道JavaScript中无法用花括号方便的创建作用域,但函数却可以形成一个作用域,域内的代码无法被外界访问。如果我们将自己的代码放入一个函数中,那么就不会污染全局命名空间,同时不会和别的代码冲突。
如我们上面定义了一个MyCalendar对象,它会被附到window对象上,为了防止这种事情发生,你可以把所有代码放到jQuery插件定义代码里去,也就是放到$.fn.mycalendar里面,但是这些对象跟插件没什么关系,这样反而让插件代码显得臃肿。$.fn.myCalendar里面我们应该更专注插件的调用以及如何与jQuery互动。
所以保持原来代码不变,我们将所有代码用自调用匿名函数包裹:

(function(){
    var MyCalendar = function(ele,opt){
        this.$element = ele,
            this.defaults = {
                'color':'#6ac13c',
                'async':false,
                'url':'',
                'rootpath':''
            },
            this.options = $.extend({},this.defaults,opt)
    }

    $.fn.mycalendar=function(options){
            var calendar = new MyCalendar(this,options);
            calendar.init();
            calendar.showCalendar();
        }
})(jQuery);