本文將透過一個實例來引出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; }
当然这只是个编写插件的例子,如果要拿出去使用还需要仔细打磨。例子虽然简单,旨在抛砖引玉。