邊做邊學jQuery系列13 - 自己打造jQuery Plugin

課程錄製時間: 2009年
自己打造jQuery Plugin

看過眾多好用的jQuery Plugin,應該能體會Plugin的精神--將常用邏輯封裝成一個Method,日後只需多載入一個js,就可以隨時享用。這樣的概念對資深的程式設計人員來說一點都不陌生,跟元件化、模組化的理念是一致的,而在我們開發專案的過程中,也常有將通用邏輯提取出成為共用元件、函數的情境,在jQuery網頁設計上,我們可以透過自製Plugin完成。

【基本樣版】

要自已動手建立一個jQuery Plugin,一點都不困難。首先,我們只需新增一個js檔案,先寫好以下的樣版:

(function($) {
    $.fn.extend({
        method1: function(options) {
            //...code...    
        },
        method2: function(options) {
            //...code...
        }
     });
})(jQuery);

這個樣版本用了幾個有趣的技巧,值得好好介紹一下。首先,我們可以在最外圈看到一個(function($) { ... })(jQuery);的寫法,我叫它可拋式匿名函數(補充說明),主要用意在於避免留下任何全域(Global)性質的函數或變數,因為我們無法掌握js如何被別人引用,會與哪些js合併使用,難保不會發生跟別的js取到同名同姓的函數或變數。因此,我們把所有的函數及變數宣告都包在一個function() { ... }中,以區域變數性質存在,就不會"撞名"的困擾發生。

但是,如果宣告成區域變數,等匿名函數結束,區域變數不就消失了? 我們如果在匿名函數中對某個元素掛了事件函數,該事件函數中又參考了這個區域變數,豈不就出包了?

答案是,不會! 神奇的Closure特性會避免這種狀況。用一個例子來說明:

(function() {
   var x = 1;
   $("span").click(function() {
      alert("In event: " + x);
   });
})();
alert("Out of scope: " + typeof x);

在這個有趣的例子裡,會看到Out of scope: undefined的結果,但按下span,卻會看到In event: 1。換句話說,Javascript在宣告$("span").click(function() { ... });時,一併記錄了在範圍之外的x變數,即便x是區域變數,在匿名函數結束時就該消失,但因被其中額外宣告的匿名函數引用,便被保存下來。Closure挺奧妙的,有興趣的人可以再看看這篇補充說明

回到正題,由於jQuery允許透過jQuery.noConflict()函數將$符號留給其他Javascript Framework使用,因此我們無法確定Plugin被呼叫時,是否$仍等同於jQuery(),一種解決方法是不要用$,一律乖乖寫jQuery;而另外一個做法,就如同上面程式示範,匿名函數宣告接入一個參數$,而在呼叫匿名函數時,順便傳入jQuery當作參數。換句話說,在此匿名函數的範圍中,$就一定等同於外部傳入的jQuery,不必再擔心是否啟用了jQuery.noConflict()。

解釋完外圈匿名函數的用意,我們來看看如何新增像addClass()、removeClass()一樣可以用在jQuery物件上的Method。我們可以直接宣告如: $.fn.myMethod = function() { alert($(this).html()); };。接著jQuery物件就多了myMethod()可用,如: $("span").myMethod(); ,這是最簡單的方法。但實務上,我們會用extend()達成同樣目標: $.fn.extend({ myMethod: function() { alert($(this).html()) } }); 。extend()也常用在參數處理上,稍後再詳細介紹。

【小試身手】

了解基本樣版後,讓我們來搞一個小範例,寫兩個自訂函數,一個讓元素上下跳(jump()),另一個讓元素左右搖(shake()),此處用先前介紹過的動畫技巧來達成。有個小撇步,我們要做出跳動及搖動的並不是目標元素,而是另外建出position=absolute的分身,而原有元素的visibility則設為hidden(不用display=none是因為要保留其佔用空間,不然會打亂原有配置),待分身表演退場後再現身。分身元素的position設成absolute,top及left則由目標元素的所在位置取得,接著就可以用animate()去調marginTop,先0.1秒向上10px,0.1秒向下20px,再0.1秒向上10px回到原位。做完效果後,分身移除,本尊還原,一切恢復正常。程式會像以下的樣子:

(function($) {
    $.fn.extend({
        jump: function() {
            var target = this;
            //建立分身
            var shadow = target.clone().appendTo("body");
            var origVis = target.css("visibility");
            //隱藏本尊, 但要保留其佔用空間
            target.css("visibility", "hidden");
            shadow //設定為絕對座標,以便任意移動
                .css({
                    position: "absolute",
                    top: target.offset().top,
                    left: target.offset().left,
                    margin: "0px"
                }) //進行三次位移
                .animate({ marginTop: "-=10px" }, 100)
                .animate({ marginTop: "+=20px" }, 100)
                .animate({ marginTop: "-=10px" }, 100, function() {
                    //本尊重現
                    target.css("visibility", origVis);
                    //狡兔死走狗烹,分身移除
                    $(this).remove();
                });

        },
        shake: function() {
            var target = this;
            var shadow = target.clone().appendTo("body");
            var origVis = target.css("visibility");
            target.css("visibility", "hidden");
            shadow 
                .css({
                    position: "absolute",
                    top: target.offset().top,
                    left: target.offset().left,
                    margin: "0px"
                })
                .animate({ marginLeft: "-=10px" }, 100)
                .animate({ marginLeft: "+=20px" }, 100)
                .animate({ marginLeft: "-=10px" }, 100, function() {
                    target.css("visibility", origVis);
                    $(this).remove();
                });        
        }
    });
})(jQuery);

前端程式我們放入兩個DIV測試效果:

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script src="jquery-1.3.2.js" type="text/javascript"></script>
    <script src="jquery.myPlugin.1.js" type="text/javascript"></script>
    <style type="text/css">
    div { width: 50px; height: 50px; margin: 20px; }
    </style>
</head>
<body>
<div id="square1" style="background-color: Blue;">
</div>
<div id="square2" style="background-color: Red;">
</div>
<script type="text/javascript">
    $(function() {
        $("#square1").jump();
        $("#square2").shake();
    });
</script>
</body>
</html>

程式是可行了,但jump()與shake()的程式碼只差在animate時移動的CSS屬性,重覆性極高,程式碼重複冗長不夠簡潔,未來若要維護修改得一次改兩個地方,故我們進行簡單的重構,提取出共用函數。

(function($) {
    function moveElem(target, moveAttr) {
        //建立分身
        var shadow = target.clone().appendTo("body");
        var origVis = target.css("visibility");
        //將原元素藏起來
        target.css("visibility", "hidden");
        //建立位移的animiate()參數
        var opt1 = {}, opt2 = {};
        opt1[moveAttr] = "-=10px";
        opt2[moveAttr] = "+=20px";
        shadow  //設定為絕對座標,以便任意移動
            .css({
                position: "absolute",
                top: target.offset().top,
                left: target.offset().left,
                margin: "0px"
            }) //進行三次位移
            .animate(opt1, 100)
            .animate(opt2, 100)
            .animate(opt1, 100, function() {
                //本尊重現
                target.css("visibility", origVis);
                //分身移除
                $(this).remove();
            });
    }

    $.fn.extend({
        jump: function() {
            moveElem(this, "marginTop");
        },
        shake: function() {
            moveElem(this, "marginLeft");
        }
    });
})(jQuery);

【錦上添花】

基本功能有了,但目前我們的Plugin只提供了固定的移動值,如果開發者想要跳高一點或搖大力一點怎麼辦? 為了提供給Plugin使用者更大的彈性,我們決定仿效其他的專業Plugin,允許在呼叫時傳入額外參數,以控制動畫行為。

一般來說,我們會用一個匿名物件當成參數的載具,Plugin在接收後,會逐一取出物件上的各屬性取得參數值。但有個小問題,如果可以操控的參數有20個,開發者可能只想修改其中3個,其餘17個則沿用預設值,此時匿名物件上應該只要設定三個指定的屬性即可,沒必要20個都註明。這種有給值就用指定值,沒給就用預設值的設計架構,jQuery提供了一個好用的函數extend()輕鬆達成目標。

jQuery.extend()的運作結果有點抽象,故直接以實例說明。以jQuery.extend(objA, objB)為例,你可以想像成objA與objB各有一些屬性(方法也會比照處理,在此只提屬性),extend()會將objB有而objA沒有的屬性加到objA裡,如果objB裡的某個屬性,objA裡剛好也有同名屬性,則會用objB的屬性值覆寫。objA最後就是整合結果,或者也可以由var objC = jQuery.extend(objA, objB)取得整合結果。(objA與objC內容相同)

