鍍金池/ 教程/ HTML/ jQuery 插件的設(shè)計(jì)模式
中介者模式
MVVM
亨元模式
設(shè)計(jì)模式分類概覽表
ES Harmony
組合模式
CommonJS
jQuery 插件的設(shè)計(jì)模式
外觀模式
觀察者模式
建造者模式
構(gòu)造器模式
外觀模式
簡(jiǎn)介
AMD
原型模式
設(shè)計(jì)模式的分類
觀察者模式
命名空間模式
代理模式
編寫設(shè)計(jì)模式
適配器模式
反模式
什么是設(shè)計(jì)模式
模塊化模式
MVC
Mixin 模式
裝飾模式
設(shè)計(jì)模式的結(jié)構(gòu)
單例模式
迭代器模式
命令模式
工廠模式
MVP
暴露模塊模式
惰性初始模式

jQuery 插件的設(shè)計(jì)模式

jQuery插件開發(fā)在過去幾年里進(jìn)步了很多. 我們寫插件的方式不再僅僅只有一種,相反有很多種?,F(xiàn)實(shí)中,某些插件設(shè)計(jì)模式在解決某些特殊的問題或者開發(fā)組件的時(shí)候比其他模式更有效。

有些開發(fā)者可能希望使用 jQuery UI 部件工廠; 它對(duì)于創(chuàng)建復(fù)雜而又靈活的UI組件是很強(qiáng)大的。有些開發(fā)者可能不想使用。

有些開發(fā)者可能想把它們的插件設(shè)計(jì)得更像模塊(與模塊模式相似)或者使用一種更現(xiàn)代化的模塊格式。 有些開發(fā)者想讓他們的插件利用原型繼承的特點(diǎn)。有些則希望使用自定義事件或者通過發(fā)布/訂閱信息來實(shí)現(xiàn)插件和他們的其它App之間的通信。

在經(jīng)過了想創(chuàng)建一刀切的jquery插件樣板的數(shù)次嘗試之后,我開始考慮插件模式。這樣的樣板在理論上是一個(gè)很好的主意,但現(xiàn)實(shí)是,我們很少使用固定的方式并且總是使用一種模式來寫插件。

讓我們假設(shè)我們已經(jīng)為了某個(gè)目標(biāo)去著手嘗試編寫我們自己的jQuery插件,并且我們可以放心的把一些東西放在一起運(yùn)作。它是起作用的。它做了它需要去做的,但是也許我們覺得它可以被構(gòu)造得更好。也許它應(yīng)該更加靈活或者被設(shè)計(jì)用來解決更多開發(fā)者普遍都碰到過的問題才對(duì)。如果這聽起來很熟悉,那么你也許會(huì)發(fā)現(xiàn)這一章是很有用的。在其中,我們將探討大量的jQuery插件模式,它們?cè)谄渌_發(fā)者的環(huán)境中都工作的不錯(cuò)。

注意:盡管開頭我們將簡(jiǎn)要回顧一些jQuery插件的基礎(chǔ)知識(shí),但這一章是針對(duì)中級(jí)到高級(jí)的開發(fā)者的。 如果你覺得對(duì)此還沒有做足夠的準(zhǔn)備,我很高興的建議你去看一看jQuery官方的插件/創(chuàng)作(Plugins/Authoring )指導(dǎo),Ben Alman的插件類型指導(dǎo)(plugin style guide)和RemySharp的“寫得不好的jQuery插件的癥候(Signs of a Poorly Written jQuery Plugin)”,作為開始這一節(jié)之前的閱讀材料。

模式

jQuery插件有一些具體的規(guī)則,它們是整個(gè)社區(qū)能夠?qū)崿F(xiàn)這令人難以置信的多樣性的原因之一。在最基本的層面上,我們能夠編寫一個(gè)簡(jiǎn)單地向jQuery的jQuery.fn對(duì)象添加一個(gè)新的功能屬性的插件,像下面這樣:

$.fn.myPluginName = function () {
    // our plugin logic
};

對(duì)于緊湊性而言這是很棒的,而下面的代碼將會(huì)是一個(gè)更好的構(gòu)建基礎(chǔ):

(function( $ ){
  $.fn.myPluginName = function () {
    // our plugin logic
  };
})( jQuery );

在這里,我們將我們的插件邏輯封裝到一個(gè)匿名函數(shù)中。為了確保我們使用的$標(biāo)記作為簡(jiǎn)寫形式不會(huì)造成任何jQuery和其它Javascript庫(kù)之間的沖突,我們簡(jiǎn)單的將其傳入這個(gè)閉包中,它會(huì)將其映射到美元符號(hào)上。這就確保了它能夠不被任何范圍之外的執(zhí)行影響到。

編寫這種模式的一個(gè)可選方式是使用jQuery.extend(),它使得我們能夠一次定義多個(gè)函數(shù),并且有時(shí)能夠獲得更多的語(yǔ)義上的意義。

(function( $ ){
    $.extend($.fn, {
        myplugin: function(){
            // your plugin logic
        }
    });
})( jQuery );

現(xiàn)在我們已經(jīng)回顧了一些jQuery插件的基礎(chǔ),但是許多更多的工作可借以更進(jìn)一步。A Lightweight Start是我們將要探討的該 設(shè)計(jì)模式的第一個(gè)完整的插件,它涵蓋了我們可以在每天的基礎(chǔ)的插件開發(fā)工作中用到的一些最佳實(shí)踐, 細(xì)數(shù)了一些值得推廣應(yīng)用的常見問題描述。

