离线下载
PDF版 ePub版

史涛 · 更新于 2018-11-28 11:00:43

jQuery 插件的设计模式

jQuery插件开发在过去几年里进步了很多. 我们写插件的方式不再仅仅只有一种,相反有很多种。现实中,某些插件设计模式在解决某些特殊的问题或者开发组件的时候比其他模式更有效。

有些开发者可能希望使用 jQuery UI 部件工厂; 它对于创建复杂而又灵活的UI组件是很强大的。有些开发者可能不想使用。

有些开发者可能想把它们的插件设计得更像模块(与模块模式相似)或者使用一种更现代化的模块格式。 有些开发者想让他们的插件利用原型继承的特点。有些则希望使用自定义事件或者通过发布/订阅信息来实现插件和他们的其它App之间的通信。

在经过了想创建一刀切的jquery插件样板的数次尝试之后,我开始考虑插件模式。这样的样板在理论上是一个很好的主意,但现实是,我们很少使用固定的方式并且总是使用一种模式来写插件。

让我们假设我们已经为了某个目标去着手尝试编写我们自己的jQuery插件,并且我们可以放心的把一些东西放在一起运作。它是起作用的。它做了它需要去做的,但是也许我们觉得它可以被构造得更好。也许它应该更加灵活或者被设计用来解决更多开发者普遍都碰到过的问题才对。如果这听起来很熟悉,那么你也许会发现这一章是很有用的。在其中,我们将探讨大量的jQuery插件模式,它们在其它开发者的环境中都工作的不错。

注意:尽管开头我们将简要回顾一些jQuery插件的基础知识,但这一章是针对中级到高级的开发者的。 如果你觉得对此还没有做足够的准备,我很高兴的建议你去看一看jQuery官方的插件/创作(Plugins/Authoring )指导,Ben Alman的插件类型指导(plugin style guide)和RemySharp的“写得不好的jQuery插件的症候(Signs of a Poorly Written jQuery Plugin)”,作为开始这一节之前的阅读材料。

模式

jQuery插件有一些具体的规则,它们是整个社区能够实现这令人难以置信的多样性的原因之一。在最基本的层面上,我们能够编写一个简单地向jQuery的jQuery.fn对象添加一个新的功能属性的插件,像下面这样:

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

对于紧凑性而言这是很棒的,而下面的代码将会是一个更好的构建基础:

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

在这里,我们将我们的插件逻辑封装到一个匿名函数中。为了确保我们使用的$标记作为简写形式不会造成任何jQuery和其它Javascript库之间的冲突,我们简单的将其传入这个闭包中,它会将其映射到美元符号上。这就确保了它能够不被任何范围之外的执行影响到。

编写这种模式的一个可选方式是使用jQuery.extend(),它使得我们能够一次定义多个函数,并且有时能够获得更多的语义上的意义。

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

现在我们已经回顾了一些jQuery插件的基础,但是许多更多的工作可借以更进一步。A Lightweight Start是我们将要探讨的该 设计模式的第一个完整的插件,它涵盖了我们可以在每天的基础的插件开发工作中用到的一些最佳实践, 细数了一些值得推广应用的常见问题描述。

注意: 尽管下面大多数的模式都会得到解释,我还是建议大家通过阅读代码里的注释来研究它们,因为这些注释能够提供关于为什么一个具体的最佳实践会被应用这个问题的更深入的理解。 我也应该提醒下,没有前面的工作往后这些没有一样是可能的,它们是来自于jQuery社区的其他成员的输入和建议。我已经将它们列到每一种模式中了,以便诸位可以根据各自的工作方向来阅读相关的内容,如果感兴趣的话。

A Lightweight Start 模式

让我们用一些遵循了(包括那些在jQuery 插件创作指导中的)最佳实践的基础的东西来开始我们针对插件模式的深入探讨。这一模式对于插件开发的新手和只想要实现一些简单的东西(例如工具插件)的人来说是理想的。A Lightweight Start 使用到了下面这些东西:

  • 诸如分号放置在函数调用之前这样一些通用的最佳实践(我们将在下面的注释中解释为什么要这样做)
  • window,document,undefined作为参数传入。
  • 基本的默认对象。
  • 一个简单的针对跟初始化创建和要一起运作的元素的赋值相关的逻辑的插件构造器。
  • 扩展默认的选项。
  • 围绕构造器的轻量级的封装,它有助于避免诸如实例化多次的问题。
  • 坚持最大限度可读性的jQuery核心风格的指导方针。
