我应该使用哪种jQuery插件设计模式?

时间:2011-08-19 21:25:00

标签: javascript jquery design-patterns jquery-plugins

我需要构建一个jQuery插件,它会为每个选择器id返回一个实例。该插件应该并且只会用于具有id的元素(不能使用与许多元素匹配的选择器),所以它应该像这样使用:

$('#element-id').myPlugin(options);
  • 我需要能够为插件提供一些私有方法以及一些公共方法。我可以实现这一点,但我的主要问题是每次调用$('#element-id')。myPlugin()时我都希望获得相同的实例。
  • 我希望有一些代码只应在第一次为给定ID(构造)初始化插件时执行。
  • 第一次提供options参数,对于构造,之后我不想执行构造,这样我就可以像$('#element-id'一样访问插件).myPlugin()
  • 该插件应该能够在同一页面上使用多个元素(通常最多2个)(但是每个元素都需要自己的配置 - 它们将通过ID初始化,而不是通用的类选择器例)。
  • 以上语法仅作为示例 - 我对如何实现该模式的任何建议持开放态度

我对其他语言有很多OOP经验,但对javascript知识有限,我真的很困惑如何正确。

修改

详细说明 - 这个插件是一个GoogleMaps v3 API包装器(帮助器)来帮助我摆脱代码重复,因为我在许多地方使用谷歌地图,通常使用标记。这是当前的库(删除了大量代码,只剩下最重要的方法):