注意: 盡管下面大多數(shù)的模式都會(huì)得到解釋,我還是建議大家通過閱讀代碼里的注釋來研究它們,因?yàn)檫@些注釋能夠提供關(guān)于為什么一個(gè)具體的最佳實(shí)踐會(huì)被應(yīng)用這個(gè)問題的更深入的理解。 我也應(yīng)該提醒下,沒有前面的工作往后這些沒有一樣是可能的,它們是來自于jQuery社區(qū)的其他成員的輸入和建議。我已經(jīng)將它們列到每一種模式中了,以便諸位可以根據(jù)各自的工作方向來閱讀相關(guān)的內(nèi)容,如果感興趣的話。

A Lightweight Start 模式

讓我們用一些遵循了(包括那些在jQuery 插件創(chuàng)作指導(dǎo)中的)最佳實(shí)踐的基礎(chǔ)的東西來開始我們針對(duì)插件模式的深入探討。這一模式對(duì)于插件開發(fā)的新手和只想要實(shí)現(xiàn)一些簡(jiǎn)單的東西(例如工具插件)的人來說是理想的。A Lightweight Start 使用到了下面這些東西:

  • 諸如分號(hào)放置在函數(shù)調(diào)用之前這樣一些通用的最佳實(shí)踐(我們將在下面的注釋中解釋為什么要這樣做)
  • window,document,undefined作為參數(shù)傳入。
  • 基本的默認(rèn)對(duì)象。
  • 一個(gè)簡(jiǎn)單的針對(duì)跟初始化創(chuàng)建和要一起運(yùn)作的元素的賦值相關(guān)的邏輯的插件構(gòu)造器。
  • 擴(kuò)展默認(rèn)的選項(xiàng)。
  • 圍繞構(gòu)造器的輕量級(jí)的封裝,它有助于避免諸如實(shí)例化多次的問題。
  • 堅(jiān)持最大限度可讀性的jQuery核心風(fēng)格的指導(dǎo)方針。
/*!
 * jQuery lightweight plugin boilerplate
 * Original author: @ajpiano
 * Further changes, comments: @addyosmani
 * Licensed under the MIT license
 */

// the semi-colon before the function invocation is a safety
// net against concatenated scripts and/or other plugins
// that are not closed properly.
;(function ( $, window, document, undefined ) {

    // undefined is used here as the undefined global
    // variable in ECMAScript 3 and is mutable (i.e. it can
    // be changed by someone else). undefined isn't really
    // being passed in so we can ensure that its value is
    // truly undefined. In ES5, undefined can no longer be
    // modified.

    // window and document are passed through as local
    // variables rather than as globals, because this (slightly)
    // quickens the resolution process and can be more
    // efficiently minified (especially when both are
    // regularly referenced in our plugin).

    // Create the defaults once
    var pluginName = "defaultPluginName",
        defaults = {
            propertyName: "value"
        };

    // The actual plugin constructor
    function Plugin( element, options ) {
        this.element = element;

        // jQuery has an extend method that merges the
        // contents of two or more objects, storing the
        // result in the first object. The first object
        // is generally empty because we don't want to alter
        // the default options for future instances of the plugin
        this.options = $.extend( {}, defaults, options) ;

        this._defaults = defaults;
        this._name = pluginName;

        this.init();
    }

    Plugin.prototype.init = function () {
        // Place initialization logic here
        // We already have access to the DOM element and
        // the options via the instance, e.g. this.element
        // and this.options
    };

    // A really lightweight plugin wrapper around the constructor,
    // preventing against multiple instantiations
    $.fn[pluginName] = function ( options ) {
        return this.each(function () {
            if ( !$.data(this, "plugin_" + pluginName )) {
                $.data( this, "plugin_" + pluginName,
                new Plugin( this, options ));
            }
        });
    }

})( jQuery, window, document );

用例:

$("#elem").defaultPluginName({
  propertyName: "a custom value"
});

完整的 Widget 工廠模式

雖然jQuery插件創(chuàng)作指南是對(duì)插件開發(fā)的一個(gè)很棒的介紹,但它并不能幫助掩蓋我們不得不定期處理的常見的插件管道任務(wù)。

jQuery UI Widget工廠是這個(gè)問題的一種解決方案,能幫助我們基于面向?qū)ο笤瓌t構(gòu)建復(fù)雜的,具有狀態(tài)性的插件。它也簡(jiǎn)化了我們插件實(shí)體的通信,也淡化了許多我們?cè)谝恍┗A(chǔ)的插件上工作時(shí)必須去編寫代碼的重復(fù)性的工作。

具有狀態(tài)性的插件幫助我們對(duì)它們的當(dāng)前狀態(tài)保持跟進(jìn),也允許我們?cè)诓寮怀跏蓟蟾淖兤鋵傩浴?有關(guān)Widget工廠最棒的事情之一是大部分的jQuery UI庫(kù)的實(shí)際上都是使用它作為其組件的基礎(chǔ)。這意味著如果我們是在尋找超越這一模式的架構(gòu)的進(jìn)一步指導(dǎo),我們將沒必要去超越GitHub上的jQuery UI進(jìn)行思考。

jQuery UI Widget 工廠模式涵蓋了包括事件觸發(fā)在內(nèi)幾乎所有的默認(rèn)支持的工廠方法。每一個(gè)模式的最后都包含了所有這些方法的使用注釋,還在內(nèi)嵌的注釋中給出了更深入的指導(dǎo)。