/*!
 * 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插件创作指南是对插件开发的一个很棒的介绍,但它并不能帮助掩盖我们不得不定期处理的常见的插件管道任务。

jQuery UI Widget工厂是这个问题的一种解决方案,能帮助我们基于面向对象原则构建复杂的,具有状态性的插件。它也简化了我们插件实体的通信,也淡化了许多我们在一些基础的插件上工作时必须去编写代码的重复性的工作。

具有状态性的插件帮助我们对它们的当前状态保持跟进,也允许我们在插件被初始化之后改变其属性。 有关Widget工厂最棒的事情之一是大部分的jQuery UI库的实际上都是使用它作为其组件的基础。这意味着如果我们是在寻找超越这一模式的架构的进一步指导,我们将没必要去超越GitHub上的jQuery UI进行思考。

jQuery UI Widget 工厂模式涵盖了包括事件触发在内几乎所有的默认支持的工厂方法。每一个模式的最后都包含了所有这些方法的使用注释,还在内嵌的注释中给出了更深入的指导。

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

嵌套的命名空间插件模式

如我们在本书的前面所述,为我们的代码加入命名空间是避免与其它的全局命名空间中的对象和变量产生冲突的一种方法。它们是很重要的,因为我们想要保护我们的插件的运作不会突然被页面上另外一段使用了同名变量或者插件的脚本所打断。作为全局命名空间的好市民,我们也必须尽我们所能来阻止其他开发者的脚本由于同样的问题而执行起来发生问题。

Javascript并不像其它语言那样真的内置有对命名空间的支持,但它却有可以被用来达到同样效果的对象。雇佣一个顶级对象作为我们命名空间的名称,我们就可以使用相同的名字检查页面上另外一个对象的存在性。如果这样的对象不存在,那么我们就定义它;如果它存在,就简单的用我们的插件对其进行扩展。

对象(或者更确切的说,对象常量)可以被用来创建内嵌的命名空间,namespace.subnamespace.pluginName,诸如此类。而为了保持简单,下面的命名空间样板会向我们展示有关这些概念的入门我们所需要的一切。

/*!
 * 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设计模式一节,我们讨论了观察者模式,而后继续论述到了jQuery对于自定义事件的支持,其为实现发布/订阅提供了一种类似的解决方案。

这里的基本观点是当我们的应用程序中发生了某些有趣的事情时,页面中的对象能够发布事件通知。其他对象就会订阅(或者侦听)这些事件,并且据此产生回应。我们应用程序的这一逻辑所产生的效果是更加显著的解耦,每一个对象不再需要直接同另外一个对象进行通信。

在接下来的jQuery UI widget工厂模式中,我们将实现一个基本的基于自定义事件的发布/订阅系统,它允许我们的插件向应用程序的其余部分发布事件通知,而这些部分将对此产生回应。

/*!
 * 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中,我们并不需要那些在其它经典的编程语言中找到的类的传统观念,但我们确实需要原型继承。有了原型继承,对象就可以从其它对象继承而来了。我们可以将此概念应用到jQuery的插件开发中。

Yepnope.js作者Alex Sexton和jQuery团队成员Scott Gonzalez已经瞩目于这个主题的细节。总之,他们发现为了组织模块化的开发,使定义插件逻辑的对象同插件生成过程本身分离是有好处的。

这一好处就是对我们插件代码的测试会变得显著的简单起来,并且我们也能够在不改变任何我们所实现的对象API的方式,这一前提下,适应事物在幕后运作的方式。

在Sexton关于这个主题的文章中,他实现了一个使我们能够将我们的一般的逻辑附加到特定插件的桥,我们已经在下面的模式中将它实现。

这一模式的另外一个优点是我们不需要去不断的重复同样的插件初始化代码,这确保了DRY开发背后的观念得以维持。一些开发者或许也会发现这一模式的代码相比其它更加易读。

/*!
 * 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 工厂桥接模式

如果你喜欢基于过去的设计模式的对象的生成插件这个主意,那么你也许会对这个在jQuery UI Widget工厂中发现的叫做$.widget.bridge的方法感兴趣。

这座桥基本上是在充当使用$.widget创建的Javascript对象和jQuery核心API之间的中间层,它提供了一种实现基于对象的插件定义的更加内置的解决方案。实际上,我们能够使用自定义的构造器去创建具有状态性的插件。

此外,$.widget.bridge还提供了对许多其它功能的访问,包括下面这些:

  • 公共的和私有的方法都如人们在经典的OOP中所希望的方式被处理(例如,公共的方法被暴露出来,而对私有方法的调用则是不可能的)。
  • 防止多次初始化的自动保护。
  • 传入对象实体的自动生成,而对它们的存储则在内置的$.datacache范围之内。
  • 选项可以在初始化后修改。

有关使用这一模式的更多信息,请看看下面内嵌的注释:

/*!
 * 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 项目框架,为设计同时能运行在主流移动设备和平台以及桌面平台的大多数常见 Web 应用带来便利。我们可以仅编写一次代码,而无需为每种设备或操作系统编写特定的应用,就能使其同时运行在 A、B 和 C 级浏览器。

JQuery mobile 背后的基本原理也可应用于插件和小部件的开发。

接下来介绍的模式令人感兴趣的是,已熟悉使用 jQuery UI Widget Factory 模式的开发者能够很快地编写针对移动设备优化的小部件,即便这会在不同设备中存在细微的差异。

下面为移动优化的widget同前面我们看到的标准UI widget模式相比,有许多有趣的不同之处。

$.mobile.widget 是继承于现有的widget原型的引用。对于标准的widget, 通过任何这样的原型进行基础的开发都是没有必要的,但是使用这种为移动应用定制的jQuery widget 原型,它提供了更多的“选项”格式供内部访问。

在_create()中,教程提供了关于官方的jQuery 移动 widget如何处理元素选择,对于基于角色的能够更好的适应jQM标记的方法的选择。这并不是说标准的选择不被推荐,只是说这种方法也许可以给予jQuery 移动页面的架构更多的意义。

也有以注释形式提供的关于将我们的插件方法应用于页面创建,还有通过数据角色和数据属性选择插件应用程序,这些内容的指导。

/*!
 * (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中的一个新页面被创建了,我们也都可以自己初始化这个widget。但一个(通过data-role="page"属性发现的)jQuery Mobile 页面一开始被初始化时, jQuery Mobile的页面插件会自己派发一个创建事件。我们能侦听那个(称作 “pagecreate”的)事件,并且在任何时候只要新的页面一被创建,就自动的让我们的插件运行。

$(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 );
});

现在我们可以在一个页面中简单的引用包含了我们的widget和pagecreate绑定的脚本,而它将像任何其它的jQuery Mobile插件一样自动的运行。

RequireJS 和 jQuery UI Widget 工厂

如我们在当代模块化设计模式一节所述,RequireJS是一种兼容AMD的脚本装载器,它提供了将应用程序逻辑封装到可管理的模块中,这样一个干净的解决方案。

它能够(通过它的顺序插件)将模块按照正确的顺序加载,简化了借助它优秀的r.js优化器整合脚本的过程,并且提供了在每一个模块的基础上定义动态依赖的方法。

在下面的样板模式中,我们展示了一种兼容AMD的jQuery UI widget(这里是RequireJS)如何能够被定义成做到下面这些事情:

  • 允许widget模块依赖的定义,构建在前面早先的jQuery UI Widget 工厂模式之上。
  • 展示一种为创建(使用Underscore.js 微模板)模板化的widget传入HTML模板集的方法。
  • 包括一种如果我们希望晚一点将其传入到RequireJS优化器,以使我们能够对我们的widget模块做出调整的快速提示。
/*!
 * 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");

    });
});

全局和每次调用的重载选项(最佳调用模式)

对于我们的下一个模式,我们将来看看一种为插件选择默认和手动配置选项的优化了的方法。 定义插件选项,我们大多数人可能熟悉的一种方法是,通过默认的字面上的对象将其传递到$.extend(),如我们在我们基础的插件样板中所展示的。

然而,如果我们正工作在一种带有许多的定制选项,对于这些定制选项我们希望用户在全局和每一次调用的级别都能重载,那样我们就能以更加优化一点的方式构造事物。

相反,通过明确的引用定义在插件命名空间中的一个选项对象(例如,$fn.pluginName.options),还有将此同任何在其最初被调用时传递到插件的选项混合,用户就要对在插件初始化期间传递选项,或者在插件外部重载选项,这两者有所选择(如这里所展示的)。

/*!
 * 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插件自身之中.取而代之我们使用了一个构造器和一种定义在它的原型之上的对象字面值,来定义我们的插件逻辑.jQuery随后被用在插件对象的实际实例中。

通过玩了两个小花样,定制被带到了一个新的层次,其中之一就是我们在前面已经看到的模式:

  • 选项不论是全局的还是集合中每一个元素的,都可以被重载。
  • 选在可以通过HTML5数据属性(在下面会有展示)在每一个元素的级别被定制.这有利于可以被应用到集合中元素的插件行为,但是会导致在不需要使用一个不同的默认值实例化每一个元素的前提下定制的内联。

在不怎么正规的场合我们不会经常见到这种非常规的选项,但是它能够成为一种重要的清晰方案(只要我们不介意这种内联的方式).如果不知道这个东西在那儿会起作用,那就想象着要为大型的元素集合编写一个可拖动的插件,这种场景.我们可以像下面这样定制它们的选项:

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

但是使用我们模式的内联方式,下面这样是可能的。

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

是什么造就了模式之外的一个优秀插件?

在今天结束之际,设计模式仅仅只是编写可维护的jQuery插件的一个方面。还有大量其它的因素值得考虑,而我也希望分享下对于用第三方插件来解决一些其它的问题,我自己的选择标准。

质量

对于你所写的Javascript和jQuery插件,请坚持遵循最佳实践的做法。是否使用jsHint或者jsLint努力使插件更加厚实了呢?插件是否被优化过了呢?

编码风格

插件是否遵循了诸如jQuery 核心风格指南这样一种一致的风格指南?如果不是,那么你的代码至少是不是相对干净,并且可读的?

兼容性

各个版本的jQuery插件兼容怎么样?通过对编译jQuery-git源码的版本或者最新的稳定版的测试,如果在jQuery 1.6发布之前写的插件,那么它可能存在有问题的属性和特性,因为他们随着新版的发布而改变。

新版本的jQuery为jQuery的项目的提高核心库的使用提供了的改进和环境,虽然偶然出现破损(主要的版本),但我们是朝着更好的方向做的,我看到插件作者在必要时更新自己的代码,至少,测试他们的新版本的插件,以确保一切都如预期般运行。

可靠性

这个插件应该有自己的一套单元测试。做这些不仅是为了证明它确实在按照预期运作,也可以改进设计而无需影响最终用户。我认为单元测试对任何重要的jQuery插件都是必要的,它们对生产环境意义重大,而且它们也不是那么难写。要想获得一个用QUnit实现自动化JavaScript测试的完美指南,你也许会对Jörn Zaefferer的“使用QUnit自动化JavaScript 测试”感兴趣。

性能

如果这个插件需要执行的任务包含有大量的处理语句,或者对DOM的大量处理,那就应该按照基准管理的最佳实践将这个任务最小化。用jsPerf.com 来测试代码段,以实现 a) 在不同的浏览器执行是否良好 以及 b)如果存在的话,找到可以进一步优化的地方。

文档

如果目的是让其他开发者使用插件,那就要保证它具有良好的文档。给API建立文档描述这些插件是如何使用的。这个插件支持什么方法和选项?它有一些用户需要注意的性能和可伸缩性问题吗?如果用户无法理解怎样使用这个插件,他们很可能会找寻一个替代者。评论你的插件代码也是很有益处的。目前这是你能提供给其他开发者的最好的礼物了。如果有人觉得他们可以很好的使用或者改进你的基础代码,那么你就是做了一件漂亮的工作。

维护的可能性

发布插件的时候,估计可能需要多少时间进行维护和支持。我们都喜欢与社区分享我们的插件,但是一个人需要对他回答问题,解决问题和持续改进的能力设置预期。只要前期简单的在README文件中,为维护支持说明项目的意图,就可以做到这一点。

结论

在本章里,我们探索了几个节省时间的设计模式,以及一些可以用来改进jQuery插件写法的最佳实践。有些更适宜于特定的使用案例,但我希望总体而言这些模式是有用的。

请记住,当选择一个模式的时候,重要的是联系实际。不要仅仅因为某个插件模式的好处而使用它,而要投入时间理解底层的结构,并搞清楚它能否很好的解决你的问题,或者适应你尝试创建的组件。

上一篇: 建造者模式 下一篇: 命名空间模式