Какой шаблон дизайна плагина 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/