/*!
 * jQuery UI Widget-factory plugin boilerplate (for 1.8/9+)
 * Author: @addyosmani
 * Further changes: @peolanha
 * Licensed under the MIT license
 */

;(function ( $, window, document, undefined ) {

    // define our widget under a namespace of your choice
    // with additional parameters e.g.
    // $.widget( "namespace.widgetname", (optional) - an
    // existing widget prototype to inherit from, an object
    // literal to become the widget's prototype );

    $.widget( "namespace.widgetname" , {

        //Options to be used as defaults
        options: {
            someValue: null
        },

        //Setup widget (e.g. element creation, apply theming
        // , bind events etc.)
        _create: function () {

            // _create will automatically run the first time
            // this widget is called. Put the initial widget
            // setup code here, then we can access the element
            // on which the widget was called via this.element.
            // The options defined above can be accessed
            // via this.options this.element.addStuff();
        },

        // Destroy an instantiated plugin and clean up
        // modifications the widget has made to the DOM
        destroy: function () {

            // this.element.removeStuff();
            // For UI 1.8, destroy must be invoked from the
            // base widget
            $.Widget.prototype.destroy.call( this );
            // For UI 1.9, define _destroy instead and don't
            // worry about
            // calling the base widget
        },

        methodB: function ( event ) {
            //_trigger dispatches callbacks the plugin user
            // can subscribe to
            // signature: _trigger( "callbackName" , [eventObject],
            // [uiObject] )
            // e.g. this._trigger( "hover", e /*where e.type ==
            // "mouseenter"*/, { hovered: $(e.target)});
            this._trigger( "methodA", event, {
                key: value
            });
        },

        methodA: function ( event ) {
            this._trigger( "dataChanged", event, {
                key: value
            });
        },

        // Respond to any changes the user makes to the
        // option method
        _setOption: function ( key, value ) {
            switch ( key ) {
            case "someValue":
                // this.options.someValue = doSomethingWith( value );
                break;
            default:
                // this.options[ key ] = value;
                break;
            }

            // For UI 1.8, _setOption must be manually invoked
            // from the base widget
            $.Widget.prototype._setOption.apply( this, arguments );
            // For UI 1.9 the _super method can be used instead
            // this._super( "_setOption", key, value );
        }
    });

})( jQuery, window, document );

用例:

var collection = $("#elem").widgetName({
  foo: false
});

collection.widgetName("methodB");

嵌套的命名空間插件模式

如我們?cè)诒緯那懊嫠?,為我們的代碼加入命名空間是避免與其它的全局命名空間中的對(duì)象和變量產(chǎn)生沖突的一種方法。它們是很重要的,因?yàn)槲覀兿胍Wo(hù)我們的插件的運(yùn)作不會(huì)突然被頁(yè)面上另外一段使用了同名變量或者插件的腳本所打斷。作為全局命名空間的好市民,我們也必須盡我們所能來阻止其他開發(fā)者的腳本由于同樣的問題而執(zhí)行起來發(fā)生問題。

Javascript并不像其它語(yǔ)言那樣真的內(nèi)置有對(duì)命名空間的支持,但它卻有可以被用來達(dá)到同樣效果的對(duì)象。雇傭一個(gè)頂級(jí)對(duì)象作為我們命名空間的名稱,我們就可以使用相同的名字檢查頁(yè)面上另外一個(gè)對(duì)象的存在性。如果這樣的對(duì)象不存在,那么我們就定義它;如果它存在,就簡(jiǎn)單的用我們的插件對(duì)其進(jìn)行擴(kuò)展。

對(duì)象(或者更確切的說,對(duì)象常量)可以被用來創(chuàng)建內(nèi)嵌的命名空間,namespace.subnamespace.pluginName,諸如此類。而為了保持簡(jiǎn)單,下面的命名空間樣板會(huì)向我們展示有關(guān)這些概念的入門我們所需要的一切。

/*!
 * jQuery namespaced "Starter" plugin boilerplate
 * Author: @dougneiner
 * Further changes: @addyosmani
 * Licensed under the MIT license
 */

;(function ( $ ) {
    if (!$.myNamespace) {
        $.myNamespace = {};
    };

    $.myNamespace.myPluginName = function ( el, myFunctionParam, options ) {
        // To avoid scope issues, use "base" instead of "this"
        // to reference this class from internal events and functions.
        var base = this;

        // Access to jQuery and DOM versions of element
        base.$el = $( el );
        base.el = el;

        // Add a reverse reference to the DOM object
        base.$el.data( "myNamespace.myPluginName" , base );

        base.init = function () {
            base.myFunctionParam = myFunctionParam;

            base.options = $.extend({},
            $.myNamespace.myPluginName.defaultOptions, options);

            // Put our initialization code here
        };

        // Sample Function, Uncomment to use
        // base.functionName = function( parameters ){
        //
        // };
        // Run initializer
        base.init();
    };

    $.myNamespace.myPluginName.defaultOptions = {
        myDefaultValue: ""
    };

    $.fn.mynamespace_myPluginName = function
        ( myFunctionParam, options ) {
        return this.each(function () {
            (new $.myNamespace.myPluginName( this,
            myFunctionParam, options ));
        });
    };

})( jQuery );

用例:

$("#elem").mynamespace_myPluginName({
  myDefaultValue: "foobar"
});

(使用Widget工廠)自定義事件插件模式

