好像没有找到过关于bootstrap插件部分的文章,前两天在做一个input放大镜,是采用bootstrap插件编码规范,从而发现原来bootstrap的编码非常让人愉悦。故而想系统性的了解一下。

一、自定义编译js文件

默认bootstrap.min.js提供12个完整插件脚本,我没有找到可以通过网上直接构建的,应该有只是源代码结构非常漂亮,每一个写前端的都心需要自己动手构建一个比较短小的bootstrap插件脚本库出来。

bootstrap是通过 grunt 来构建的,关于grunt使用见参考资料。

bootstrap: {
    src: [
        'js/transition.js',
        'js/alert.js',
        'js/button.js',
        'js/carousel.js',
        'js/collapse.js',
        'js/dropdown.js',
        'js/modal.js',
        'js/tooltip.js',
        'js/popover.js',
        'js/scrollspy.js',
        'js/tab.js',
        'js/affix.js'
    ],
    dest: 'dist/js/<%= pkg.name %>.js'
}

以上是 Gruntfile.js 的一部分【他JSON格式】,用于拼接所有插件脚本,所以很简单只需要把不需要的加个 // 即可。

当然这里只有JS部分,还得去掉CSS部分, bootstrap采用的是LESS编写CSS,存放于 /less/bootstrap.less 也只需要找到我们并不需要的插件样式的 @import "xxx.less"

完成所有操作所以,只需要在源代码根目录下,输入CMD命令: grunt dist,会自动在 dist 目录下重新生成一份新的文件。

二、闭包

+function ($) {
  'use strict';

  // BUTTON PUBLIC CLASS DEFINITION
  // ==============================

  var Button = function (element, options) {
    this.$element  = $(element)
    this.options   = $.extend({}, Button.DEFAULTS, options)
    this.isLoading = false
  }}(jQuery);

很典型的一个闭包应用,这里将 jQuery 对象做为 $ 参数,这样可以避免闭包类使用的是 jQuery 对象,而不是其他类库或自己定义的 $

细心的人还会发现,为什么 function 前面会有一个 + 号,这是一个保障作用,特别是将bootstrap脚本库作为项目当中的一个库;并将所有脚本压缩成一个文件时,如果紧邻前面脚本没有加上 ; 来区分结束时,就会合在一起。有时我们也会使用 ;function(){} 逗号。

三、避免插件同名冲突

  var old = $.fn.button

  $.fn.button             = Plugin
  $.fn.button.Constructor = Button


  // BUTTON NO CONFLICT
  // ==================

  $.fn.button.noConflict = function () {
    $.fn.button = old
    return this
  }

$.fn.button.noConflict 每一个插件都会相类似的代码,用来处理插件同名冲突问题,比如:button,在 jquery.ui 下也有一个同名,假如你同时引用这两个类库时,就会产生两个问题:

  1. jquery.ui 在 bootstrap 前面,这个时候你使用 $('').button() 调用的是 bootstrap 的 button
  2. bootstrap 在 jquery.ui 前面,这个时候你使用 $('').button() 调用的是 jquery.ui 的 button

为什么?因为加载顺序的关系插件被覆盖了,而对于 jquery.ui 而言并未有同时冲突的处理,所以这里强烈要求将 jquery.ui库放在bootstrap前面,有这个前提,再来看我下面的示例,就会明白 noConflict 有什么作用。

//$.fn.button.noConflict();
$('button').button();

jquery.ui库放在bootstrap前面 的前提下,我注释掉第1行代码时,调用的是 bootstrapbutton 插件。反之调用的是 jquery.ui 下的 button 插件。怎么样?有没有感叹 bootstrap 的编码方式很完美。

四、data-* 属性

所有bootstrap的插件都可以不需要在页面上写上脚本调用代码,包括插件参数,都是以 data-参数名="值" 这种形式出现,有没有觉得很高大上。

<button type="button" class="btn btn-primary" data-toggle="button" aria-pressed="false" autocomplete="off">
  Single toggle
</button>

以上当我点击按钮时会自动加载 .action 样式,再次点击取消样式。而实现这一切要归于以下代码:

  $(document)
    .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) {
      var $btn = $(e.target)
      if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn')
      Plugin.call($btn, 'toggle')
      e.preventDefault()
    })
    .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) {
      $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type))
    })

这里的 click.bs.button.data-api 带有多级的事件命名空间,虽然不太会有重复的事件名称存在,但是如果你有洁癖不想bootstrap监控这些事件,我们可以直接注销掉,比如:

$(document).off('.bs'); // 所有bootstrap监控事件
$(document).off('click.bs.button'); // 只对button插件有效

另一方面将参数 data-* 化,核心其实就是调用 jQuery.data() 他会自动处理这一方面,比如:

  Button.prototype.setState = function (state) {
    var d    = 'disabled'
    var $el  = this.$element
    var val  = $el.is('input') ? 'val' : 'html'
    var data = $el.data()

    state = state + 'Text'

    if (!data.resetText) $el.data('resetText', $el[val]())

    $el[val](data[state] || this.options[state])

    // push to event loop to allow forms to submit
    setTimeout(function () {
      state == 'loadingText' ?
        $el.addClass(d).attr(d, d) :
        $el.removeClass(d).removeAttr(d);
    }, 0)
  }

其中 var data = $el.data() 就是自动将当前元素的所有 data-* 转化为对象。关于这个详见 jQuery.Data()

五、API接口

bootstrap插件都是单一入口、可链接式,比如我们可以这么调用一个 button 插件。

$('button').button('loading').addClass('active');

关于单一入口,比如:

$('button').button('loading');
$('button').button('reset');

是不是觉得记忆成本非常低,有没有?

六、关于事件

bootstrap 每个插件都有属于自己的一序列自定义事件,比如 modelhidden.bs.modal,而所有这一切都是通过 jQuery.trigger 来触发,关于 jQuery.trigger 事件的一些细节,比如:版本不同事件是否冒泡也不一样等等。

当我们需要监听这些事件时,我们可以这么做:

$('#model').on('hidden.bs.modal', function(e) {});

其中 $('#model') 是当前模态框的元素,这样有效的限定同一页面多个模态分别监听。

当然也可以用 $(document) 监听,这归于 jQuery.trigger 事件冒泡,但如果有多个模态框时那么任何一个关闭都将引时该监听。

一个题外话,在查bootstrap源代码时,发现大量的使用 $.proxy(this.hide, this),作用是保持在事件回调函数内上下文依然是插件本身。

七、CSS3兼容

每个插件都有做如下处理:

      var callbackRemove = function () {
        that.removeBackdrop()
        callback && callback()
      }
      $.support.transition && this.$element.hasClass('fade') ?
        this.$backdrop
          .one('bsTransitionEnd', callbackRemove)
          .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) :
        callbackRemove()

    } else if (callback) {
      callback()
    }

这里会监测是否支持CSS3中的 transition,当不支持时直接回调用,否则当动画结束后再回调。

参考资料

  1. Grunt 快速入门
  2. bootstrap github