Какой шаблон дизайна плагина jQuery следует использовать?

Мне нужно построить плагин jQuery, который вернет один экземпляр на идентификатор селектора. Плагин должен и будет использоваться только для элементов с id (невозможно использовать селектор, который соответствует многим элементам), поэтому его следует использовать следующим образом:

$('#element-id').myPlugin(options);
  • Мне нужно иметь несколько частных методов для плагина, а также несколько общедоступных методов. Я могу добиться этого, но моя главная проблема в том, что я хочу получить тот же самый экземпляр каждый раз, когда я вызываю $('# element-id'). MyPlugin().
  • И я хочу иметь некоторый код, который должен выполняться только в первый раз, когда плагин инициализируется для данного ID (конструкции).
  • Параметр options должен быть предоставлен в первый раз, для конструкции, после чего я не хочу, чтобы конструкция выполнялась, чтобы я мог получить доступ к плагину так же, как $('# element-id'). MyPlugin()
  • Плагин должен иметь возможность работать с несколькими элементами (обычно до двух) на одной странице (но для каждого из них потребуется собственная конфигурация, опять же - они будут инициализированы по идентификатору, а не для обычного селектора классов для пример).
  • Вышеприведенный синтаксис, например, - я открыт для любых предложений о том, как достичь этого шаблона.

У меня есть довольно некоторый опыт ООП с другим языком, но ограниченное знание javascript, и я действительно смущен тем, как все правильно.

ИЗМЕНИТЬ

Чтобы разработать - этот плагин представляет собой API-интерфейс 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 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, где карта инициализируется. Он используется в качестве ключа в .map и .settings имеет карты, где я храню настройки и экземпляр MapMobject GoogleMaps для каждого инициализированного таких GoogleMaps на странице. $jsOptions и $data из кода PHP являются объектами JSON.

Теперь мне нужно создать экземпляр GoogleMapsHelper, который содержит свои собственные настройки и объект карты GoogleMaps, чтобы после того, как я инициализировал его на определенном элементе (по его идентификатору), я могу повторно использовать этот экземпляр. Но если я инициализирую его на N элементах на странице, каждый из них должен иметь собственную конфигурацию, объект карты и т.д.

Я не утверждаю, что это реализовано как плагин jQuery!. Я настаиваю на том, что он гибкий и расширяемый, потому что я буду использовать его в большом проекте с более чем дюжинами запланированных в настоящее время различных экранов, где это будет использоваться так через несколько месяцев, изменение интерфейса использования будет кошмаром для рефакторирования по всему проекту.

Я добавлю щедрость для этого.

Ответ 1

Когда вы скажете "получить" экземпляр через $('#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();

Это образец, который я использовал много раз, который отлично подходит для моих нужд.

Вы также можете открыть класс, чтобы использовать его без прототипа jQuery, добавив window.MyClass = MyClass. Это позволяет использовать следующий синтаксис:

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

Ответ 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, вы можете использовать атрибуты данных для элемента.

ИЗМЕНИТЬ

На ум пришло еще одно. Вы можете использовать виджеты jQuery.UI.

Ответ 3

Я думаю, что вам нужно решить вашу проблему, это в основном хорошая структура OO для хранения как вашей настройки, так и GoogleMap.

Если вы не привязаны к jQuery и хорошо знаете OOP, я бы использовал YUI3 Widget.

Взгляд на Пример шаблона виджета должен дать вам представление о том, что структура обеспечивает доступ к структуре ООП, например:

  • Он обеспечивает поддержку пространства имен.
  • Поддерживает понятие классов и объектов
  • Он поддерживает расширение класса аккуратно
  • Он предоставляет конструктор и деструктор
  • Он поддерживает концепцию переменных экземпляра
  • Он обеспечивает привязку реквизита и события

В вашем случае:

  • Вы можете создать свой класс GoogleHelper, который имеет свои собственные переменные экземпляра вместе с объектом Google Map, который, как я думаю, является тем, что вы намеревались.
  • Затем вы начнете создавать экземпляр этого класса со своими собственными настройками.
  • Для каждого нового экземпляра вам просто нужно сопоставить его с идентификатором, который вы могли бы ссылаться позже. Сопоставляя идентификатор экземпляра GoogleHelper, который имеет как настройки, так и GoogleMap, вам не нужно сохранять две карты (одна для хранения настроек и одна для 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

Вот фрагмент, в который вы поместите свой код:

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

Обратите внимание, что код не проверен.

Счастливое кодирование:)

Ответ 6

Это может быть вне сферы вашего вопроса, но я действительно думаю, что вам следует реорганизовать, как вы обрабатываете переход PHP → JS (в частности, весь ваш последний блок кода PHP).

Я думаю, что это анти-шаблон для генерации тонны JS в PHP, который затем запускается на клиенте. Вместо этого вы должны возвращать данные 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>

Ответ 7

Я рассмотрел эти проблемы в шаблоне плагина 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/