在本書的Javascript設(shè)計(jì)模式一節(jié),我們討論了觀察者模式,而后繼續(xù)論述到了jQuery對(duì)于自定義事件的支持,其為實(shí)現(xiàn)發(fā)布/訂閱提供了一種類似的解決方案。

這里的基本觀點(diǎn)是當(dāng)我們的應(yīng)用程序中發(fā)生了某些有趣的事情時(shí),頁(yè)面中的對(duì)象能夠發(fā)布事件通知。其他對(duì)象就會(huì)訂閱(或者偵聽)這些事件,并且據(jù)此產(chǎn)生回應(yīng)。我們應(yīng)用程序的這一邏輯所產(chǎn)生的效果是更加顯著的解耦,每一個(gè)對(duì)象不再需要直接同另外一個(gè)對(duì)象進(jìn)行通信。

在接下來的jQuery UI widget工廠模式中,我們將實(shí)現(xiàn)一個(gè)基本的基于自定義事件的發(fā)布/訂閱系統(tǒng),它允許我們的插件向應(yīng)用程序的其余部分發(fā)布事件通知,而這些部分將對(duì)此產(chǎn)生回應(yīng)。

/*!
 * jQuery custom-events plugin boilerplate
 * Author: DevPatch
 * Further changes: @addyosmani
 * Licensed under the MIT license
 */

// In this pattern, we use jQuery's custom events to add
// pub/sub (publish/subscribe) capabilities to widgets.
// Each widget would publish certain events and subscribe
// to others. This approach effectively helps to decouple
// the widgets and enables them to function independently.

;(function ( $, window, document, undefined ) {
    $.widget( "ao.eventStatus", {
        options: {

        },

        _create : function() {
            var self = this;

            //self.element.addClass( "my-widget" );

            //subscribe to "myEventStart"
            self.element.on( "myEventStart", function( e ) {
                console.log( "event start" );
            });

            //subscribe to "myEventEnd"
            self.element.on( "myEventEnd", function( e ) {
                console.log( "event end" );
            });

            //unsubscribe to "myEventStart"
            //self.element.off( "myEventStart", function(e){
                ///console.log( "unsubscribed to this event" );
            //});
        },

        destroy: function(){
            $.Widget.prototype.destroy.apply( this, arguments );
        },
    });
})( jQuery, window , document );

// Publishing event notifications
// $( ".my-widget" ).trigger( "myEventStart");
// $( ".my-widget" ).trigger( "myEventEnd" );

用例:

var el = $( "#elem" );
el.eventStatus();
el.eventStatus().trigger( "myEventStart" );

使用DOM-To-Object橋接模式的原型繼承

正如前面所介紹的,在Javascript中,我們并不需要那些在其它經(jīng)典的編程語(yǔ)言中找到的類的傳統(tǒng)觀念,但我們確實(shí)需要原型繼承。有了原型繼承,對(duì)象就可以從其它對(duì)象繼承而來了。我們可以將此概念應(yīng)用到j(luò)Query的插件開發(fā)中。

Yepnope.js作者Alex Sexton和jQuery團(tuán)隊(duì)成員Scott Gonzalez已經(jīng)矚目于這個(gè)主題的細(xì)節(jié)??傊?,他們發(fā)現(xiàn)為了組織模塊化的開發(fā),使定義插件邏輯的對(duì)象同插件生成過程本身分離是有好處的。

這一好處就是對(duì)我們插件代碼的測(cè)試會(huì)變得顯著的簡(jiǎn)單起來,并且我們也能夠在不改變?nèi)魏挝覀兯鶎?shí)現(xiàn)的對(duì)象API的方式,這一前提下,適應(yīng)事物在幕后運(yùn)作的方式。

在Sexton關(guān)于這個(gè)主題的文章中,他實(shí)現(xiàn)了一個(gè)使我們能夠?qū)⑽覀兊囊话愕倪壿嫺郊拥教囟ú寮臉颍覀円呀?jīng)在下面的模式中將它實(shí)現(xiàn)。

這一模式的另外一個(gè)優(yōu)點(diǎn)是我們不需要去不斷的重復(fù)同樣的插件初始化代碼,這確保了DRY開發(fā)背后的觀念得以維持。一些開發(fā)者或許也會(huì)發(fā)現(xiàn)這一模式的代碼相比其它更加易讀。

/*!
 * jQuery prototypal inheritance plugin boilerplate
 * Author: Alex Sexton, Scott Gonzalez
 * Further changes: @addyosmani
 * Licensed under the MIT license
 */

// myObject - an object representing a concept we wish to model
// (e.g. a car)
var myObject = {
  init: function( options, elem ) {
    // Mix in the passed-in options with the default options
    this.options = $.extend( {}, this.options, options );

    // Save the element reference, both as a jQuery
    // reference and a normal reference
    this.elem  = elem;
    this.$elem = $( elem );

    // Build the DOM's initial structure
    this._build();

    // return this so that we can chain and use the bridge with less code.
    return this;
  },
  options: {
    name: "No name"
  },
  _build: function(){
    //this.$elem.html( "<h1>"+this.options.name+"</h1>" );
  },
  myMethod: function( msg ){
    // We have direct access to the associated and cached
    // jQuery element
    // this.$elem.append( "<p>"+msg+"</p>" );
  }
};

// Object.create support test, and fallback for browsers without it
if ( typeof Object.create !== "function" ) {
    Object.create = function (o) {
        function F() {}
        F.prototype = o;
        return new F();
    };
}

