jQuery弹簧插件编写基础之“又见弹窗”

本文将通过一个实例来引出jQuery插件开发中的一些细节,首先介绍下jQuery插件开发的一些基础知识。

jQuery的插件开发主要分为两类:

1. 类级别,即在jQuery类本身上扩展方法,类似与 $.ajax,$.get 等。

2. 对象级别,这里所谓的对象是指通过jQuery选择器选中的jQuery对象,在该对象上添加方法。例如:$('div').css(), $('div').show() 等。

在实际开发中,我们通常选用对象级别的方法来开发插件,jQuery强大的选择器及对象操作是我们选择这种方式很大的一个原因。

接下来我们看看两种方式的具体写法是什么:

类级别的插件开发

$.extend({
  foo: function() {
    //...
  },
  bar: function() {
    //...
  }
})
//调用
$.foo();

在这里,对扩展方法的命名需要考究一些,以免与jQuery类中的原有方法重名。即便如此,当我们需要在类上扩展多个方法时仍有可能会出现命名冲突的情况,为此我们可以创建自定义的命名空间:

$.myPlugin = {
  foo: function() {
    //...
  },
  bar: function() {
    //...
  }
}
 //调用
$.myPulgin.foo();

对象级别的插件开发

$.fn.foo = function() {
  //doSomething...
}
//调用(假设拥有一个id为obj的元素)
$('#obj').foo();
有个会问 fn 是什么东东?粘一段别人截取的jQuery源码就明白了:
jQuery.fn = jQuery.prototype = {
  init: function(selector, context) {
    //....
  }
}

原来是原型链啊。。。

接收配置参数

在编写一个插件时,我们可以让使用插件的人能按自己的意愿设置插件的一些属性,这就需要插件有接收参数的功能,同时当使用插件的人不传入参数时,插件内部也有一套自己默认的配置参数。

$.fn.foo = function(options) {
  var defaults = {
    color: '#000',
    backgroundColor: 'red'
  };
  var opts = $.extend({}, defaults, options); 
  alert(opts.backgroundColor); //yellow
}
$('#obj').foo({
  backgroundColor: 'yellow'  
})

这里的关键就是 $.extend 方法,它能够将对象进行合并。对于相同的属性,后面的对象会覆盖前面的对象。为什么extend方法第一个参数是一个空对象呢?因为该方法会将后者合并到前者上,为了不让 defaults 被改变所以第一个参数设为空对象。

如果我们允许使用插件的人能够设置默认参数,就需要将其暴露出来:

$.fn.foo = function(options) {
  var opts = $.extend({}, $.fn.foo.defaults, options); 
  alert(opts.backgroundColor);
}
$.fn.foo.defaults = {
  color: '#000',
  backgroundColor: 'red'
}

这样就可以在外部对插件的默认参数进行修改了。

适当的暴露一些方法

$.fn.foo = function(options) {
  var opts = $.extend({}, $.fn.foo.defaults, options); 
  $.fn.foo.sayColor(opts.backgroundColor);
}
$.fn.foo.sayColor = function(bgColor) {
  alert(bgColor);
}
$.fn.foo.defaults = {
  color: '#000',
  backgroundColor: 'red'
}

改写:

$.fn.foo.sayColor = function(bgColor) {
  alert('background color is ' + bgColor);
}

暴露插件中的一部分方法是很牛逼的,它使得别人可以对你的方法进行扩展、覆盖。但是当别人对你的参数或方法进行修改时,很可能会影响其他很多东西。所以在考虑要不要暴露方法时候要头脑清楚,不确定的就不要暴露了。

保持函数的私有性

说到保持私有性,首先想到什么?没错,就是闭包:

;(function($) {
  $.fn.foo = function(options) {
    var opts = $.extend({}, $.fn.foo.defaults, options); 
    debug(opts.backgroundColor);
  }
  function debug(bgColors) {
    console.log(bgColors);
  }
  $.fn.foo.defaults = {
    color: '#000',
    backgroundColor: 'red'
  }
})(jQuery)