;(function($) {
    /**
     * csGoogleMapsHelper set function.
     * @param options map settings for the google maps helper. Available options are as follows:
     * - mapTypeId: constant, http://code.google.com/apis/maps/documentation/javascript/reference.html#MapTypeId
     * - mapTypeControlPosition: constant, http://code.google.com/apis/maps/documentation/javascript/reference.html#ControlPosition
     * - mapTypeControlStyle: constant, http://code.google.com/apis/maps/documentation/javascript/reference.html#MapTypeControlStyle
     * - mapCenterLatitude: decimal, -180 to +180 latitude of the map initial center
     * - mapCenterLongitude: decimal, -90 to +90 latitude of the map initial center
     * - mapDefaultZoomLevel: integer, map zoom level
     * 
     * - clusterEnabled: bool
     * - clusterMaxZoom: integer, beyond this zoom level there will be no clustering
     */
    $.fn.csGoogleMapsHelper = function(options) {
        var id = $(this).attr('id');
        var settings = $.extend(true, $.fn.csGoogleMapsHelper.defaults, options);

        $.fn.csGoogleMapsHelper.settings[id] = settings;

        var mapOptions = {
            mapTypeId: settings.mapTypeId,
            center: new google.maps.LatLng(settings.mapCenterLatitude, settings.mapCenterLongitude),
            zoom: settings.mapDefaultZoomLevel,
            mapTypeControlOptions: {
                position: settings.mapTypeControlPosition,
                style: settings.mapTypeControlStyle
            }
        };

        $.fn.csGoogleMapsHelper.map[id] = new google.maps.Map(document.getElementById(id), mapOptions);
    };

    /**
     * 
     * 
     * @param options settings object for the marker, available settings:
     * 
     * - VenueID: int
     * - VenueLatitude: decimal
     * - VenueLongitude: decimal
     * - VenueMapIconImg: optional, url to icon img
     * - VenueMapIconWidth: int, icon img width in pixels
     * - VenueMapIconHeight: int, icon img height in pixels
     * 
     * - title: string, marker title
     * - draggable: bool
     * 
     */
    $.fn.csGoogleMapsHelper.createMarker = function(id, options, pushToMarkersArray) {
        var settings = $.fn.csGoogleMapsHelper.settings[id];

        markerOptions = {
                map:  $.fn.csGoogleMapsHelper.map[id],
                position: options.position || new google.maps.LatLng(options.VenueLatitude, options.VenueLongitude),
                title: options.title,
                VenueID: options.VenueID,
                draggable: options.draggable
        };

        if (options.VenueMapIconImg)
            markerOptions.icon = new google.maps.MarkerImage(options.VenueMapIconImg, new google.maps.Size(options.VenueMapIconWidth, options.VenueMapIconHeight));

        var marker = new google.maps.Marker(markerOptions);
        // lets have the VenueID as marker property
        if (!marker.VenueID)
            marker.VenueID = null;

        google.maps.event.addListener(marker, 'click', function() {
             $.fn.csGoogleMapsHelper.loadMarkerInfoWindowContent(id, this);
        });

        if (pushToMarkersArray) {
            // let's collect the markers as array in order to be loop them and set event handlers and other common stuff
             $.fn.csGoogleMapsHelper.markers.push(marker);
        }

        return marker;
    };

    // this loads the marker info window content with ajax
    $.fn.csGoogleMapsHelper.loadMarkerInfoWindowContent = function(id, marker) {
        var settings = $.fn.csGoogleMapsHelper.settings[id];
        var infoWindowContent = null;

        if (!marker.infoWindow) {
            $.ajax({
                async: false, 
                type: 'GET', 
                url: settings.mapMarkersInfoWindowAjaxUrl, 
                data: { 'VenueID': marker.VenueID },
                success: function(data) {
                    var infoWindowContent = data;
                    infoWindowOptions = { content: infoWindowContent };
                    marker.infoWindow = new google.maps.InfoWindow(infoWindowOptions);
                }
            });
        }

        // close the existing opened info window on the map (if such)
        if ($.fn.csGoogleMapsHelper.infoWindow)
            $.fn.csGoogleMapsHelper.infoWindow.close();

        if (marker.infoWindow) {
            $.fn.csGoogleMapsHelper.infoWindow = marker.infoWindow;
            marker.infoWindow.open(marker.map, marker);
        }
    };

    $.fn.csGoogleMapsHelper.finalize = function(id) {
        var settings = $.fn.csGoogleMapsHelper.settings[id];
        if (settings.clusterEnabled) {
            var clusterOptions = {
                cluster: true,
                maxZoom: settings.clusterMaxZoom
            };

            $.fn.csGoogleMapsHelper.showClustered(id, clusterOptions);

            var venue = $.fn.csGoogleMapsHelper.findMarkerByVenueId(settings.selectedVenueId);
            if (venue) {
                google.maps.event.trigger(venue, 'click');
            }
        }

        $.fn.csGoogleMapsHelper.setVenueEvents(id);
    };

    // set the common click event to all the venues
    $.fn.csGoogleMapsHelper.setVenueEvents = function(id) {
        for (var i in $.fn.csGoogleMapsHelper.markers) {
            google.maps.event.addListener($.fn.csGoogleMapsHelper.markers[i], 'click', function(event){
                $.fn.csGoogleMapsHelper.setVenueInput(id, this);
            });
        }
    };

    // show the clustering (grouping of markers)
    $.fn.csGoogleMapsHelper.showClustered = function(id, options) {
        // show clustered
        var clustered = new MarkerClusterer($.fn.csGoogleMapsHelper.map[id], $.fn.csGoogleMapsHelper.markers, options);
        return clustered;
    };

    $.fn.csGoogleMapsHelper.settings = {};
    $.fn.csGoogleMapsHelper.map = {};
    $.fn.csGoogleMapsHelper.infoWindow = null;
    $.fn.csGoogleMapsHelper.markers = [];
})(jQuery);

它的用法看起来像这样(实际上并不完全像这样,因为有一个PHP包装器可以通过一次调用自动化它,但基本上是这样):

$js = "$('#$id').csGoogleMapsHelper($jsOptions);\n";

if ($this->venues !== null) {
    foreach ($this->venues as $row) {
        $data = GoogleMapsHelper::getVenueMarkerOptionsJs($row);
        $js .= "$.fn.csGoogleMapsHelper.createMarker('$id', $data, true);\n";
    }
}

$js .= "$.fn.csGoogleMapsHelper.finalize('$id');\n";
echo $js;

上述实现的问题是我不喜欢为“设置”和“地图”保留哈希图

$id是初始化地图的DIV元素ID。它用作.map中的一个键,而.settings有一些映射,我在页面上为每个初始化的这样的GoogleMaps保存设置和GoogleMaps MapObject实例。 PHP代码中的$jsOptions$data是JSON对象。

