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

2018-02-24 15:19 更新

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

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

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

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

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

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

注意:盡管開頭我們將簡要回顧一些jQuery插件的基礎(chǔ)知識,但這一章是針對中級到高級的開發(fā)者的。 如果你覺得對此還沒有做足夠的準(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ī)則,它們是整個社區(qū)能夠?qū)崿F(xiàn)這令人難以置信的多樣性的原因之一。在最基本的層面上,我們能夠編寫一個簡單地向jQuery的jQuery.fn對象添加一個新的功能屬性的插件,像下面這樣:

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

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

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

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

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

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

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

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

A Lightweight Start 模式

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

  • 諸如分號放置在函數(shù)調(diào)用之前這樣一些通用的最佳實踐(我們將在下面的注釋中解釋為什么要這樣做)
  • window,document,undefined作為參數(shù)傳入。
  • 基本的默認(rèn)對象。
  • 一個簡單的針對跟初始化創(chuàng)建和要一起運作的元素的賦值相關(guān)的邏輯的插件構(gòu)造器。
  • 擴展默認(rèn)的選項。
  • 圍繞構(gòu)造器的輕量級的封裝,它有助于避免諸如實例化多次的問題。
  • 堅持最大限度可讀性的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)作指南是對插件開發(fā)的一個很棒的介紹,但它并不能幫助掩蓋我們不得不定期處理的常見的插件管道任務(wù)。

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

具有狀態(tài)性的插件幫助我們對它們的當(dāng)前狀態(tài)保持跟進(jìn),也允許我們在插件被初始化之后改變其屬性。 有關(guān)Widget工廠最棒的事情之一是大部分的jQuery UI庫的實際上都是使用它作為其組件的基礎(chǔ)。這意味著如果我們是在尋找超越這一模式的架構(gòu)的進(jìn)一步指導(dǎo),我們將沒必要去超越GitHub上的jQuery UI進(jìn)行思考。

jQuery UI Widget 工廠模式涵蓋了包括事件觸發(fā)在內(nèi)幾乎所有的默認(rèn)支持的工廠方法。每一個模式的最后都包含了所有這些方法的使用注釋,還在內(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");

嵌套的命名空間插件模式

如我們在本書的前面所述,為我們的代碼加入命名空間是避免與其它的全局命名空間中的對象和變量產(chǎn)生沖突的一種方法。它們是很重要的,因為我們想要保護(hù)我們的插件的運作不會突然被頁面上另外一段使用了同名變量或者插件的腳本所打斷。作為全局命名空間的好市民,我們也必須盡我們所能來阻止其他開發(fā)者的腳本由于同樣的問題而執(zhí)行起來發(fā)生問題。

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

對象(或者更確切的說,對象常量)可以被用來創(chuàng)建內(nèi)嵌的命名空間,namespace.subnamespace.pluginName,諸如此類。而為了保持簡單,下面的命名空間樣板會向我們展示有關(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è)計模式一節(jié),我們討論了觀察者模式,而后繼續(xù)論述到了jQuery對于自定義事件的支持,其為實現(xiàn)發(fā)布/訂閱提供了一種類似的解決方案。

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

在接下來的jQuery UI widget工廠模式中,我們將實現(xiàn)一個基本的基于自定義事件的發(fā)布/訂閱系統(tǒng),它允許我們的插件向應(yīng)用程序的其余部分發(fā)布事件通知,而這些部分將對此產(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)典的編程語言中找到的類的傳統(tǒng)觀念,但我們確實需要原型繼承。有了原型繼承,對象就可以從其它對象繼承而來了。我們可以將此概念應(yīng)用到j(luò)Query的插件開發(fā)中。

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

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

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

這一模式的另外一個優(yōu)點是我們不需要去不斷的重復(fù)同樣的插件初始化代碼,這確保了DRY開發(fā)背后的觀念得以維持。一些開發(fā)者或許也會發(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è)計模式的對象的生成插件這個主意,那么你也許會對這個在jQuery UI Widget工廠中發(fā)現(xiàn)的叫做$.widget.bridge的方法感興趣。

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

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

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

有關(guān)使用這一模式的更多信息,請看看下面內(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 是一個 jQuery 項目框架,為設(shè)計同時能運行在主流移動設(shè)備和平臺以及桌面平臺的大多數(shù)常見 Web 應(yīng)用帶來便利。我們可以僅編寫一次代碼,而無需為每種設(shè)備或操作系統(tǒng)編寫特定的應(yīng)用,就能使其同時運行在 A、B 和 C 級瀏覽器。

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

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

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

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

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

也有以注釋形式提供的關(guān)于將我們的插件方法應(yīng)用于頁面創(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" );

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

$(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)在我們可以在一個頁面中簡單的引用包含了我們的widget和pagecreate綁定的腳本,而它將像任何其它的jQuery Mobile插件一樣自動的運行。

RequireJS 和 jQuery UI Widget 工廠

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

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

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

  • 允許widget模塊依賴的定義,構(gòu)建在前面早先的jQuery UI Widget 工廠模式之上。
  • 展示一種為創(chuàng)建(使用Underscore.js 微模板)模板化的widget傳入HTML模板集的方法。
  • 包括一種如果我們希望晚一點將其傳入到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="https://atts.w3cschool.cn/attachments/image/cimg/pre>

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)用的重載選項(最佳調(diào)用模式)

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

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

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

/*!
 * 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"
});

高可配置和可變插件模式

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

通過玩了兩個小花樣,定制被帶到了一個新的層次,其中之一就是我們在前面已經(jīng)看到的模式:

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

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

$( ".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>

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

/*
 * "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"
});

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

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

質(zhì)量

對于你所寫的Javascript和jQuery插件,請堅持遵循最佳實踐的做法。是否使用jsHint或者jsLint努力使插件更加厚實了呢?插件是否被優(yōu)化過了呢?

編碼風(fēng)格

插件是否遵循了諸如jQuery 核心風(fēng)格指南這樣一種一致的風(fēng)格指南?如果不是,那么你的代碼至少是不是相對干凈,并且可讀的?

兼容性

各個版本的jQuery插件兼容怎么樣?通過對編譯jQuery-git源碼的版本或者最新的穩(wěn)定版的測試,如果在jQuery 1.6發(fā)布之前寫的插件,那么它可能存在有問題的屬性和特性,因為他們隨著新版的發(fā)布而改變。

新版本的jQuery為jQuery的項目的提高核心庫的使用提供了的改進(jìn)和環(huán)境,雖然偶然出現(xiàn)破損(主要的版本),但我們是朝著更好的方向做的,我看到插件作者在必要時更新自己的代碼,至少,測試他們的新版本的插件,以確保一切都如預(yù)期般運行。

可靠性

這個插件應(yīng)該有自己的一套單元測試。做這些不僅是為了證明它確實在按照預(yù)期運作,也可以改進(jìn)設(shè)計而

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號