使用 jQuery UI Widget Factory 編寫有狀態的插件(Stateful Plugins)

Note

這一章節的內容是基於 Scott Gonzalez 一篇博客 Building Stateful jQuery Plugins(已獲作者許可)

雖然大多數的 jQuery 插件都是無狀態的(stateless),也就是說, 與插件進行交互的就限於調用插件時的那一組對象, 但是有好大一部分功能需求沒辦法通過這種簡單的插件模式來實現。

爲了填補這一空白,jQuery UI 實現一套更加先進的插件系統。 它可以管理狀態,允許通過一個插件暴露多個函數,並提供多個擴展點。 這套系統被稱爲 widget factory,對應jQuery.widget, 也是 jQuery UI 1.8 的一部分。不過,它是可以獨立於 jQuery UI 使用的。

我們接下來創建一個簡單的進度條插件,用來演示 widget factory 的能力。

我們首先創建一個只能設置一次的進度條。 下面是實現代碼,使用 jQuery.widget 創建一個插件。 它接受兩個參數,插件名字和帶有具體實現方法的對象。 當插件被調用時,它會創建一個新的插件實例,而插件方法的執行對象也就是那個實例。 這與標準 jQuery 插件實現有兩點是很不一樣的。一是,執行者是對象而不是 DOM 元素; 二是,執行者永遠是單個對象,而不是元素集。

Example 8.3. 用 jQuery UI widget factory 創建一個簡單的有狀態的插件

$.widget("nmk.progressbar", {
    _create: function() {
        var progress = this.options.value + "%";
        this.element
            .addClass("progressbar")
            .text(progress);
    }
});

插件名字必須包含一個命名空間,這裏我們用了 nmk 這個命名空間。 但這個命名空間有個限制——只允許一層,也就是說,我們不能使用像nmk.foo 這樣的命名空間。另外可以看到 widget factory 給我們提供了兩個屬性。一是this.element, 它指向一個只包含一個元素的 jQuery 對象,如果插件是由包含多個元素的 jQuery 對象調用時,會給其中的每一個元素都分配一個插件實例, 並讓this.element 指向它;二是this.options, 是包含鍵值對形式的插件參數的 hash 對象,插件的參數就是像這樣傳遞進來的。

Note

本例中使用了 nmk 作爲命名空間。 命名空間 ui 則是保留給官方 jQuery UI 插件的。 創建自己的插件的時候,應該使用自有的命名空間的, 這樣可以讓人一看就清楚這插件哪來的,是否是一個大體系的一部分。

Example 8.4. 給 widget 傳遞參數

$("<div></div>")
    .appendTo( "body" )
    .progressbar({ value: 20 });

當我們調用 jQuery.widget 時,與創建標準插件的方式一樣, 它也是通過往 jQuery.fn 上面添加方法的方式來擴展 jQuery 對象。 而那個方法的名稱就是我們定義的插件名稱除去命名空間的部分,案例中是 jQuery.fn.progressbar。調用時所傳遞的參數會傳遞給插件實例的 this.options。在下面的代碼中,我們可以在參數中設置一些默認值。 在設計 API 的時候,你應該先搞清楚最通常的用例,並據此設定相應的默認參數, 那麼這些參數就成爲可選項了。

Example 8.5. 給 widget 設置默認值

$.widget("nmk.progressbar", {
    // default options
    options: {
        value: 0
    },

    _create: function() {
        var progress = this.options.value + "%";
        this.element
            .addClass( "progressbar" )
            .text( progress );
    }
});

接下來就要初始化進度條了。我們使它可以通過調用插件實例方法的方式來執行一些操作。 要給插件定義方法,只需要將其實現代碼放在定義體內即可。 我們也可以通過在方法名前加下劃線的方式來定義“私有”方法。

Example 8.6. 創建 widget 的方法

$.widget("nmk.progressbar", {
    options: {
        value: 0
    },

    _create: function() {
        var progress = this.options.value + "%";
        this.element
            .addClass("progressbar")
            .text(progress);
    },

    // create a public method
    value: function(value) {
        // no value passed, act as a getter
        if (value === undefined) {
            return this.options.value;
        // value passed, act as a setter
        } else {
            this.options.value = this._constrain(value);
            var progress = this.options.value + "%";
            this.element.text(progress);
        }
    },

    // create a private method
    _constrain: function(value) {
        if (value > 100) {
            value = 100;
        }
        if (value < 0) {
            value = 0;
        }
        return value;
    }
});

將方法名作爲參數傳進去即可調用插件實例的方法。 如果調用的方法需要傳遞參數,只需要將那些參數作爲後續參數一同傳遞。

