【问题标题】:Which jQuery plugin design pattern should I use?我应该使用哪种 jQuery 插件设计模式?
【发布时间】:2011-10-30 22:52:48
【问题描述】:

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

$('#element-id').myPlugin(options);
  • 我需要能够为插件提供一些私有方法以及一些公共方法。我可以做到这一点,但我的主要问题是每次调用 $('#element-id').myPlugin() 时我都想获得相同的实例。
  • 而且我希望有一些代码应该只在第一次为给定 ID(构造)初始化插件时执行。
  • options 参数应该第一次提供,对于构造,之后我不希望构造被执行,这样我就可以像 $('#element-id') 一样访问插件。我的插件()
  • 插件应该能够在同一页面上处理多个元素(通常最多 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 对象。

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

我不坚持这是作为一个 jQuery 插件实现的!我坚持认为它是灵活且可扩展的,因为我将在一个大型项目中使用它,其中包含十多个当前计划的不同屏幕几个月后就会被使用,改变它的使用界面将是对整个项目进行重构的一场噩梦。

我会为此添加赏金。

【问题讨论】:

  • 坦率地说,在不了解这个插件的情况下,作为一个有经验的 jQuery 用户,这整个“不可能使用匹配许多元素的选择器”的事情将是一个严重的问题。如果你正在制作一个 jQuery 插件,它应该像其他 jQuery 插件一样工作,除非它真的在做一些真正独特的事情。
  • 您的问题中有很多 wants,但只有一行代码(从调用者的角度来看,这无济于事)。你能详细说明你尝试了什么吗?如果您发布更多代码,您的问题可能会有答案。也就是说,如果我理解正确,您需要一个每元素单例,它是通过只接受这个单个元素的方法创建的。这与我所知道的 jQuery 和插件引擎的根本概念相反。
  • 很抱歉给您带来了困惑。它实际上是 GoogleMaps JS API 的助手和包装器。它确实是独一无二的,因为它旨在通过我在许多地方使用带有标记的谷歌地图的项目来节省我的代码重复。它不打算在项目之外使用。由于我太累了,我将发布一些我为此目的构建的当前实现的助手的代码,我觉得它的代码不是正确的方法。
  • 我用更多细节编辑了我的问题,但如果有任何不清楚的地方,请告诉我如何改进我的问题。
  • 谷歌地图 v3 不需要 需要 ID,DOM 元素就可以了,因此您可以在 jQuery 中使用任何选择器。

标签: javascript jquery design-patterns jquery-plugins


【解决方案1】:

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

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

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

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

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

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

(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 中。现在,要检索某个实例的应用方法,您可以这样做:

$('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

【讨论】:

  • 最后我得到了与此非常相似的东西(非常感谢@darhazer!他更详细地向我解释了你的想法并给了我关于如何为我的案例实施的直接说明),所以你应该得到赏金:)
  • 我不是私人桥的粉丝。有没有另一种方法可以做到这一点而不必将函数名作为字符串传递?
  • @Ryan 您可以在同一范围内定义一个本地函数,并将当前实例作为函数范围调用它,f.ex Private.increaseId.call(this, val);。但是如果你有很多私有函数,它可能会在代码方面变得混乱。
  • 如何使用这种模式从私有方法调用公共方法?
  • @Ryan 试试MyClass.prototype.increaseId.call(this, 5);
【解决方案2】:

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

(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,则可以在元素上使用 data- 属性。

编辑

我想到了另一件事。您可以使用 jQuery.UI 小部件。

【讨论】:

    【解决方案3】:

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

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

    看一眼Sample Widget Template 应该会让您知道该框架提供了对 OOP 结构的访问,例如:

    1. 它提供命名空间支持。
    2. 它支持类和对象的概念
    3. 巧妙地支持类扩展
    4. 提供构造函数和析构函数
    5. 支持实例变量的概念
    6. 它提供渲染和事件绑定

    在你的情况下:

    1. 您可以创建具有自己的实例变量的 GoogleHelper 类以及我认为您想要的 Google Map 对象。
    2. 然后您将开始使用自己的设置创建此类的实例。
    3. 对于每个新实例,您只需将其映射到一个您可以稍后引用它的 ID。通过将 ID 引用到同时具有设置和 GoogleMap 的 GoogleHelper 实例,您不必保留两张地图(一张用于保存设置,一张用于 GoogleMap),我碰巧同意您的观点,这不是理想的情况。

    这基本上可以追溯到基本的 OO 编程,而正确的 JS 框架可以使您能够做到这一点。虽然也可以使用其他 OO JS 框架,但我发现 YUI3 为大型 Javascript 项目提供了比其他框架更好的结构。

    【讨论】:

      【解决方案4】:

      我将提供一个链接,指向我最近写的一篇关于类似事情的博文。 http://aknosis.com/2011/05/11/jquery-pluginifier-jquery-plugin-instantiator-boilerplate/

      基本上,这个包装器(我称之为插件程序)将允许您创建一个单独的 JavaScript 对象,该对象将容纳所有内容(公共/私有方法/选项对象等),但允许使用常见的 $(' 进行快速检索和创建#myThing').myPlugin();

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

      这是一个 sn-p,您可以在其中放置代码:

      //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 代码位于单独的文件中,但也可以包含在与您的插件代码相同的文件中。

      【讨论】:

        【解决方案5】:

        您的许多要求都是不必要的。无论如何,这里是我为自己采用的设计模式的粗略轮廓 - 它基本上直接来自 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);
        

        请注意,代码未经测试。

        快乐编码:)

        【讨论】:

        • 不想挑剔但是...distroy?或destroy?
        • @James Khoury 是的,写得又晚又快 :)
        【解决方案6】:

        这可能超出了你的问题范围,但我真的认为你应该重构你如何处理 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>
        

        【讨论】:

        • 我的 JS lib 使用的 PHP 包装器是一个可重用和可配置的小部件。在视图文件中,我将配置传递给小部件,它会输出带有 JS 的 JSON 配置。它类似于 widget('GoogleMapsHelperWidget', array(/*config*/)); ?> 这样我就不必在每个视图中都写“$.fn.csGoogleMapsHelper.createMarker, .finelize, etc”
        【解决方案7】:

        我在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/

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-11-29
          • 1970-01-01
          相关资源
          最近更新 更多