How would you organize a large complex web application (see basic example)?
Just to keep things interesting and close my final open question, the solution that implements the below functionality in a nicely organized manner with a decent architecture gets a good bounty. The full code is on jsfiddle, and feel free to ask any questions :)
How do you usually organize complex web applications that are extremely rich on the client side. I have created a contrived example to indicate the kind of mess it's easy to get into if things are not managed well for big apps. Feel free to modify/extend this example as you wish - http://jsfiddle.net/NHyLC/1/
The example basically mirrors part of the comment posting on SO, and follows the following rules:
- Must have 15 characters minimum, after multiple spaces are trimmed out to one.
- If
Add Comment
is clicked, but the size is less than 15 after removing multiple spaces, then show a popup with the error. - Indicate amount of characters remaining and summarize with color coding. Gray indicates a small comment, brown indicates a medium comment, orange a large comment, and red a comment overflow开发者_StackOverflow中文版.
- One comment can only be submitted every 15 seconds. If comment is submitted too soon, show a popup with appropriate error message.
A couple of issues I noticed with this example.
- This should ideally be a widget or some sort of packaged functionality.
- Things like a comment per 15 seconds, and minimum 15 character comment belong to some application wide policies rather than being embedded inside each widget.
- Too many hard-coded values.
- No code organization. Model, Views, Controllers are all bundled together. Not that MVC is the only approach for organizing rich client side web applications, but there is none in this example.
How would you go about cleaning this up? Applying a little MVC/MVP along the way?
Here's some of the relevant functions, but it will make more sense if you saw the entire code on jsfiddle:
/**
* Handle comment change.
* Update character count.
* Indicate progress
*/
function handleCommentUpdate(comment) {
var status = $('.comment-status');
status.text(getStatusText(comment));
status.removeClass('mild spicy hot sizzling');
status.addClass(getStatusClass(comment));
}
/**
* Is the comment valid for submission
* But first, check if it's all good.
*/
function commentSubmittable(comment) {
var notTooSoon = !isTooSoon();
var notEmpty = !isEmpty(comment);
var hasEnoughCharacters = !isTooShort(comment);
return notTooSoon && notEmpty && hasEnoughCharacters;
}
/**
* Submit comment.
* But first, check if it's all good!
*/
$('.add-comment').click(function() {
var comment = $('.comment-box').val();
// submit comment, fake ajax call
if(commentSubmittable(comment)) {
..
}
// show a popup if comment is mostly spaces
if(isTooShort(comment)) {
if(comment.length < 15) {
// blink status message
}
else {
popup("Comment must be at least 15 characters in length.");
}
}
// show a popup is comment submitted too soon
else if(isTooSoon()) {
popup("Only 1 comment allowed per 15 seconds.");
}
});
Edit 1:
@matpol Thanks for the suggestion for a wrapper object and plugin. That will really be a big improvement over the existing mess. However, the plugin is not independent and as I mentioned, it would be part of a larger complex application. Application wide policies on client/server side would dictate things like minimum/maximum length of a comment, how often can a user comment, etc. Surely the plugin can be fed this information as parameters.
Also, for a rich client side application, the data would have to be separated from its html representation, as many server round-trips can be saved since the application is data-aware and things could be stored locally, and periodically updated on the server, or upon interesting events within the application itself (such as when the window is closed). Here's why I don't really like a plugin approach. It would work as in provide a packaged representation, but it would still be centered around the DOM, which is going to be problematic when you have 20 such plugins in the application, which is not an absurd number by any means.
The way in which I would do this is 3 fold.
- Encapsulate javascript in small well-defined classes within namespaces
- Javascript classes should have HTML they require "injected" into them as dependency allowing out-of-browser unit testing
- Move as much client-side functionality to server as possible and use a concept known as AHAH
Javascript name-spacing
This can be achieved easily and has been covered in other posts such as this Is there a "concise" way to do namespacing in JavaScript?
Small encapsulated classes
Javascript code, just like server-side code should be well encapsulated with small cohesive classes and methods. Each class lives in a separate file, named along with the namespace it is in, eg: MyCompany.SomePackage.MyClass.js. Excessive HTTP requests to each file can be saved via combining and minifying these class files at build time.
Dependency Inversion in Javascript
So effectively instead of selecting the elements you require to work with inside your class, like this:
var MyNamespace.MyClass = function() {
var elementINeed = $('#IdOfElementINeed');
}
You would inject it as such:
var foo = new MyNamspace.MyClass($('#IdOfElementINeed'));
var MyNamespace.MyClass = function(incomingDependency) {
var elementINeed = incomingDependency;
}
This technique lends itself well to testable javscript and seperation of concerns through MVC style layering of your code.
AHAH and Client-side simplification
AHAH is quite an old technique that has been around for quite some time in web-development, although is making a resurgence amongst web aficionados for its pure simplicity. However, the philosophy must be bought into at more than the architectural technique level and it must be used as a replacement for all your client side javascript eg: validation, showing/hiding dynamic content, calculations etc
Where you may used to have attached an onClick event with client-side complexity:
$('#someElement').click(function(){
// insert complex client-side functionality here, such as validating input
// eg var isValid = $(this).val() < minimumCommentLength;
// update page based on result of javascript code
// eg $('#commentTooLong').show();
})
Now you would simply trigger an ajax request back to the server to get the new HTML and simply replace all or some of the elements you are interested in as such:
$('#addCommentButton').click(function(){
$.ajax({
url: "/comment/add",
context: document.body, success:
function(responseHTML){
$('body').html(reponseHTML);
}});
})
Obviously this is a trivial example, but when used effectively, ANY javascript event on the page, simply fires off the identical ajax request and HTML replacement, greatly reducing the amount of client-side code required. Moving it to the server where it can be effectively tested.
AHAH nay-sayers, will argue that this is not a performant way to run a web-site, however I have used and seen this technique on sites with 56k modem access and also massively scaled public web-sites. The result is of course slower, but you can still produce sub 100 millisecond round trips, which is practically instant to humans.
Matpol gave a literal solution to the specific information provided. Your edit implies that you are looking for an answer to a more hypothetical question. In other words, you're looking for the "approach".
Answering the question in this manner; the single example you gave is a bit of a red herring. It is but a single item in the whole app, and prevents us from "seeing the forest from the trees". So what is the Forest? The question, asked as such, doesn't define it. But I think all programmers will agree when I say it's a nightmare to work on a project that has no definition. So, I'll rephrase your question and answer, "What is the process for defining a project?"
Mine is, in fact, asking a series of questions:
- What is the core purpose of this application?
- When do you want it launched? If we had to launch it in 1/4 that time, which features would you keep?
- Which features are you absolutely certain you're going to need afterwards?
A lot of the time, to get to the bottom of those questions, I need to ask other business questions:
- Who is your audience?
- Why do they care about this site? What's going to keep them coming back?
- How are you going to generate revenue?
- What is your call to action? If you could funnel all of your users down a single path, which would it be?
Hopefully, these questions will result in a set of foundational code that you can consider your core. Your core, as you suspected, probably doesn't fit the modular approach. And as you identified, you will want to break that core down into a Model / View / Controller layout.
But it's inevitable that you will need to add design fluff. And this brings us back to your code example - its fluff. Fluff belongs in a plugin, separated from the core. This isn't to say that all of your plugins should be delivered to the user in separate js files... but for your development purposes, they should be viewed as modular, and independent from the core code base.
I would either turn it in to a jQuery plugin or a static object.
The static object just acts as a kind or wrapper. I would also break it up in to smaller functions e.g.
init()
checkLength()
checkTime()
So you might end up with something like:
Widget = {
init:function(){//setup events etc},
checkLength:function(){},
checkTime:function(){},
doMessage:function(){}
}
The current code is a good start and can be made to "scale" into large apps that avoid hard-coding and have clear MVC separation with just a few changes.
- This should ideally be a widget or some sort of packaged functionality
A widget will make reusing comment functionality easier and provide for reuse in different pages/apps. Extend the encapsulation and separation of concerns not only to the presentation, but also to the widget model. When you think of a comment field, it's intuitive to think of the state of the component as the comment text, yet all the parameters affecting it's behaviour can be part of it's model, including validation parameters. So, in addition to the comment text, I would have the model include:
- a mapping of character count to a size-category (too-small, small, medium, large, overflow).
- the max comment submit frequency (15 seconds)
The widget model updates the size-category as the text is changed. The view listens to changes to the size-category, and the size-category value used to update the text class to produce CSS-styling for different comment lengths. The size-category is also checked when "Add Comment" is clicked. If it is "too-small" or "overflow" then a popup can be shown. Note that the Add Comment handler does not checking character count - that is isolated in the model - it checks the size-category. If necessary, the "Add Comment" controller could use the same mapping of character count to size-category that the model uses to produce helpful messages for the user. E.g. "Comments must be at least 15 characters" where 15 is fetched from the size-category map.
The model also provides a "number of characters left" property whose change events are used to update the widget's UI. The maximum character count is fetched from the character to size-category map.
- Things like a comment per 15 seconds, and minimum 15 character comment belong to some application wide policies rather than being embedded inside each widget.
- Too many hard-coded values.
- No code organization. Model, Views, Controllers are all bundled together. Not that MVC is the only approach for organizing rich client side web applications, but there is none in this example.
There may be many types of comments (e.g. different items being commented on) and different types of comment-widget. If they should all have the same minimum/maximum range then they should all be parameterized with the same model values controlling the comment validation. This is best done server-side when building the data for the comment model. The comment text is fetched for that specific comment, and the comment validation values, such as size-category mapping are fetched from page or application configuration defaults. By having central logic for producing the component validation model, adding new rules becomes much simpler - e.g. such as "moderators can post comments upto 1K", becomes a change in one piece of code. Another point to having the component model computed server side is that the model should also be validated server-side - client validation is more a user-convenience (inconvenience some might think!) - and not a hard enforcement. JavaScript can be disabled and HTTP requests constructed independently from the validating client.
To sum up, much of this can be seen as organizing production of the widget model server side. By doing this server side, the server can enforce validation rules and shield the widget from the complexity of the rules and application-wide configuration.
I've not mentioned jQuery or any UI technogy since this pattern is valid for applications irrespective of UI technology. How you apply the pattern will be UI-specific to some extent such as how the validation model is provided to the widget, or how to hook up listeners to the model, but the organizational level of the pattern is orthogonal to the UI. The primary focus is on the model - extending it to include validation aspects and computing it server side. Once that is in place, the organization problem is pretty much solved.
精彩评论