Example 8.7. 調用插件實例的方法

var bar = $("<div></div>")
    .appendTo("body")
    .progressbar({ value: 20 });

// get the current value
alert(bar.progressbar("value"));

// update the value
bar.progressbar("value", 50);

// get the current value again
alert(bar.progressbar("value"));

Note

初始化用所用的 jQuery 方法,向它傳遞方法名就可以執行方法,這看起來似乎很奇怪。 但這樣可以在維持原來的鏈式調用的方式的同時,防止 jQuery 命名空間被污染。

有一個方法 option,是自動生成的。它可以實現在初始化過後, 對參數進行查詢或設置,就像 css,attr 的用法那樣,只傳名字時是查詢, 名字和值都有時是做設置,如果是包含鍵值對的 hash 對象則進行多項設置。 進行查詢時,插件會返回當前該參數的值。 做設置時,插件的_setOption 方法會被調用,修改多少個就調用多少次。 我們可以自己實現_setOption 方法來響應這些參數的修改。

Example 8.8. 當參數被修改時執行一些操作

$.widget("nmk.progressbar", {
    options: {
        value: 0
    },

    _create: function() {
        this.element.addClass("progressbar");
        this._update();
    },

    _setOption: function(key, value) {
        this.options[key] = value;
        this._update();
    },

    _update: function() {
        var progress = this.options.value + "%";
        this.element.text(progress);
    }
});

擴展插件的一個最簡單的辦法就是添加回調功能, 這樣使用者就可以根據插件狀態的改變來採取行動。下面,我們來嘗試添加一個回調功能, 在進度達到 100% 時觸發。_trigger 方法介紹三個參數: 回調名稱,觸發回調的本地事件對象以及相關的數據。雖然其中只有回調名稱是必須的, 不過其它參數對使用者來說挺有用的。比如說,創建一個可拖拽插件, 我們可以在觸發回調時將原生的 mouseover 事件對象傳遞過去, 用戶在回調函數裏就可以根據這個對象中的 x/y 座標對拖拽進行一些處理。

Example 8.9. 提供回調功能讓用戶進行擴展

$.widget("nmk.progressbar", {
    options: {
        value: 0
    },

    _create: function() {
        this.element.addClass("progressbar");
        this._update();
    },

    _setOption: function(key, value) {
        this.options[key] = value;
        this._update();
    },

    _update: function() {
        var progress = this.options.value + "%";
        this.element.text(progress);
        if (this.options.value == 100) {
            this._trigger("complete", null, { value: 100 });
        }
    }
});

回調函數實際上只是另外一種參數,因此你也可以像其它參數一樣進行查詢和修改了。 無論回調函數是否設置,事件都會觸發的。事件類型則是由插件名稱和回調名稱合併而成。 回調和事件被觸發時會收到同樣的兩個參數:事件對象和相關數據。可以看下面的例子。

如果你的插件提供些功能是允許用戶阻止操作的,最好的方式就是提供一個可撤銷的回調。 用戶可以像撤銷原生事件那樣,調用 event.preventDefault() 或者return false,去撤銷回調和相關的事件。如果用戶撤銷了回調,_trigger 方法會返回 false, 在插件中就可以據此採取相應的動作。

Example 8.10. 綁定 widget 事件

var bar = $("<div></div>")
    .appendTo("body")
    .progressbar({
        complete: function(event, data) {
            alert( "Callbacks are great!" );
        }
    })
    .bind("progressbarcomplete", function(event, data) {
        alert("Events bubble and support many handlers for extreme flexibility.");
        alert("The progress bar value is " + data.value);
    });

bar.progressbar("option", "value", 100);

有時候,插件讓用戶可以應用,然後過一陣再解除應用是有意義的。 這可以通過 destroy 方法的來實現。在 destroy 方法內部, 你應該取消你的插件能造成的所有修改,初始化過程中或者後面的使用中造成的。destroy 方法在 DOM 刪除時會被自動調用,所以它可以用於垃圾回收。 默認的destroy 方法會刪掉 DOM 元素與插件實例直接的連接, 所以在覆蓋它時是調用原先插件提供的基礎 destroy 方法,是很重要的。

Example 8.11. 給 widget 添加 destroy 方法

$.widget( "nmk.progressbar", {
    options: {
        value: 0
    },

    _create: function() {
        this.element.addClass("progressbar");
        this._update();
    },

    _setOption: function(key, value) {
        this.options[key] = value;
        this._update();
    },

    _update: function() {
        var progress = this.options.value + "%";
        this.element.text(progress);
        if (this.options.value == 100 ) {
            this._trigger("complete", null, { value: 100 });
        }
    },

    destroy: function() {
        this.element
            .removeClass("progressbar")
            .text("");

        // call the base destroy function
        $.Widget.prototype.destroy.call(this);
    }
});