这是jQuery官方给出的插件开发方式,好处包括:1.没有全局依赖 2.避免其他人破坏 3.兼容 '$' 与 'jQuery' 操作符。

如上,debug 方法就成了插件内部的私有方法,外部无法对其进行修改。在闭包前面加 ; 是防止进行代码合并时,如果闭包前的代码缺少分号从而导致后面报错的情况。

合并

;(function($) {
  //定义插件
  $.fn.foo = function(options) {
    //doSomething...
  }
  //私有函数
  function debug() {
    //doSomething...
  }
  //定义暴露函数
  $.fn.foo.sayColor = function() {
    //doSomething...
  }
  //插件默认参数
  $.fn.foo.default = {
    color: '#000',
    backgroundColor: 'red'
  }
})(jQuery);

以上的代码就创建了一个完整且规范的插件骨架,看起来虽然很简单但在实际开发中还是有很多技巧与注意事项,接下来我们通过一个实例来看看。

想了半天,觉得将弹窗做成插件当作示例是比较合适的。在开发之前我们先构想一下这个弹窗插件的结构与功能等:

从上图我们看出包括三个部分,标题、内容、以及按钮组。这里需要申明一点,我们不想只做成浏览器里默认的只包含一个按钮的alert框,而是使用者可以自定义按钮数量,这样该弹出框也能完成类似confirm框的功能。

搭建插件骨架

function SubType($ele, options) {
  this.$ele = $ele;
  this.opts = $.extend({}, $.fn.popWin.defaults, options);
}
SubType.prototype = {
  createPopWin: function() {
  }
};
$.fn.popWin = function(options) {
  //this指向被jQuery选择器选中的对象
  var superType = new SubType(this, options);
  superType.createPopWin();
};
$.fn.popWin.defaults = {};

1. 我们创建了基于对象且名为 popWin 方法,并将 defaults 默认配置参数暴露出去以便使用的人进行修改;

2. 这里使用面向对象的方法来管理我们的私有函数,createPopWin 方法就是我们私有的用来创建弹窗的函数。

3. 在插件被调用时将jq对象与自定义的参数传入构造函数中并实例化。

调用

设想一下我们该怎么调用这个插件呢?我们可以在自己的文档树中合适的位置插入一个 div 元素,选中该 div 并调用我们定义在jQuery对象上的 popWin 方法。

$('#content').popWin({
  a: 1,
  b: 2,
  callback: function() {}
});

调用 popWin 的同时传入自定义的配置参数,之后被选中的 div 元素就被神奇的转化成一个弹窗了!当然,这只是我们的设想,下面开始码代码。

确定默认配置

$.fn.popWin.defaults = {
  width: '600', //弹窗宽
  height: '250', //弹窗高
  title: '标题', //标题
  desc: '描述', //描述
  winCssName: 'pop-win', //弹窗的CSS类名
  titleCssName: 'pop-title', //标题区域的CSS类名
  descCssName: 'pop-desc', //描述区域的CSS类名
  btnAreaCssName: 'pop-btn-box', //按钮区域的CSS类名
  btnCssName: 'pop-btn', //单个按钮的CSS类名
  btnArr: ['确定'], //按钮组
  callback: function(){} //点击按钮之后的回调函数
}

我们定义了如上的参数,为什么有要传入这么多的CSS类名呢?1. 为了保证JS与CSS尽可能的解耦。 2. 你的样式有很大可能别人并不适用。所以你需要配置一份样式表文件来对应你的默认类名,当别人需要更改样式时可以传入自己编写的样式。

按钮组为一个数组,我们的弹窗需要根据其传入的数组长度来动态的生成若干个按钮。回调函数的作用是在用户点击了某个按钮时返回他所点击按钮的索引值,方便他进行后续的操作。

弹窗DOM创建