现在我需要能够创建一个拥有自己的设置和GoogleMaps地图对象的GoogleMapsHelper实例,以便在我对某个元素(通过其ID)初始化之后,我可以重用该实例。但是如果我在页面上的N个元素上初始化它,它们中的每一个都应该有自己的配置,地图对象等。

我并不坚持认为这是作为jQuery插件实现的!我坚持认为它是灵活且可扩展的,因为我将在一个大型项目中使用它,目前有十几个不同的屏幕,将在几个月后使用,改变它的使用界面将是整个项目重构的噩梦。

我会为此添加一笔赏金。

7 个答案:

答案 0 :(得分:19)

当您通过$('#element').myPlugin()说“获取”实例时,我认为您的意思是:

var instance = $('#element').myPlugin();
instance.myMethod();

这一开始似乎是一个好主意,但是因为你破坏了jQuery实例链,所以它被认为是扩展jQuery原型的不良做法。

另一种方便的方法是将实例保存在$.data对象中,因此您只需初始化插件一次,然后您可以随时使用DOM元素作为参考来获取实例,f .EX:

$('#element').myPlugin();
$('#element').data('myplugin').myMethod();

这是我用来在JavaScript和jQuery中维护类似类结构的模式(包含注释,希望你能遵循):

(function($) {

    // the constructor
    var MyClass = function( node, options ) {

        // node is the target
        this.node = node;

        // options is the options passed from jQuery
        this.options = $.extend({

            // default options here
            id: 0

        }, options);

    };

    // A singleton for private stuff
    var Private = {

        increaseId: function( val ) {

            // private method, no access to instance
            // use a bridge or bring it as an argument
            this.options.id += val;
        }
    };

    // public methods
    MyClass.prototype = {

        // bring back constructor
        constructor: MyClass,

        // not necessary, just my preference.
        // a simple bridge to the Private singleton
        Private: function( /* fn, arguments */ ) {

            var args = Array.prototype.slice.call( arguments ),
                fn = args.shift();

            if ( typeof Private[ fn ] == 'function' ) {
                Private[ fn ].apply( this, args );
            }
        },

        // public method, access to instance via this
        increaseId: function( val ) {

            alert( this.options.id );

            // call a private method via the bridge
            this.Private( 'increaseId', val );

            alert( this.options.id );

            // return the instance for class chaining
            return this;

        },

        // another public method that adds a class to the node
        applyIdAsClass: function() {

            this.node.className = 'id' + this.options.id;

            return this;

        }
    };


    // the jQuery prototype
    $.fn.myClass = function( options ) {

        // loop though elements and return the jQuery instance
        return this.each( function() {

            // initialize and insert instance into $.data
            $(this).data('myclass', new MyClass( this, options ) );
        });
    };

}( jQuery ));

现在,你可以这样做:

$('div').myClass();

这将为找到的每个div添加一个新实例,并将其保存在$ .data中。现在,为了检索某个实例的apply方法,你可以这样做:

$('div').eq(1).data('myclass').increaseId(3).applyIdAsClass();

这是我多次使用的模式,非常适合我的需求。

您还可以通过添加window.MyClass = MyClass来公开该类,以便在不使用jQuery原型的情况下使用它。这允许使用以下语法:

var instance = new MyClass( document.getElementById('element'), {
    id: 5
});
instance.increaseId(5);
alert( instance.options.id ); // yields 10

答案 1 :(得分:4)

这是一个想法......

(function($){
    var _private = {
        init: function(element, args){
           if(!element.isInitialized) {
               ... initialization code ...
               element.isInitialized = true;
           }
        }
    }

    $.fn.myPlugin(args){
        _private.init(this, args);
    }
})(jQuery);

...然后您可以添加更多私有方法。如果要“保存”更多数据,可以使用传递给init函数的元素并将对象保存到dom元素......如果您使用的是HTML5,则可以在元素上使用数据属性。

修改

另一件事情浮现在脑海中。您可以使用jQuery.UI小部件。

答案 2 :(得分:4)

我认为解决问题所需的基本上是一个很好的OO结构来保存你的设置和GoogleMap。

如果你不依赖于jQuery并且非常了解OOP,我会使用YUI3 Widget