Widget factory 是創建有狀態的插件的唯一途徑。我們有不同的插件模型可供選用, 各有優缺。Widget factory 解決了大量基礎性問題,有助於提高效率,有利於代碼重用, 非常適合用來創建 jQuery UI 和其它有狀態的插件。

==================================================================================================================================

使用jQueryUI的widget來寫插件,相比於基本的jquery插件有一些好處:

* 方便實現繼承,代碼重用

* 默認是單例

* widget已經給你實現好的一些常用方法,例如destroy

帶來好處的同時也帶來了荊棘和陷阱,本文的目的就是梳理這些荊棘,標出哪裏有陷阱。

 

基本知識:命名規範,public, private, this, this.element

如何開始寫一個widget呢?模板如下:

(function ($) {
    // utility functions (won’t be inherited)
    function foo() {}
    
    $.widget('命名空間.插件名', $.繼承插件的命名空間.插件名,{ /* snip */ });
})(jQuery);        

其中命名空間是可選的,要不要繼承別的widget也是可選的。大頭是後面snip的部分,這也是下文要講的。

一般來說工具函數寫在widget外面比較合適,但如果你想要這些工具函數被子類繼承,則需要寫在widget裏面。

寫在widget裏面的,就有public和private之分,規則是:

public方法首字符不是_

private方法首字符是_


當調用方法時,會先判斷是否以_開頭,如果是則不執行調用。

如果我非要在外面調用private方法,該怎麼做?並非一點辦法也沒有:

var instance = $('<div>');
instance.mywidget('publicFunction'); // work
instance.mywidget('_privateFunction'); // silently fail
instance.data('mywidget')._privateFunction(); // work
$.mynamespace.mywidget.prototype._privateFunction(); // work
 

在widget內,this表示的是什麼?我們在widget的一個public函數內用console.log(this)打出來瞧瞧:

image

日誌顯示,this是一個$.widget.$.(anonymous function).(anonymous function)

this.element是變成widget的那個jQuery對象,如果要用jquery的方法,往往首先要取到jquery對象。

this.options是插件的選項,下文會詳解。

this.__proto__包含了插件中定義的所有public和private函數,以及繼承過來的方法。

這裏簡單介紹一下__proto__:每個對象都會在其內部初始化一個屬性,就是__proto__,當我們訪問一個對象的屬性 時,如果這個對象內部不存在這個屬性,那麼他就會去__proto__裏找這個屬性,這個__proto__又會有自己的__proto__,於是就這樣 一直找下去,也就是我們平時所說的原型鏈的概念。

_create  _init    destroy

widget factory實現了一種單例模式,即不允許在同一個jQuery對象上多次實例化。

當調用$(XX).widgetName()進行初始化的時候,會執行以下代碼(源碼截取自jquery.ui.widget.js):

var instance = $.data( this, name ); // 從widget自身取出名字爲name的數據
if ( instance ) {
    instance.option( options || {} )._init();  // 若該數據已經存在則只調用_init
} else {
    $.data( this, name, new object( options, this ) ); // 若數據還沒有則新建一個實例,並將實例保存
}

 

當調用$(XX).widgetName(‘destroy’)進行銷燬的時候,執行以下代碼(源碼截取自jquery.ui.widget.js):

this.element
    .unbind( "." + this.widgetName )
    .removeData( this.widgetName ); // 刪除在create時保存的數據 

有一個removeData的操作,那麼下次調用$(XX).widgetName()就會重新實例化了。

 

需要注意的是,destroy方法在jquery.ui.widget.js中是有默認實現的,而_create和_init沒有實現。因此如果用自己的方法覆蓋destroy,不要忘記調用默認的

destory: function () {
    console.log('destory');

    // call the original destroy method since we overwrote it
    $.Widget.prototype.destroy.call(this);
}

 

以下示例代碼驗證_create和_init的區別以及destroy的作用:

var mw = $('#test').myWidget(); // _create  _init
mw = $('#test').myWidget(); // _init
mw.myWidget('destory');
mw = $('#test').myWidget(); // _create  _init

 

那麼在_create和_init以及destroy裏分別應該做什麼:

_create: 生成HTML,事件綁定。

_init: 執行默認的初始化動作,例如把頁面變成初始狀態。

destory: 調用$.Widget.prototype.destroy.call(this),刪除HTML。

 