var popWinDom,titleAreaDom,descAreaDom,btnAreaDom;
SubType.prototype = {
  createPopWin: function() {
    var _this = this;
    //首次创建弹窗
    //背景填充整个窗口
    this.$ele.css({
      position: 'fixed',
      top: 0,
      left: 0,
      right: 0,
      bottom: 0,
      backgroundColor: 'rgba(0,0,0,0.4)',
      overflow: 'hidden'
    });
    
    //窗口区域
    popWinDom = $('<div><div></div><div></div><div></div></div>').css({
      width: this.opts.width,
      height: this.opts.height,
      position: 'absolute',
      top: '30%',
      left: '50%',
      marginLeft: '-' + (this.opts.width.split('px')[0] / 2) + 'px'
    }).attr('class',this.opts.winCssName);
    
    //标题区域
    titleAreaDom = popWinDom.find('div:eq(0)')
              .text(this.opts.title)
              .attr('class',this.opts.titleCssName);
    
    //描述区域
    descAreaDom = popWinDom.find('div:eq(1)')
            .text(this.opts.desc)
            .attr('class',this.opts.descCssName);
    //按钮区域   
    btnAreaDom = popWinDom.find('div:eq(2)')
            .attr('class',this.opts.btnAreaCssName);
    
    //插入按钮
    this.opts.btnArr.map(function(item, index) {
      btnAreaDom.append($('<button></button>')
        .text(item)
        .attr({'data-index':index, 'class':_this.opts.btnCssName})
        .on('click', function() {
          _this.opts.callback($(this).attr('data-index'));
        }));
      });
    
    this.$ele.append(popWinDom);
  }
}

1. 首先命名了四个变量用来缓存我们将要创建的四个DOM,将传入的jQuery对象变形成覆盖整个窗口半透明元素;

2. 创建窗口DOM,根据传入的高、宽来设置尺寸并居中,之后另上传入的窗口CSS类名;

3. 创建标题、描述、按钮组区域,并将传入的标题、描述内容配置上去;

4. 动态加入按钮,并为按钮加上data-index的索引值。注册点击事件,点击后调用传入的回调函数,将索引值传回。

好了,我们先看下效果。调用如下:

$('#content').popWin({
  width: '500',
  height: '200',
  title: '系统提示',
  desc: '注册成功',
  btnArr: ['关闭'],
  callback: function(clickIndex) {
    console.log(clickIndex);
  }
});

可以看到一个弹窗的DOM已被渲染到页面中了,当点击关闭按钮时控制台会打印出 "0",因为按钮组只有一个值嘛,当然是第0个了。

如果我们需要多次调用这个弹窗,每次都要传入高、宽我会觉得很麻烦。这时我们可以直接在一开始修改插件内部的默认配置,这也是我们将默认配置暴露的好处:

$.fn.popWin.defaults.width = '500';
$.fn.popWin.defaults.height = '200';

要注意的当然是不能直接改变defaults的引用,以免露掉必须的参数。 这样以后的调用都无需传入尺寸了。

我们加一个按钮并且传入一个自定义的样式看看好使不呢?

$('#content').popWin({
  title: '系统提示',
  desc: '是否删除当前内容',
  btnArr: ['确定','取消'],
  winCssName: 'pop-win-red',
  callback: function(clickIndex) {
    console.log(clickIndex);
  }
});

可以看到都是生效了的,当点击“确定”按钮时回调函数返回 0,点击“取消”按钮时回调函数返回 1。这样使用插件的人就知道自己点击的是哪一个按钮,以完成接下来的操作。

显示&隐藏

接下来要进行打开、关闭弹窗功能的开发。回想上面介绍的概念,我们想让使用该插件的人能够对这两个方法进行扩展或者重写,所以将这两个方法暴露出去:

$.fn.popWin.show = function($ele) {
  $ele.show();
}
$.fn.popWin.hide = function($ele) {
  $ele.hide();
}

之后在createPopWin方法中需要的地方调用这两个方法。

