我应该使用哪种 jQuery 插件设计模式?
我需要构建一个 jQuery 插件,它会为每个选择器 ID 返回一个实例.该插件应该并且只会用于具有 id 的元素(不可能使用匹配许多元素的选择器),因此应该像这样使用它:
I need to build a jQuery plugin that would return a single instance per selector id. The plugin should and will only be used on elements with id (not possible to use selector that matches many elements), so it should be used like this:
$('#element-id').myPlugin(options);
- 我需要能够为插件提供一些私有方法以及一些公共方法.我可以做到这一点,但我的主要问题是每次调用 $('#element-id').myPlugin() 时我都想获得相同的实例.
- 我想要一些代码,只在第一次为给定 ID(构造)初始化插件时执行.
options
参数应该第一次提供,对于构造,之后我不希望构造被执行,这样我就可以像 $('#element 一样访问插件-id').myPlugin()- 插件应该能够在同一页面上处理多个元素(通常最多 2 个)(但每个元素都需要自己的配置,再次重申 - 它们将由 ID 初始化,而不是通用的类选择器例子).
- 上面的语法只是一个例子——我愿意接受任何关于如何实现该模式的建议
- I need to be able to have few private methods for the plugin as well as few public methods. I can achieve that but my main issue is that I want to get the very same instance every time I call $('#element-id').myPlugin().
- And I want to have some code that should be executed only the first time the plugin is initialized for a given ID (construct).
- The
options
parameter should be supplied the first time, for the construct, after that I do not want the construct to be executed, so that I can access the plugin just like $('#element-id').myPlugin() - The plugin should be able to work with multiple elements (usually up to 2) on the same page (but each and every one of them will need own config, again - they will be initialized by ID, not common class selector for example).
- The above syntax is just for example - I'm open for any suggestions on how to achieve that pattern
我在其他语言方面有相当多的 OOP 经验,但对 javascript 的了解有限,我真的很困惑如何正确地做到这一点.
I have quite some OOP experience with other language, but limited knowledge of javascript and I'm really confused on how do it right.
编辑
详细说明 - 这个插件是一个 GoogleMaps v3 API 包装器(帮助器),可以帮助我摆脱代码重复,因为我在很多地方使用谷歌地图,通常带有标记.这是当前的库(删除了很多代码,只剩下最重要的方法):
To elaborate - this plugin is a GoogleMaps v3 API wrapper (helper) to help me get rid of code duplication as I use google maps on many places, usually with markers. This is the current library (lots of code removed, just most important methods are left to see):
;(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 包装器可以通过一次调用来自动化它,但基本上):
It's usage looks like this (not actually exactly like this, because there is a PHP wrapper to automate it with one call, but basically):
$js = "$('#$id').csGoogleMapsHelper($jsOptions);
";
if ($this->venues !== null) {
foreach ($this->venues as $row) {
$data = GoogleMapsHelper::getVenueMarkerOptionsJs($row);
$js .= "$.fn.csGoogleMapsHelper.createMarker('$id', $data, true);
";
}
}
$js .= "$.fn.csGoogleMapsHelper.finalize('$id');
";
echo $js;
上述实现的问题是我不喜欢为设置"和地图"保留哈希映射
The problems of the above implementation are that I don't like to keep a hash-map for "settings" and "maps"
$id
是初始化地图的 DIV 元素 ID.它用作 .map 中的键,并且 .settings 具有地图,其中我保存页面上每个已初始化的此类 GoogleMaps 的设置和 GoogleMaps MapObject 实例.PHP 代码中的 $jsOptions
和 $data
是 JSON 对象.
The $id
is the DIV element ID where the map is initialized. It's used as a key in the .map and .settings has maps where I hold the settings and GoogleMaps MapObject instance for each initialized such GoogleMaps on the page. The $jsOptions
and $data
from the PHP code are JSON objects.
现在我需要能够创建一个 GoogleMapsHelper 实例来保存它自己的设置和 GoogleMaps 地图对象,以便在我对某个元素(通过它的 ID)初始化它之后,我可以重用该实例.但是如果我在页面上的 N 个元素上初始化它,每个元素都应该有自己的配置、地图对象等.
Now I need to be able to create a GoogleMapsHelper instance that holds its own settings and GoogleMaps map object so that after I initialize it on certain element (by its ID), I can reuse that instance. But if I initialize it on N elements on the page, each and every of them should have own configuration, map object, etc.
我不坚持这是作为一个 jQuery 插件实现的!我坚持认为它是灵活和可扩展的,因为我将在一个大型项目中使用它,其中包含十多个当前计划的不同屏幕几个月后就会被使用,改变它的使用界面将是对整个项目进行重构的一场噩梦.
I do not insist that this is implemented as a jQuery plugin! I insist that it's flexible and extendable, because I will be using it in a large project with over dozen currently planned different screens where it will be used so in few months, changing it's usage interface would be a nightmare to refactor on the whole project.
我会为此添加赏金.
推荐答案
当你说通过 $('#element').myPlugin()
获取"实例时,我假设你的意思是:
When you say "get" the instance via $('#element').myPlugin()
I assume you mean something like:
var instance = $('#element').myPlugin();
instance.myMethod();
一开始这似乎是个好主意,但它被认为是扩展 jQuery 原型的不好做法,因为您破坏了 jQuery 实例链.
This might seem to be a good idea at first, but it’s considered bad practice for extending the jQuery prototype, since you break the jQuery instance chain.
另一种方便的方法是将实例保存在 $.data
对象中,因此您只需初始化插件一次,然后您可以随时使用 DOM 元素获取实例作为参考,f.ex:
Another handy way to do this is to save the instance in the $.data
object, so you just initialize the plugin once, then you can fetch the instance at any time with just the DOM element as a reference, f.ex:
$('#element').myPlugin();
$('#element').data('myplugin').myMethod();
这是我用来在 JavaScript 和 jQuery 中维护类类结构的模式(包括注释,希望你能关注):
Here is a pattern I use to maintain a class-like structure in JavaScript and jQuery (comments included, hope you can follow):
(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 ));
现在,你可以这样做了:
Now, you can do:
$('div').myClass();
这将为找到的每个 div 添加一个新实例,并将其保存在 $.data 中.现在,要检索某个实例的应用方法,您可以这样做:
This will add a new instance for each div found, and save it inside $.data. Now, to retrive a certain instance an apply methods, you can do:
$('div').eq(1).data('myclass').increaseId(3).applyIdAsClass();
这是我多次使用的模式,非常适合我的需求.
This is a pattern I have used many times that works great for my needs.
您还可以通过添加 window.MyClass = MyClass
公开该类,以便在没有 jQuery 原型的情况下使用它.这允许使用以下语法:
You can also expose the class so you can use it without the jQuery prototyp by adding window.MyClass = MyClass
. This allows the following syntax:
var instance = new MyClass( document.getElementById('element'), {
id: 5
});
instance.increaseId(5);
alert( instance.options.id ); // yields 10
相关文章