注意:綁定事件要注意給事件名加命名空間後綴:例如 .bind('mouseenter.mywidget', this._hover)

 

options

選項,在widget中的定義是options,而在調用時是option,注意定義的時候有s,調用的時候沒s。

定義:

option

s

: {
    field1: 'default',
    function1: function () {
        console.log('default option function1');
    }
},

調用:

$('#test').mywidget('option', 'field1', 2);

widget默認實現了兩個函數:_setOptions和_setOption,_setOptions的實現就是對每個要修改的option調用_setOption,也就是說真正修改的動作在_setOption裏。因此,如果要重寫_setOption函數,則一定不要忘記寫

$.Widget.prototype._setOption.apply(this, arguments);

 

_setOptions和_setOption這倆函數什麼時候被調用呢?用下面這個例子來說明。

例如有這樣的_setOption和_setOptions:

 
_setOption: function (key, value) {
    console.log('_setOption: key=%s  value=%s', key, value);
    $.Widget.prototype._setOption.apply(this, arguments);
},

_setOptions: function (options) {
    var key;

    console.group('_setOptions');
    for (key in options) {
        this._setOption(key, options[key]);
    }
    console.groupEnd();

    return this;
},

以及一個打印options值的函數printOptions:

printOptions: function () {
    console.group('options');
    console.log('field1: %s', this.options.field1);
    console.log('function1: %s', this.options.function1);
    console.groupEnd();
},

我們像下面這樣調用:

var instance = $('<div>');

// create widget with default options
console.group();

instance.mywidget(); 
instance.mywidget('printOptions');

console.groupEnd();

// create widget with specified options
instance.mywidget('destroy');
console.group();

var opts = {
    field1: 'specified',
    function1: function () {
        console.log('specified option function1');
    },
};
instance.mywidget(opts); 
instance.mywidget('printOptions');

console.log('-------------');
instance.mywidget(opts); 

console.groupEnd();

// modify options
console.group();

instance.mywidget('option', 'field1', 2);
instance.mywidget('printOptions');

console.groupEnd();
 

打出的日誌如下:

image

日誌分爲三大塊。

第一塊是不使用options來初始化,可以看到直接使用定義裏默認的options,是不調用_setOption的。

第二塊是使用options來初始化,這一塊做了兩個實驗(日誌中用--------將兩塊分隔),第一個實驗是完全重建(_create, _init),從日誌可以看到並沒有調用_setOption;第二個實驗只是重新初始化(_init),用的options都一樣,從日誌可以看到它調用了_setOption,且在_init之前調用的。

第三塊不是初始化,而僅僅是修改option值,可以清楚看到調用了_setOption。

 

何時會調用_setOption的結論:

1. 像instance.mywidget('option', 'field1', 2); 這樣顯式設置option時。

2. 帶着options初始化時:

如果實例不存在,即需要調用_create,則不調用_setOption;

如果實例已存在,僅需要調用_init,則會在調用_init之前調用_setOption。

 

 

_trigger

注意這個_trigger是jQueryUI widget factory裏的,和jQuery裏$.fn命名空間下的trigger函數不是一個東西(後者不帶下劃線)。

_trigger一般用來回調用戶傳入options的callback。

在插件內部調用_trigger(‘myEvent’)即相當於調用options裏面的myEvent這個回調函數。

要改動options裏的event handler應該怎麼做呢?不要使用bind/unbind,而是去修改options:

// bind (overwrite, not add event handler)
mw.myWidget('option', 'myEvent', function (event, ui) {
    console.log('new implement');
});
// unbind
mw.myWidget('option', 'myEvent', null);

 

總結一下:

this._trigger(‘eventName’)是widget特有的,用於調用options裏定義的callback。

this.element.trigger(‘eventName’)是jQuery的,可參考jQuery的事件的用法。(其中this.element是表示該插件的jQuery對象)

 

一個_trigger的樣例:

// 模板
this._trigger( "callbackName" , [eventObject], [uiObject] )

callbackNameThe name of the event you want to dispatcheventObject(Optional)An (mocked) event object._trigger wraps this object and stores it in event.originalEventThe user receives an object with event.type ==this.widgetEventPrefix + "eventname"uiObject(Optional)An object containing useful properties the user may need to access.Protip: Use a method like._uito generate objects with a consistent schema. 

// 調用樣例
this._trigger( "hover", e /* e.type == "mouseenter" */, { hovered: $(e.target)});
// The user can subscribe using an init option
$("#elem").filterable( { hover: function(e,ui) { } } );
// Or with traditional event binding/delegation
$("#elem").bind( "filterablehover" , function(e,ui) { } );
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章