jQuery.extend()可以支援多個物件屬性/方法的整併,並不限兩個。例如: jQuery.extend(objA, objB, objC),objB, objC多出的屬性都會加到objA裡,如果有objA已有同名屬性,則會用objC/objB裡的屬性值覆寫之,若objB, objC都有同名屬性,則會排在後方的objC為準(後令壓前令)。

jQuery.extend()很常用來處理Plugin或函數的傳入參數,比如函數用到的參數有10個,但大部分情況呼叫時只需要指定其中一兩個,其餘用預設值。我們便可在函數中宣告一個預設值物件objDefault,裡面先放上10個屬性當預設值,,呼叫函數時則傳入objOption,裡面只放要特別指定的屬性值,經過var objSetting = jQuery.extend(objDefault, objOption)之後,我們得到10個"有指定用指定值,沒指定用預設值"的屬性群組供後續使用。舉個例子:

function addDiv(options) {
    var defaults = { 
        border: "solid 1px black",
        backgroundColor: "#cccccc",
        width: "200px", height: "50px",
    	margin: "10px"
    };
    var settings = $.extend(defaults, options);
    $("<div></div>").css(settings).appendTo("body");
}
addDiv({ width: "400px" });
addDiv({ backgroundColor: "orange", height: "100px" });

了解jQuery.extend()後,我們為jump()及shake()多增加一個參數,可以控制移動的距離以及動畫時間長度,另外再支援一個callback函數,在動作完成後進行呼叫。於是,程式可以改寫如下:

(function($) {
    function moveElem(target, moveAttr, options) {
        //處理參數
        var settings = { duration: 100, movement: 10, oncomplete: null };
        $.extend(settings, options);
        //建立分身
        var shadow = target.clone().appendTo("body");
        var origVis = target.css("visibility");
        //將原元素藏起來
        target.css("visibility", "hidden");
        //建立位移的animiate()參數
        var opt1 = {}, opt2 = {};
        opt1[moveAttr] = "-=" + settings.movement + "px";
        opt2[moveAttr] = "+=" + (settings.movement * 2) + "px";
        shadow  //設定為絕對座標,以便任意移動
            .css({
                position: "absolute",
                top: target.offset().top,
                left: target.offset().left,
                margin: "0px"
            }) //進行三次位移
            .animate(opt1, settings.duration)
            .animate(opt2, settings.duration)
            .animate(opt1, settings.duration, function() {
                //本尊重現
                target.css("visibility", origVis);
                //分身移除
                $(this).remove();
                //如有callback,呼叫之
                if ($.isFunction(settings.oncomplete))
                    settings.oncomplete();
            });
    }

    $.fn.extend({
        jump: function(options) {
            moveElem($(this), "marginTop", options);
        },
        shake: function(options) {
            moveElem($(this), "marginLeft", options);
        }
    });
})(jQuery);

試試$("#square1").jump({ movement: 30, duration: 500, oncomplete: function() { alert("DONE!"); } });,可驗證移動距離變大,速度變慢,結束時也多彈出DONE字樣。

【代代相傳】

到此,我們的Plugin已算功能完整,但有一個小缺憾。jQuery大部分的函數都可以串接,但目前的寫法在呼叫jump()/shake()後,後面若再串上一般的jQuery Method,則會發生該Method找不到的狀況。原因是我們的函數在執行完成後,並未將原本的jQuery物件一五一十地傳承下去。做法很簡單,將原本的邏輯包在return this.each(function() { ... })中即可:

    $.fn.extend({
        jump: function(options) {
            return this.each(function() {
                moveElem($(this), "marginTop", options);
            });
        },
        shake: function(options) {
            return this.each(function() {
                moveElem($(this), "marginLeft", options);
            });
        }
    });

唯一要留意的地方是,在this.each(function() { ... });中,this指向的會是jQuery元素陣列中的個別元素,故要包成$(this)才能轉化為jQuery物件。我們寫一行: $("#square2").click(function() { $(this).jump().after("<span>SPAN</span>"); }); 就可以在點選時,除了跳動外,還在方塊後方新增<SPAN>元素。

【範例檔案下載】

歡迎推文分享:



 
RSS
【工商服務】
OrcsWeb: Windows Server Hosting
twMVC

關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。