How to structure Javascript programs in complex web applications?
I have a problem, which is not easily described. I'm writing a web application that makes strong usage of jQuery and AJAX calls. Now I don't have a lot of experience in Javascript archicture, but I realize that my program has not a good structure. I think I have too many identifiers referring to the same (at least more or less) thing.
Let's have an look at an arbitrary exemplary UI widget that makes up a tiny part of the application: The widget may be a part of a window and the window may be a part of a window manager:
- The eventhandlers use DOM elements as parameters. The DOM element represents a widget in the browser.
- A lot of times I use jQuery objects (Basically wrapper开发者_如何学Cs around DOM elements) to do something with the widget. Sometimes they are used transiently, sometimes they are stored in a variable for later purposes.
- The AJAX function calls use string identifiers for these widgets. They are processed server side.
- Beside that I have a widget class whose instances represent a widget. It is instantiated through the new operator.
Now I have somehow four different object identifiers for the same thing, which needs to be kept in sync until the page is loaded anew. This seems not to be a good thing.
Any advice?
EDIT: *@Will Morgan*: It's a form designer that allows to create web forms within the browser. The backend is Zope, a python web application server. It's difficult to get more explicit as this is a general problem I observe all the time when doing Javascript development with the trio jQuery, DOM tree and my own prototyped class instances.EDIT2:
I think it would helpful to make an example, albeit an artificial one. Below you see a logger widget that can be used to add a block element to a web page in which logged items are displayed.makeLogger = function(){
var rootEl = document.createElement('div');
rootEl.innerHTML = 'Logged items:';
rootEl.setAttribute('class', 'logger');
var append = function(msg){
// append msg as a child of root element.
var msgEl = document.createElement('div');
msgEl.innerHTML = msg;
rootEl.appendChild(msgEl);
};
return {
getRootEl: function() {return rootEl;},
log : function(msg) {append(msg);}
};
};
// Usage
var logger = makeLogger();
var foo = document.getElementById('foo');
foo.appendChild(logger.getRootEl());
logger.log('What\'s up?');
At this point I have a wrapper around the HTMLDivElement (the hosted object). With having the logger instance (the native object) at hand I can easily work with it through the function logger.getRootEl().
Where I get stuck is when I only have the DOM element at hand and need to do something with the public API returned by function makeLogger (e.g. in event handlers). And this is where the mess starts. I need to hold all the native objects in a repository or something so that I can retrieve again. It would be so much nicer to have a connection (e.g. a object property) from the hosted object back to my native object. I know it can be done, but it has some drawbacks:- These kind of (circular) references are potentially memory leaking up to IE7
- When to pass the hosted object and when to pass the native object (in functions)?
For now, I do the back referencing with jQuery's data() method. But all in all I don't like the way I have to keep track of the relation between the hosted object and its native counterpart.
How do you handle this scenario?
EDIT3: After some insight I've gained from Anurag's example.. *@Anurag:* If I've understood your example right, the critical point is to set up the correct (what's correct depends on your needs, though) execution context for the event handlers. And this is in your case the presentation object instance, which is done with Mootool's bind() function. So you ensure that you're *ALWAYS* dealing with the wrapper object (I've called it the native object) instead of the DOM object, right? A note for the reader: You're not forced to use Mootools to achieve this. In jQuery, you would setup your event handlers with the *$.proxy()* function, or if you're using plain old Javascript, you would utilize the *apply* property that every function exposes.You could use a global registry:
window.WidgetRegistry = {};
window.WidgetRegistry['foowidget'] = new Widget('#myID');
and when AJAX calls return, they can get the widget like this:
var widgetID = data.widgetID;
if (widgetID in window.WidgetRegistry) {
var widget = window.WidgetRegistry[widgetID];
}
For your jQuery calls: I'd guess they are relatively inexpensive, since jQuery caches objects for later use. But you could extend the above suggested WidgetRegistry
by using .data()
:
var $widget = $('#myWidget');
var widgetID = 'foo';
$widget.data('widget', widgetID);
In this way, you can store the widget ID attached to each jQuery object and re-access it from the global registry.
Testing, if an jQuery object has an existing widget:
return $('#test').data('widget') &&
($('#test').data('widget') in window.WidgetRegistry);
Note, that these are just suggestions. Actually, there are dozens of ways to achieve a consolidation like this. If you want to combine your code deeper with jQuery, you could extend the jQuery object, so that you could write something like:
$('#widget').widget({'foo':'bar'});
// and/or
var allWidgets = $('*:widget');
// ...
For the four objects that need to be synchronized, you could have a single object and pass the reference around in a constructor, or as function arguments.
The way I fix this problem is to never lose a reference to the wrapper object. Whenever a DOM object is needed (for example inserting into the page), this wrapper object provides it. But sticking that widget onto the screen, the wrapper object sets up all event handling and AJAX handling code specific to the widget, so the reference the the wrapper is maintained at all times in these event handlers and AJAX callbacks.
I've created a simple example on jsfiddle using MooTools that might make sense to you.
I'm not sure I've fully understood your question, but I'll try to point some ideas.
In my opinion, you should make base widget class, which contains common functionality for widgets.
Let's use for example AppName.Widgets.base(). One of the instance variables is _events, which is object that stores events as keys and function as values. That way each class defines the events for this widget, and you can easily bind them in the constructor. As for the string identifiers, the easiest way is to use toString().
Example:
namespace('AppName.Widgets'); // you can find implementations easy
AppName.Widgets.base = function() {
if (!this._type) return;
this._dom = $('div.widget.'+this._type);
for (var e in this._events) {
this._dom.bind(e, this._events[e]);
}
this.toString = function() { return this._type; };
}
AppName.Widgets.example = function() { // extends AppName.Widgets.base
this._type = 'example';
this._events = { 'click' : function(e) { alert('click'); } };
AppName.Widgets.base.call(this);
}
A lot of what you can or can't do will depend on how much control you have over the javascript. Personally I often have to use libraries built by others so I might only get a DOM node to work with, but I really need my object instead. In these cases I find using the data feature in jQuery is very handy. By using the data feature, you can 'store' your object inside the DOM node to retrieve it later.
Given your example above, here's how you could use the data feature to get back your widget after having functions that only use the DOM node.
makeLogger = function(){
var rootEl = document.createElement('div');
rootEl.innerHTML = 'Logged items:';
rootEl.setAttribute('class', 'logger');
var append = function(msg){
// append msg as a child of root element.
var msgEl = document.createElement('div');
msgEl.innerHTML = msg;
rootEl.appendChild(msgEl);
};
var self = {
getRootEl: function() {return rootEl;},
log : function(msg) {append(msg);}
};
// Save a copy to the domNode
$(rootEl).data("logger", self);
return self;
};
// Example of only getting the dom node
function whatsUp (domNode){
// Get the logger from the domNode
$(domNode).data('logger').log('What\'s up?');
}
// Usage
var logger = makeLogger();
var loggerNode = logger.getRootEl();
var foo = document.getElementById('foo');
foo.appendChild(loggerNode);
whatsUp(loggerNode);
精彩评论