// Create a plugin based on a defined object
$.plugin = function( name, object ) {
  $.fn[name] = function( options ) {
    return this.each(function() {
      if ( ! $.data( this, name ) ) {
        $.data( this, name, Object.create( object ).init(
        options, this ) );
      }
    });
  };
};

用例:

$.plugin( "myobj", myObject );

$("#elem").myobj( {name: "John"} );

var collection = $( "#elem" ).data( "myobj" );
collection.myMethod( "I am a method");

jQuery UI Widget 工廠橋接模式

如果你喜歡基于過去的設(shè)計(jì)模式的對(duì)象的生成插件這個(gè)主意,那么你也許會(huì)對(duì)這個(gè)在jQuery UI Widget工廠中發(fā)現(xiàn)的叫做$.widget.bridge的方法感興趣。

這座橋基本上是在充當(dāng)使用$.widget創(chuàng)建的Javascript對(duì)象和jQuery核心API之間的中間層,它提供了一種實(shí)現(xiàn)基于對(duì)象的插件定義的更加內(nèi)置的解決方案。實(shí)際上,我們能夠使用自定義的構(gòu)造器去創(chuàng)建具有狀態(tài)性的插件。

此外,$.widget.bridge還提供了對(duì)許多其它功能的訪問,包括下面這些:

  • 公共的和私有的方法都如人們?cè)诮?jīng)典的OOP中所希望的方式被處理(例如,公共的方法被暴露出來,而對(duì)私有方法的調(diào)用則是不可能的)。
  • 防止多次初始化的自動(dòng)保護(hù)。
  • 傳入對(duì)象實(shí)體的自動(dòng)生成,而對(duì)它們的存儲(chǔ)則在內(nèi)置的$.datacache范圍之內(nèi)。
  • 選項(xiàng)可以在初始化后修改。

有關(guān)使用這一模式的更多信息,請(qǐng)看看下面內(nèi)嵌的注釋:

/*!
 * jQuery UI Widget factory "bridge" plugin boilerplate
 * Author: @erichynds
 * Further changes, additional comments: @addyosmani
 * Licensed under the MIT license
 */

// a "widgetName" object constructor
// required: this must accept two arguments,
// options: an object of configuration options
// element: the DOM element the instance was created on
var widgetName = function( options, element ){
  this.name = "myWidgetName";
  this.options = options;
  this.element = element;
  this._init();
}

// the "widgetName" prototype
widgetName.prototype = {

    // _create will automatically run the first time this
    // widget is called
    _create: function(){
        // creation code
    },

    // required: initialization logic for the plugin goes into _init
    // This fires when our instance is first created and when
    // attempting to initialize the widget again (by the bridge)
    // after it has already been initialized.
    _init: function(){
        // init code
    },

    // required: objects to be used with the bridge must contain an
    // "option". Post-initialization, the logic for changing options
    // goes here.
    option: function( key, value ){

        // optional: get/change options post initialization
        // ignore if you don't require them.

        // signature: $("#foo").bar({ cool:false });
        if( $.isPlainObject( key ) ){
            this.options = $.extend( true, this.options, key );

        // signature: $( "#foo" ).option( "cool" ); - getter
        } else if ( key && typeof value === "undefined" ){
            return this.options[ key ];

        // signature: $( "#foo" ).bar("option", "baz", false );
        } else {
            this.options[ key ] = value;
        }

        // required: option must return the current instance.
        // When re-initializing an instance on elements, option
        // is called first and is then chained to the _init method.
        return this; 
    },

    // notice no underscore is used for public methods
    publicFunction: function(){
        console.log( "public function" );
    },

    // underscores are used for private methods
    _privateFunction: function(){
        console.log( "private function" );
    }
};

用例:

// connect the widget obj to jQuery's API under the "foo" namespace
$.widget.bridge( "foo", widgetName );

// create an instance of the widget for use
var instance = $( "#foo" ).foo({
   baz: true
});

// our widget instance exists in the elem's data
// Outputs: #elem
console.log(instance.data( "foo" ).element);

// bridge allows us to call public methods
// Outputs: "public method"
instance.foo("publicFunction");

// bridge prevents calls to internal methods
instance.foo("_privateFunction");

使用 Widget 工廠的 jQuery Mobile 小部件

jQuery Mobile 是一個(gè) jQuery 項(xiàng)目框架,為設(shè)計(jì)同時(shí)能運(yùn)行在主流移動(dòng)設(shè)備和平臺(tái)以及桌面平臺(tái)的大多數(shù)常見 Web 應(yīng)用帶來便利。我們可以僅編寫一次代碼,而無(wú)需為每種設(shè)備或操作系統(tǒng)編寫特定的應(yīng)用,就能使其同時(shí)運(yùn)行在 A、B 和 C 級(jí)瀏覽器。

JQuery mobile 背后的基本原理也可應(yīng)用于插件和小部件的開發(fā)。

接下來介紹的模式令人感興趣的是,已熟悉使用 jQuery UI Widget Factory 模式的開發(fā)者能夠很快地編寫針對(duì)移動(dòng)設(shè)備優(yōu)化的小部件,即便這會(huì)在不同設(shè)備中存在細(xì)微的差異。

下面為移動(dòng)優(yōu)化的widget同前面我們看到的標(biāo)準(zhǔn)UI widget模式相比,有許多有趣的不同之處。

