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

22
我需要编写一个jQuery插件,它将根据选择器ID返回单个实例。该插件应仅用于具有ID的元素(不可能使用匹配多个元素的选择器),因此应像这样使用:
$('#element-id').myPlugin(options);
  • 我需要为插件编写一些私有方法和公共方法。虽然我可以实现这一点,但我的主要问题是每次调用$('#element-id').myPlugin()时都想获得完全相同的实例。
  • 我希望有些代码仅在给定ID(构造函数)初始化插件的第一次执行时才被执行。
  • options参数应该在第一次为构造函数提供,之后我不希望再执行构造函数,这样我就可以像使用$('#element-id').myPlugin()一样访问插件了。
  • 插件应该能够在同一页上与多个元素(通常最多为2个)一起使用(但每个元素都需要自己的配置,再次强调-它们将通过ID进行初始化,而不是通过共同的类选择器)。
  • 上述语法只是示例-我对如何实现该模式的任何建议都持开放态度。

我在其他语言方面具有相当的OOP经验,但对javascript的知识知之甚少,我真的很困惑如何正确地做到这一点。

编辑

详细说明-此插件是GoogleMaps v3 API包装器(助手),可帮助我消除重复代码,因为我通常在许多地方使用google地图,通常带有标记。 这是当前的库(删除了大量代码,只留下最重要的方法):

;(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 的 MapObject 实例的键。PHP 代码中的 $jsOptions 和 $data 是 JSON 对象。
现在,我需要能够创建一个 GoogleMapsHelper 实例,它拥有自己的设置和 GoogleMaps 地图对象,以便在特定元素上(通过其 ID)初始化后,可以重复使用该实例。但是,如果在页面上初始化了 N 个元素,每个元素都应该有自己的配置、地图对象等。
我并不坚持将其实现为 jQuery 插件!我坚持它是灵活和可扩展的,因为我将在一个包括十几个不同屏幕的大型项目中使用它,在未来几个月里,更改它的使用界面将是整个项目的一场噩梦般的重构。
我会为此添加赏金。

4
作为一名有经验的jQuery用户,如果没有更多关于这个插件的了解,就光凭“无法使用匹配多个元素的选择器”这一点,我觉得这会是一个严重的问题。如果你要开发一个jQuery插件,它应该像其他jQuery插件一样工作,除非它确实在做一些独特的事情。 - Pointy
1
你在问题中提出了许多“想要”的东西,但只有一行代码(来自调用方,这并没有帮助)。你能详细说明一下你尝试过什么吗?如果您发布更多的代码,可能会有答案回答您的问题。话虽如此,如果我理解正确,您想要一个每个元素的单例,该单例是通过仅接受此单个元素的方法创建的。这与jQuery和我所知道的插件引擎的根本概念相反。 - Frédéric Hamidi
抱歉造成困惑。它实际上是GoogleMaps JS API的帮助程序和包装器。它真正独特的地方在于,它旨在通过我在项目中使用Google Maps并在许多地方使用标记来节省我的代码重复。它不打算在项目之外使用。由于我太累了,我将发布一些当前实现的帮助程序代码,我觉得它的代码不是正确的方式。 - ddinchev
我对我的问题进行了更详细的编辑,但如果有任何不清楚的地方,请告诉我如何改进我的问题。 - ddinchev
你不需要一个ID来使用Google Maps v3,DOM元素就可以了,因此你可以在jQuery中使用任何选择器。 - martin
同时,仅在操作jQuery DOM元素集合的函数中使用fn命名空间。 - martin
7个回答

19

当你使用$('#element').myPlugin()获取实例时,我理解你的意思是:

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

一开始这似乎是个好主意,但扩展jQuery原型被认为是不良实践,因为会破坏jQuery实例链。

另一个方便的方法是将实例保存在$.data对象中,这样你只需初始化插件一次,然后可以通过DOM元素引用随时获取实例,例如:

$('#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 中。现在,要检索某个实例并应用方法,您可以执行以下操作:
$('div').eq(1).data('myclass').increaseId(3).applyIdAsClass();

这是我经常使用的一种模式,非常适合我的需求。

你还可以公开这个类,以便在没有jQuery的原型的情况下使用它,方法是添加 window.MyClass = MyClass。这允许使用以下语法:

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

最终,我得到了与此非常相似的东西(非常感谢@darhazer!他向我更详细地解释了您的想法,并为我的情况提供了简单明了的实现指南),所以您应该得到赏金 :) - ddinchev
我不是私有桥接的粉丝。有没有其他方法可以在不必将函数名称作为字符串传递的情况下完成这个操作? - Ryan
1
@Ryan,你可以在相同的作用域中定义一个本地函数,并将当前实例作为函数作用域进行调用,例如 Private.increaseId.call(this, val);。但是,如果你有许多私有函数,代码可能会变得混乱。 - David Hellsing
如何使用这种模式从私有方法调用公共方法? - Ryan
1
@Ryan 尝试使用 MyClass.prototype.increaseId.call(this, 5); - David Hellsing

4

这里有个想法...

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

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

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

编辑

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


4
我认为你需要解决问题的基础是一个良好的面向对象结构,可以容纳你的设置和GoogleMap。
如果你没有使用jQuery的限制,并且对面向对象编程非常了解,我建议使用YUI3 Widget
浏览示例小部件模板应该会让你了解到该框架提供了对面向对象结构的访问,例如:
  1. 它提供命名空间支持。
  2. 它支持类和对象的概念
  3. 它整洁地支持类扩展
  4. 它提供构造函数和析构函数
  5. 它支持实例变量的概念
  6. 它提供呈现和事件绑定
在你的情况下:
  1. 您可以创建自己的GoogleHelper类,其中包含自己的实例变量以及我认为是您想要的Google地图对象。
  2. 然后,您将开始使用其自身的设置创建此类的实例。
  3. 对于每个新实例,您只需要使用ID将其映射,稍后可以引用它。通过将ID引用到具有设置和GoogleMap的GoogleHelper实例,您不必保留两个地图(一个用于保存设置,另一个用于GoogleMap),我同意这种情况并不理想。

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


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的代码在一个单独的文件中,但也可以与插件代码放在同一个文件中。

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);

请注意,该代码未经过测试。
愉快的编码 :)

1
不是挑剔,但是...是 distroy 还是 destroy - James Khoury
@James Khoury 是的,写得很匆忙。 - martin

1

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

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

这个例子不完整,但我认为它给你一个想法。所有的 JS 实际上都应该在 JS 中,唯一需要发送来回的是 JSON。生成动态 JS 不是一个明智的做法,在我看来。

<?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 库的 PHP 包装器使用是一个可重用和可配置的小部件。在视图文件中,我将配置传递给小部件并输出带有 JS 的 JSON 配置。它类似于 <?php echo $this->widget('GoogleMapsHelperWidget', array(/*config*/)); ?>。这样,我就不必在每个视图中编写 "$.fn.csGoogleMapsHelper.createMarker"、".finelize" 等内容了。 - ddinchev

0

我在jQuery插件模板-最佳实践、约定、性能和内存影响中解决了这些问题。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接