本文將通過一個實例來引出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;}
當然這只是個編寫插件的例子,如果要拿出去使用還需要仔細打磨。例子雖然簡單,旨在拋磚引玉。