$.mobile.widget 是繼承于現(xiàn)有的widget原型的引用。對(duì)于標(biāo)準(zhǔn)的widget, 通過任何這樣的原型進(jìn)行基礎(chǔ)的開發(fā)都是沒有必要的,但是使用這種為移動(dòng)應(yīng)用定制的jQuery widget 原型,它提供了更多的“選項(xiàng)”格式供內(nèi)部訪問。

在_create()中,教程提供了關(guān)于官方的jQuery 移動(dòng) widget如何處理元素選擇,對(duì)于基于角色的能夠更好的適應(yīng)jQM標(biāo)記的方法的選擇。這并不是說標(biāo)準(zhǔn)的選擇不被推薦,只是說這種方法也許可以給予jQuery 移動(dòng)頁(yè)面的架構(gòu)更多的意義。

也有以注釋形式提供的關(guān)于將我們的插件方法應(yīng)用于頁(yè)面創(chuàng)建,還有通過數(shù)據(jù)角色和數(shù)據(jù)屬性選擇插件應(yīng)用程序,這些內(nèi)容的指導(dǎo)。

/*!
 * (jQuery mobile) jQuery UI Widget-factory plugin boilerplate (for 1.8/9+)
 * Author: @scottjehl
 * Further changes: @addyosmani
 * Licensed under the MIT license
 */

;(function ( $, window, document, undefined ) {

    // define a widget under a namespace of our choice
    // here "mobile" has been used in the first argument
    $.widget( "mobile.widgetName", $.mobile.widget, {

        // Options to be used as defaults
        options: {
            foo: true,
            bar: false
        },

        _create: function() {
            // _create will automatically run the first time this
            // widget is called. Put the initial widget set-up code
            // here, then we can access the element on which
            // the widget was called via this.element
            // The options defined above can be accessed via
            // this.options

            // var m = this.element,
            // p = m.parents( ":jqmData(role="page")" ),
            // c = p.find( ":jqmData(role="content")" )
        },

        // Private methods/props start with underscores
        _dosomething: function(){ ... },

        // Public methods like these below can can be called
        // externally:
        // $("#myelem").foo( "enable", arguments );

        enable: function() { ... },

        // Destroy an instantiated plugin and clean up modifications
        // the widget has made to the DOM
        destroy: function () {
            // this.element.removeStuff();
            // For UI 1.8, destroy must be invoked from the
            // base widget
            $.Widget.prototype.destroy.call( this );
            // For UI 1.9, define _destroy instead and don't
            // worry about calling the base widget
        },

        methodB: function ( event ) {
            //_trigger dispatches callbacks the plugin user can
            // subscribe to
            // signature: _trigger( "callbackName" , [eventObject],
            //  [uiObject] )
            // e.g. this._trigger( "hover", e /*where e.type ==
            // "mouseenter"*/, { hovered: $(e.target)});
            this._trigger( "methodA", event, {
                key: value
            });
        },

        methodA: function ( event ) {
            this._trigger( "dataChanged", event, {
                key: value
            });
        },

        // Respond to any changes the user makes to the option method
        _setOption: function ( key, value ) {
            switch ( key ) {
            case "someValue":
                // this.options.someValue = doSomethingWith( value );
                break;
            default:
                // this.options[ key ] = value;
                break;
            }

            // For UI 1.8, _setOption must be manually invoked from
            // the base widget
            $.Widget.prototype._setOption.apply(this, arguments);
            // For UI 1.9 the _super method can be used instead
            // this._super( "_setOption", key, value );
        }
    });

})( jQuery, window, document );

用例:

var instance = $( "#foo" ).widgetName({
  foo: false
});

instance.widgetName( "methodB" );

不論什么時(shí)候jQuery Mobile中的一個(gè)新頁(yè)面被創(chuàng)建了,我們也都可以自己初始化這個(gè)widget。但一個(gè)(通過data-role="page"屬性發(fā)現(xiàn)的)jQuery Mobile 頁(yè)面一開始被初始化時(shí), jQuery Mobile的頁(yè)面插件會(huì)自己派發(fā)一個(gè)創(chuàng)建事件。我們能偵聽那個(gè)(稱作 “pagecreate”的)事件,并且在任何時(shí)候只要新的頁(yè)面一被創(chuàng)建,就自動(dòng)的讓我們的插件運(yùn)行。

$(document).on("pagecreate", function ( e ) {
    // In here, e.target refers to the page that was created
    // (it's the target of the pagecreate event)
    // So, we can simply find elements on this page that match a
    // selector of our choosing, and call our plugin on them.
    // Here's how we'd call our "foo" plugin on any element with a
    // data-role attribute of "foo":
    $(e.target).find( "[data-role="foo"]" ).foo( options );

    // Or, better yet, let's write the selector accounting for the configurable
    // data-attribute namespace
    $( e.target ).find( ":jqmData(role="foo")" ).foo( options );
});

現(xiàn)在我們可以在一個(gè)頁(yè)面中簡(jiǎn)單的引用包含了我們的widget和pagecreate綁定的腳本,而它將像任何其它的jQuery Mobile插件一樣自動(dòng)的運(yùn)行。

RequireJS 和 jQuery UI Widget 工廠

如我們?cè)诋?dāng)代模塊化設(shè)計(jì)模式一節(jié)所述,RequireJS是一種兼容AMD的腳本裝載器,它提供了將應(yīng)用程序邏輯封裝到可管理的模塊中,這樣一個(gè)干凈的解決方案。

