Какой шаблон дизайна плагинов jQuery мне следует использовать? - PullRequest
22 голосов
/ 20 августа 2011

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

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

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

РЕДАКТИРОВАТЬ

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

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

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

Я добавлю награду за это.

Ответы [ 7 ]

19 голосов
/ 23 августа 2011

Когда вы говорите «получить» экземпляр через $('#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. Теперь, чтобы получить определенный экземпляр метода применения, вы можете сделать:

$('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
4 голосов
/ 23 августа 2011

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

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

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

  1. Предоставляет поддержку пространства имен.
  2. Поддерживает понятие классов и объектов
  3. Он поддерживает расширение класса аккуратно
  4. Предоставляет конструктор и деструктор
  5. Поддерживает концепцию переменных экземпляра
  6. Обеспечивает визуализацию и привязку событий

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

  1. Вы можете создать свой класс GoogleHelper, который имеет свои собственные переменные экземпляра вместе с объектом Google Map, который, как я думаю, и является вашим намерением.
  2. Затем вы начнете создавать экземпляр этого класса с его собственными настройками.
  3. Для каждого нового экземпляра вам просто нужно сопоставить его с идентификатором, который вы могли бы использовать позже. Ссылаясь на идентификатор экземпляра GoogleHelper, который имеет настройки и GoogleMap, вам не нужно сохранять две карты (одну для хранения настроек и одну для GoogleMap), и я согласен с вами, что это не идеально ситуация.

Это в основном восходит к базовому программированию ОО, и правильная среда JS может дать вам возможность сделать это. Хотя можно использовать и другую OO JS-инфраструктуру, я считаю, что YUI3 обеспечивает лучшую структуру, чем другие, для большого проекта Javascript.

4 голосов
/ 20 августа 2011

Вот идея ...

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

EDIT

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

3 голосов
/ 26 августа 2011

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

2 голосов
/ 22 августа 2011

Многие ваши требования не нужны. Как бы то ни было, вот примерный план шаблона проектирования, который я принял для себя - который по сути прямо из документации по 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);

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

Happy Coding:)

1 голос
/ 22 августа 2011

Это может выходить за рамки вашего вопроса, но я действительно считаю, что вам следует изменить методы обработки перехода 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>
0 голосов
/ 06 декабря 2013

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

...