只需看一眼Sample Widget Template就可以了解框架提供对OOP结构的访问,例如:

  1. 它提供了命名空间支持。
  2. 它支持类和对象的概念
  3. 它整齐地支持课程扩展
  4. 它提供构造函数和析构函数
  5. 它支持实例变量的概念
  6. 它提供渲染和事件绑定
  7. 在你的情况下:

    1. 您可以创建自己的GoogleHelper类,其中包含自己的实例变量以及我认为您想要的Google Map对象。
    2. 然后,您将开始使用自己的设置创建此类的实例。
    3. 对于每个新实例,您只需要使用稍后可以引用的ID映射它。通过将ID引用到同时具有设置和GoogleMap的GoogleHelper实例,您不必保留两个地图(一个用于保存设置,另一个用于GoogleMap),我恰好认同它不是理想的情况。
    4. 这基本上可以追溯到基本的OO编程,正确的JS框架可以帮助您实现这一目标。虽然也可以使用其他OO JS框架,但我发现YUI3为大型Javascript项目提供了比其他框架更好的结构。

答案 3 :(得分:3)

我将提供我最近发表的关于类似内容的博客文章的链接。 http://aknosis.com/2011/05/11/jquery-pluginifier-jquery-plugin-instantiator-boilerplate/

基本上这个包装器(我称之为pluginifier)将允许您创建一个单独的JavaScript对象,它将容纳所有内容(公共/私有方法/选项对象等),但允许快速检索和创建公共$(' #myThing')为myplugin();

源代码也可以在github上找到:https://github.com/aknosis/jquery-pluginifier

以下是您可以放置​​代码的代码段:

//This should be available somewhere, doesn't have to be here explicitly
var namespace = {

    //This will hold all of the plugins
    plugins : {}
};

//Wrap in a closure to secure $ for jQuery
(function( $ ){

    //Constructor - This is what is called when we create call new namspace.plugins.pluginNameHere( this , options );
    namespace.plugins.pluginNameHere = function( ele , options ){
        this.$this = $( ele );
        this.options = $.extend( {} , this.defaults , options );
    };

    //These prototype items get assigned to every instance of namespace.plugins.pluginNameHere
    namespace.plugins.pluginNameHere.prototype = {

        //This is the default option all instances get, can be overridden by incoming options argument
        defaults : { 
            opt: "tion"
        },

        //private init method - This is called immediately after the constructor 
        _init : function(){
            //useful code here
            return this; //This is very important if you want to call into your plugin after the initial setup
        },

        //private method - We filter out method names that start with an underscore this won't work outside
        _aPrivateMethod : function(){ 
            //Something useful here that is not needed externally
        },

        //public method - This method is available via $("#element").pluginNameHere("aPublicMethod","aParameter");
        aPublicMethod : function(){
            //Something useful here that anyone can call anytime
        }
    };

    //Here we register the plugin - $("#ele").pluginNameHere(); now works as expected
    $.pluginifier( "pluginNameHere" );

})( jQuery );

$ .pluginifier代码位于单独的文件中,但也可以包含在与插件代码相同的文件中。

答案 4 :(得分:2)

您的许多要求都是不必要的。无论如何,这里是我自己采用的设计模式的大致轮廓 - 这基本上直接来自jQuery创作文档。如果您有任何疑问,请给我留言。

所描述的模式允许以下用途:

var $myElements = $('#myID').myMapPlugin({
    center:{
        lat:174.0,
        lng:-36.0
    }
});

$myElements.myMapPlugin('refresh');

$myElements.myMapPlugin('addMarker', {
    lat:174.1,
    lng:-36.1
});

$myElements.myMapPlugin('update', {
    center:{
        lat:175.0,
        lng:-33.0
    }
});

$myElements.myMapPlugin('destroy');

这是一般模式 - 只实施了一些方法。