这里多强调一点,也是做弹窗控件不可避免的一点:只有当我们点击按钮以及灰色背景区域时允许弹窗关闭,点击弹窗其他地方不允许关闭。由于弹窗属于整个灰色区域的子节点,必然牵扯到的就是事件冒泡的问题。

所以在给最外层加上点击关闭的事件时,要在弹窗区域阻止事件冒泡。

popWinDom = $('<div><div></div><div></div><div></div></div>').css({
  width: this.opts.width,
  height: this.opts.height,
  position: 'absolute',
  top: '30%',
  left: '50%',
  marginLeft: '-' + (this.opts.width.split('px')[0] / 2) + 'px'
}).attr('class',this.opts.winCssName).on('click', function(event) {
  event.stopPropagation();
});

二次打开

我们只需要在第一次调用插件时创建所有创建DOM,第二次调用时只更改其参数即可,所以在createPopWin方法最前面加入如下方法:

if (popWinDom) { //弹窗已创建
  popWinDom.css({
    width: this.opts.width,
    height: this.opts.height
  }).attr('class',this.opts.winCssName);
  titleAreaDom.text(this.opts.title).attr('class',this.opts.titleCssName);
  descAreaDom.text(this.opts.desc).attr('class',this.opts.descCssName);
  btnAreaDom.html('').attr('class',this.opts.btnAreaCssName);
  this.opts.btnArr.map(function(item, index) {
    btnAreaDom.append($('<button></button>')
      .text(item)
      .attr('data-index',index)
      .attr('class',_this.opts.btnCssName)
      .on('click', function() {
        _this.opts.callback($(this).attr('data-index'));
        $.fn.popWin.hide(_this.$ele);
      }));
    });
  $.fn.popWin.show(this.$ele);
  return;
}

合并整个插件代码

;(function($) {
  function SubType(ele, options) {
    this.$ele = ele;
    this.opts = $.extend({}, $.fn.popWin.defaults, options);
  }
  var popWinDom,titleAreaDom,descAreaDom,btnAreaDom;
  SubType.prototype = {
    createPopWin: function() {
      var _this = this;
      if (popWinDom) { //弹窗已创建
        popWinDom.css({
          width: this.opts.width,
          height: this.opts.height
        }).attr('class',this.opts.winCssName);
        titleAreaDom.text(this.opts.title).attr('class',this.opts.titleCssName);
        descAreaDom.text(this.opts.desc).attr('class',this.opts.descCssName);
        btnAreaDom.html('').attr('class',this.opts.btnAreaCssName);
        this.opts.btnArr.map(function(item, index) {
          btnAreaDom.append($('<button></button>')
            .text(item)
            .attr('data-index',index)
            .attr('class',_this.opts.btnCssName)
            .on('click', function() {
              _this.opts.callback($(this).attr('data-index'));
              $.fn.popWin.hide(_this.$ele);
            }));
        });
        $.fn.popWin.show(this.$ele);
        return;
      }
      //首次创建弹窗
      this.$ele.css({
        position: 'fixed',
        top: 0,
        left: 0,
        right: 0,
        bottom: 0,
        backgroundColor: 'rgba(0,0,0,0.4)',
        overflow: 'hidden',
        display: 'none'
      }).on('click', function() {
        $.fn.popWin.hide(_this.$ele);
      });
      popWinDom = $('<div><div></div><div></div><div></div></div>').css({
        width: this.opts.width,
        height: this.opts.height,
        position: 'absolute',
        top: '30%',
        left: '50%',
        marginLeft: '-' + (this.opts.width.split('px')[0] / 2) + 'px'
      }).attr('class',this.opts.winCssName).on('click', function(event) {
        event.stopPropagation();
      });
      titleAreaDom = popWinDom.find('div:eq(0)')
              .text(this.opts.title)
              .attr('class',this.opts.titleCssName);
      descAreaDom = popWinDom.find('div:eq(1)')
              .text(this.opts.desc)
              .attr('class',this.opts.descCssName);
      btnAreaDom = popWinDom.find('div:eq(2)')
              .attr('class',this.opts.btnAreaCssName);
      this.opts.btnArr.map(function(item, index) {
        btnAreaDom.append($('<button></button>')
          .text(item)
          .attr({'data-index':index, 'class':_this.opts.btnCssName})
          .on('click', function() {
            _this.opts.callback($(this).attr('data-index'));
            $.fn.popWin.hide(_this.$ele);
          }));
      });
      this.$ele.append(popWinDom);
      $.fn.popWin.show(this.$ele);
    }
  }
  $.fn.popWin = function(options) {
    var superType = new SubType(this, options);
    superType.createPopWin();
    return this;
  }
  $.fn.popWin.show = function($ele) {
    $ele.show();
  }
  $.fn.popWin.hide = function($ele) {
    $ele.hide();
  }
  $.fn.popWin.defaults = {
    width: '600',
    height: '250',
    title: 'title',
    desc: 'description',
    winCssName: 'pop-win',
    titleCssName: 'pop-title',
    descCssName: 'pop-desc',
    btnAreaCssName: 'pop-btn-box',
    btnCssName: 'pop-btn',
    btnArr: ['确定'],
    callback: function(){}
  }
})(jQuery);