它能夠(通過它的順序插件)將模塊按照正確的順序加載,簡(jiǎn)化了借助它優(yōu)秀的r.js優(yōu)化器整合腳本的過程,并且提供了在每一個(gè)模塊的基礎(chǔ)上定義動(dòng)態(tài)依賴的方法。

在下面的樣板模式中,我們展示了一種兼容AMD的jQuery UI widget(這里是RequireJS)如何能夠被定義成做到下面這些事情:

  • 允許widget模塊依賴的定義,構(gòu)建在前面早先的jQuery UI Widget 工廠模式之上。
  • 展示一種為創(chuàng)建(使用Underscore.js 微模板)模板化的widget傳入HTML模板集的方法。
  • 包括一種如果我們希望晚一點(diǎn)將其傳入到RequireJS優(yōu)化器,以使我們能夠?qū)ξ覀兊膚idget模塊做出調(diào)整的快速提示。
/*!
 * jQuery UI Widget + RequireJS module boilerplate (for 1.8/9+)
 * Authors: @jrburke, @addyosmani
 * Licensed under the MIT license
 */

// Note from James:
//
// This assumes we are using the RequireJS+jQuery file, and
// that the following files are all in the same directory:
//
// - require-jquery.js
// - jquery-ui.custom.min.js (custom jQuery UI build with widget factory)
// - templates/
//    - asset.html
// - ao.myWidget.js

// Then we can construct the widget as follows:

// ao.myWidget.js file:
define( "ao.myWidget", ["jquery", "text!templates/asset.html", "underscore", "jquery-ui.custom.min"], function ( $, assetHtml, _ ) {

    // define our widget under a namespace of our choice
    // "ao" is used here as a demonstration
    $.widget( "ao.myWidget", {

        // Options to be used as defaults
        options: {},

        // Set up widget (e.g. create element, apply theming,
        // bind events, etc.)
        _create: function () {

            // _create will automatically run the first time
            // this widget is called. Put the initial widget
            // set-up code here, then we can access the element
            // on which the widget was called via this.element.
            // The options defined above can be accessed via
            // this.options

            // this.element.addStuff();
            // this.element.addStuff();

            // We can then use Underscore templating with
            // with the assetHtml that has been pulled in
            // var template = _.template( assetHtml );
            // this.content.append( template({}) );
        },

        // Destroy an instantiated plugin and clean up modifications
        // that the widget has made to the DOM
        destroy: function () {
            // this.element.removeStuff();
            // For UI 1.8, destroy must be invoked from the base
            // widget
            $.Widget.prototype.destroy.call( this );
            // For UI 1.9, define _destroy instead and don't worry
            // about calling the base widget
        },

        methodB: function ( event ) {
            // _trigger dispatches callbacks the plugin user can
            // subscribe to
            // signature: _trigger( "callbackName" , [eventObject],
            // [uiObject] )
            this._trigger( "methodA", event, {
                key: value
            });
        },

        methodA: function ( event ) {
            this._trigger("dataChanged", event, {
                key: value
            });
        },

        // Respond to any changes the user makes to the option method
        _setOption: function ( key, value ) {
            switch (key) {
            case "someValue":
                // this.options.someValue = doSomethingWith( value );
                break;
            default:
                // this.options[ key ] = value;
                break;
            }

            // For UI 1.8, _setOption must be manually invoked from
            // the base widget
            $.Widget.prototype._setOption.apply( this, arguments );
            // For UI 1.9 the _super method can be used instead
            // this._super( "_setOption", key, value );
        }

    });
});

用例:

index.html:

<script data-main="scripts/main" src="http://requirejs.org/docs/release/1.0.1/minified/require.js"></script>

main.js

require({

    paths: {
        "jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min",
        "jqueryui": "https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.18/jquery-ui.min",
        "boilerplate": "../patterns/jquery.widget-factory.requirejs.boilerplate"
    }
}, ["require", "jquery", "jqueryui", "boilerplate"],
function (req, $) {

    $(function () {

        var instance = $("#elem").myWidget();
        instance.myWidget("methodB");

    });
});

全局和每次調(diào)用的重載選項(xiàng)(最佳調(diào)用模式)

對(duì)于我們的下一個(gè)模式,我們將來看看一種為插件選擇默認(rèn)和手動(dòng)配置選項(xiàng)的優(yōu)化了的方法。 定義插件選項(xiàng),我們大多數(shù)人可能熟悉的一種方法是,通過默認(rèn)的字面上的對(duì)象將其傳遞到$.extend(),如我們?cè)谖覀兓A(chǔ)的插件樣板中所展示的。

然而,如果我們正工作在一種帶有許多的定制選項(xiàng),對(duì)于這些定制選項(xiàng)我們希望用戶在全局和每一次調(diào)用的級(jí)別都能重載,那樣我們就能以更加優(yōu)化一點(diǎn)的方式構(gòu)造事物。

相反,通過明確的引用定義在插件命名空間中的一個(gè)選項(xiàng)對(duì)象(例如,$fn.pluginName.options),還有將此同任何在其最初被調(diào)用時(shí)傳遞到插件的選項(xiàng)混合,用戶就要對(duì)在插件初始化期間傳遞選項(xiàng),或者在插件外部重載選項(xiàng),這兩者有所選擇(如這里所展示的)。

/*!
 * jQuery "best options" plugin boilerplate
 * Author: @cowboy
 * Further changes: @addyosmani
 * Licensed under the MIT license
 */