;(function($) {
    var privateFunction = function () {
        //do something
    }

    var methods = {
        init : function( options ) {

            var defaults = {
                center: {
                    lat: -36.8442,
                    lng: 174.7676
                }
             };
             var t = $.extend(true, defaults, options);

             return this.each(function () {
                 var $this = $(this),
                 data = $this.data('myMapPlugin');

                 if ( !data ) {

                     var map = new google.maps.Map(this, {
                         zoom: 8,
                         center: new google.maps.LatLng(t['center'][lat], t['center']['lng']),
                         mapTypeId: google.maps.MapTypeId.ROADMAP,
                         mapTypeControlOptions:{
                             mapTypeIds: [google.maps.MapTypeId.ROADMAP]
                         }
                     });

                     var geocoder  = new google.maps.Geocoder();

                     var $form = $('form', $this.parent());
                     var form = $form.get(0);
                     var $search = $('input[data-type=search]', $form);

                     $form.submit(function () {
                         $this.myMapPlugin('search', $search.val());
                         return false;
                     });

                     google.maps.event.addListener(map, 'idle', function () {
                         // do something
                     });

                     $this.data('myMapPlugin', {
                         'target': $this,
                         'map': map,
                         'form':form,
                         'geocoder':geocoder
                     });
                 }
             });
         },
         resize : function ( ) {
             return this.each(function(){
                 var $this = $(this),
                     data = $this.data('myMapPlugin');

                 google.maps.event.trigger(data.map, 'resize');
             });
         },
         search : function ( searchString ) {
             return this.each(function () {
             // do something with geocoder              
             });
         },
         update : function ( content ) {
             // ToDo
         },
         destroy : function ( ) {
             return this.each(function(){

                 var $this = $(this),
                 data = $this.data('myMapPlugin');

                 $(window).unbind('.locationmap');
                 data.locationmap.remove();
                 $this.removeData('locationmap');
             });
        }
    };


    $.fn.myMapPlugin = function (method) {
        if ( methods[method] ) {
            return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
        } else if ( typeof method === 'object' || ! method ) {
            return methods.init.apply( this, arguments );
        } else {
            $.error( 'Method ' +  method + ' does not exist on jQuery.myMapPlugin' );
        }
   };
})(jQuery);

请注意,代码未经测试。

快乐编码:)

答案 5 :(得分:1)

这可能超出了你的问题的范围,但我真的认为你应该重构你如何处理PHP - > JS转换(具体来说,就是你最后的整个PHP代码块)。

我认为在PHP中生成大量JS是一种反模式,然后在客户端上运行。相反,您应该将JSON数据返回给您的客户端,该客户端根据该数据调用所需的任何内容。

这个例子不完整,但我认为它会给你一个想法。你的所有JS实际上都应该在JS中,并且唯一被发送回来的东西应该是JSON。生成动态JS并不是理所当然的IMO。

<?php
// static example; in real use, this would be built dynamically
$data = array(
    $id => array(
        'options' => array(),
        'venues' => array(/* 0..N venues here */),
    )
);

echo json_encode($data);
?>

<script>
xhr.success = function (data) {
    for (var id in data)
    {
        $('#' + id).csGoogleMapsHelper(data[id].options);
        for (var i = 0, len = data[id].venues.length; i < len; i++)
        {
            $.fn.csGoogleMapsHelper.createMarker(id, data[id].venues[i], true);
        }
        $.fn.csGoogleMapsHelper.finalize(id);
    }
}
</script>

答案 6 :(得分:0)

我在jQuery plugin template - best practice, convention, performance and memory impact

处理了这些问题

我在jsfiddle.net发布的部分内容:

;(function($, window, document, undefined){
   var myPluginFactory = function(elem, options){
   ........
   var modelState = {
      options: null //collects data from user + default
   };
   ........
   function modeler(elem){
      modelState.options.a = new $$.A(elem.href);
      modelState.options.b = $$.B.getInstance();
   };
   ........
   return {
         pluginName: 'myPlugin',
         init: function(elem, options) {
            init(elem, options);
         },
         get_a: function(){return modelState.options.a.href;},
         get_b: function(){return modelState.options.b.toString();}
      };
   };
   //extend jquery
   $.fn.myPlugin = function(options) {
      return this.each(function() {
         var plugin = myPluginFactory(this, options);
         $(this).data(plugin.pluginName, plugin);
      });
   };
}(jQuery, window, document));

我的项目:https://github.com/centurianii/jsplugin

请参阅:http://jsfiddle.net/centurianii/s4J2H/1/