如上,一个完整的弹窗插件就在这里了。

说下这个标红的 return this 是干什么用的,前面已说过 this 在这里是被选中的jQuery对象。将其return就可以在调用完我们的插件方法后可以继续调用jQ对象上的其他方法,也就是jQuery的链式操作,说玄乎点就叫级联函数。

OK!趁热打铁,我们来看看暴露出去的两个方法重写之后效果怎么样,毕竟对插件暴露部分的扩展和重写是很牛逼的一块东西。

想象个情景,你用了这个插件后觉得简单的show和hide效果简直是low爆了,决定重写这个弹出和隐藏的效果:

$.fn.popWin.show = function($ele) {
  $ele.children().first().css('top','-30%').animate({top:'30%'},500);
  $ele.show();
}
$.fn.popWin.hide = function($ele) {
  $ele.children().first().animate({top:'-30%'},500,function() {
    $ele.hide();
  });
}

你在自己的代码里加上上面两段,然后发现弹窗有了一个简单的上下滑动进入屏幕的效果,同时又不会影响我们弹窗的创建,证明我们的暴露方法还算合理。

当然你也可以让它竖着进、横着进、翻着跟头进,这就看你自己了。

最后贴上默认的样式表,为了急着想粘回去试试的同学们。

.pop-win {
  border: 1px solid #fff;
  padding: 10px;
  background-color: #fff;
  -wekbit-border-radius: 6px;
  border-radius: 6px;
  -webkit-box-shadow: 0 3px 9px rgba(0,0,0,0.3);
  box-shadow: 0 3px 9px rgba(0,0,0,0.3);
}
.pop-win-red {
  padding: 10px;
  background-color: red;
  -wekbit-border-radius: 6px;
  border-radius: 6px;
  -webkit-box-shadow: 0 3px 9px rgba(0,0,0,0.3);
  box-shadow: 0 3px 9px rgba(0,0,0,0.3);
}
.pop-title {
  width: 100%;
  height: 20%;
  line-height: 40px;
  padding-left: 10px;
  box-sizing: border-box;
  border-bottom: 1px solid #eee;
  font-size: 17px;
  font-weight: bold;
}
.pop-desc {
  width: 100%;
  height: 60%;
  box-sizing: border-box;
  padding: 10px 0 0 10px;
  border-bottom: 1px solid #eee;
}
.pop-btn-box {
  width: 100%;
  height: 20%;
  text-align: right;
}
.pop-btn {
  margin: 10px 10px 0 0;
  width: 60px;
  height: 30px;
}

当然这只是个编写插件的例子,如果要拿出去使用还需要仔细打磨。例子虽然简单,旨在抛砖引玉。