;(function ( $, window, document, undefined ) {

    $.fn.pluginName = function ( options ) {

        // Here's a best practice for overriding "defaults"
        // with specified options. Note how, rather than a
        // regular defaults object being passed as the second
        // parameter, we instead refer to $.fn.pluginName.options
        // explicitly, merging it with the options passed directly
        // to the plugin. This allows us to override options both
        // globally and on a per-call level.

        options = $.extend( {}, $.fn.pluginName.options, options );

        return this.each(function () {

            var elem = $(this);

        });
    };

    // Globally overriding options
    // Here are our publicly accessible default plugin options
    // that are available in case the user doesn't pass in all
    // of the values expected. The user is given a default
    // experience but can also override the values as necessary.
    // e.g. $fn.pluginName.key ="otherval";

    $.fn.pluginName.options = {

        key: "value",
        myMethod: function ( elem, param ) {

        }
    };

})( jQuery, window, document );

用例:

$("#elem").pluginName({
  key: "foobar"
});

高可配置和可變插件模式

在這個(gè)模式中,同Alex Sexton的原型繼承插件模式類似,我們插件的邏輯并不嵌套在一個(gè)jQuery插件自身之中.取而代之我們使用了一個(gè)構(gòu)造器和一種定義在它的原型之上的對(duì)象字面值,來定義我們的插件邏輯.jQuery隨后被用在插件對(duì)象的實(shí)際實(shí)例中。

通過玩了兩個(gè)小花樣,定制被帶到了一個(gè)新的層次,其中之一就是我們?cè)谇懊嬉呀?jīng)看到的模式:

  • 選項(xiàng)不論是全局的還是集合中每一個(gè)元素的,都可以被重載。
  • 選在可以通過HTML5數(shù)據(jù)屬性(在下面會(huì)有展示)在每一個(gè)元素的級(jí)別被定制.這有利于可以被應(yīng)用到集合中元素的插件行為,但是會(huì)導(dǎo)致在不需要使用一個(gè)不同的默認(rèn)值實(shí)例化每一個(gè)元素的前提下定制的內(nèi)聯(lián)。

在不怎么正規(guī)的場(chǎng)合我們不會(huì)經(jīng)常見到這種非常規(guī)的選項(xiàng),但是它能夠成為一種重要的清晰方案(只要我們不介意這種內(nèi)聯(lián)的方式).如果不知道這個(gè)東西在那兒會(huì)起作用,那就想象著要為大型的元素集合編寫一個(gè)可拖動(dòng)的插件,這種場(chǎng)景.我們可以像下面這樣定制它們的選項(xiàng):

$( ".item-a" ).draggable( {"defaultPosition":"top-left"} );
$( ".item-b" ).draggable( {"defaultPosition":"bottom-right"} );
$( ".item-c" ).draggable( {"defaultPosition":"bottom-left"} );
//etc

但是使用我們模式的內(nèi)聯(lián)方式,下面這樣是可能的。

$( ".items" ).draggable();
html
<li class="item" data-plugin-options="{"defaultPosition":"top-left"}"></div>
<li class="item" data-plugin-options="{"defaultPosition":"bottom-left"}"></div>

諸如此類.我們也許更加偏好這些方法之一,但它僅僅是我們值得去意識(shí)到的另外一個(gè)差異。

/*
 * "Highly configurable" mutable plugin boilerplate
 * Author: @markdalgleish
 * Further changes, comments: @addyosmani
 * Licensed under the MIT license
 */

// Note that with this pattern, as per Alex Sexton's, the plugin logic
// hasn't been nested in a jQuery plugin. Instead, we just use
// jQuery for its instantiation.

;(function( $, window, document, undefined ){

  // our plugin constructor
  var Plugin = function( elem, options ){
      this.elem = elem;
      this.$elem = $(elem);
      this.options = options;

      // This next line takes advantage of HTML5 data attributes
      // to support customization of the plugin on a per-element
      // basis. For example,
      // <div class=item" data-plugin-options="{"message":"Goodbye World!"}"></div>
      this.metadata = this.$elem.data( "plugin-options" );
    };

  // the plugin prototype
  Plugin.prototype = {
    defaults: {
      message: "Hello world!"
    },

    init: function() {
      // Introduce defaults that can be extended either
      // globally or using an object literal.
      this.config = $.extend( {}, this.defaults, this.options,
      this.metadata );

      // Sample usage:
      // Set the message per instance:
      // $( "#elem" ).plugin( { message: "Goodbye World!"} );
      // or
      // var p = new Plugin( document.getElementById( "elem" ),
      // { message: "Goodbye World!"}).init()
      // or, set the global default message:
      // Plugin.defaults.message = "Goodbye World!"

      this.sampleMethod();
      return this;
    },

    sampleMethod: function() {
      // e.g. show the currently configured message
      // console.log(this.config.message);
    }
  }

  Plugin.defaults = Plugin.prototype.defaults;

  $.fn.plugin = function( options ) {
    return this.each(function() {
      new Plugin( this, options ).init();
    });
  };

  // optional: window.Plugin = Plugin;

})( jQuery, window , document );

用例:

$("#elem").plugin({
  message: "foobar"
});

是什么造就了模式之外的一個(gè)優(yōu)秀插件?

在今天結(jié)束之際,設(shè)計(jì)模式僅僅只是編寫可維護(hù)的jQuery插件的一個(gè)方面。還有大量其它的因素值得考慮,而我也希望分享下對(duì)于用第三方插件來解決一些其它的問題,我自己的選擇標(biāo)準(zhǔn)。

質(zhì)量

下一篇:簡(jiǎn)介