Project information
- Category: Web framework
- Project date: December, 2012
Why would we need another web framework?
In the decade of AJAX-boom, web applications has evolved from being a bunch of linked web pages towards the highly interactive rich clients running in the browser. This experience is not just limited to the users of the AJAXed web applications. Programmers, too, try to align their development practices to what is actually happening in the application, rather than to tweak underlying HTTP protocol day after day. And the great variety of existing web frameworks is supposed to help them to get there.
Low-level web frameworks, like Struts, Spring or ASP.NET MVC remain very HTTP-centric and provide a generic set of utilities and helpers for handling HTTP requests and rendering responses. The AJAX support in such frameworks is very basic, encouraging developers to invent their own approach for effective client-server interaction. And they gladly use the opportunity in the fanciest ways, even within one product. The common result is the chaos on the server- and javascript floods on the client-side.
In contrast to the classical web frameworks, AJAX web application frameworks like GWT, Vaadin, ZK or Echo2 offer great possibilities for authoring highly interactive web applications with virtually no javascript, in a high-level language and in a very desktop-like style. The only caveat of those frameworks is their heaviness.
Such heavyweight frameworks offer a holistic approach to web application development, imposing own rules and even markup languages for creating layouts, binding data, events etc. Sometimes it works quite well. Sometimes, however, it feels like an overkill with a pretty steep learning curve.
Besides, many heavyweight frameworks seriously affect crucial technical aspects of a web application. For instance, to keep application state server-centric frameworks like ZK or echo2 extensively use server memory, which is not very good for scalability.
Probably, I got dazzled by the abundance of the existing AJAX web frameworks at some point, but I'm really missing a simple and handy one among them. So, I came up with my own.
What would I expect from a good lightweight AJAX web framework?
- minimalistic and tidy client scripts
- seamless and close integration of server and client code
- server-centric approach for the business logic
- fat-client programming model: plain and stateful objects, event driven development
- simplicity and comprehensible mode of operation
- high scalability
- low intrusiveness and usage of the good old HTML-templating
The points 1-4 are pretty good addressed by many AJAX web frameworks, but unfortunately by sacrificing 5-7. In this framework I'm trying to reconcile all the points mentioned above. The next chapter explains how I'm going to get there.
Framework Features
To meet the targets mentioned before, the web framework builds on the following concepts.
Total AJAX
Unification and simplification of the client-server communication
- Every request is an AJAX request
- Response payload is Javascript-code
- The Javascript functions are bundled in the client-side Javascript components
- Server-side complementary components (proxy components) are used to generate Javascript code for calling the corresponding Javascript components
Providing a context
Maintaining (quasi) stateful form instances, modelled with plain old objects
- HTML form component serves as a context holder
- Every "page" is represented by a derivation of the Form-component (form object)
- Page context or state is implemented with instance variables of the form object
- User actions cause an AJAX submit of the whole form, including state descriptor and form fields
- Event handlers are implemented as methods of the form component
- Event handlers can modify the form state (instance variables), change UI by calling proxy components and do any other actions
- Form object maintains its state between the actions on the form, until disposed
Standard view rendering
- Using respective HTML templating technologies to generate views
- Proxy components can be bound into templates via server-side tags or similar
Modelling flow control with objects
Form objects are just plain objects
- Navigation to another page as instantiation of a new form component
- Using parameterized constructors
- Keeping parent form as an instance variable for later use
AJAX Web Framework Architecture
User actions
Every client-side component can trigger component specific events. Every event is triggered on the parent form component by calling jasty.raiseEvent
method (see click
handler in the Button-component).
The "raiseEvent"-method makes an AJAX-submit including event handler name, event parameters and the
whole form data. All AJAX-submits are sent to a central Form Engine URL
The submit is then processed on the server by the Form Engine, which retrieves request parameters, restores the stateful presenter (Form) and calls the specified event handler method.
The classical submits are obsolete and not used anymore. AJAX form submits can do all the same things and doesn't require the page to be completely re-rendered.
Forms
In the classical MVC/MVP frameworks, actions don't share any data between requests by design. It may make those frameworks slim, fast and easy to use for building dynamic web sites, but doesn't facilitate the modelling of rich client applications.
In a rich client application, life cycle of a view can span a number of actions, each working on the same data context. Thus, view object could be modelled by a POJO with the instance fields as the data context and with the methods as event handlers. Such view object is very similar to a classical controller/presenter, except for being stateful. We call such view object a Form.
Form state on the client
The next question is how to maintain Form object between requests. Keeping it in session, like some frameworks do, is not a particularly good idea, because of the memory and scalability issues. A slightly better alternative is to keep it on the client.
When rendering the Form's main view, the Form-object gets serialized and sent in the response to the
browser. Client-side form component keeps serialized Form and jasty.raiseEvent
posts it back along
with the other data. Form Engine on the server side
deserializes the Form and calls the appropriate event handler method.
Event handlers
Event handlers are instance methods of the Form-class. The name of the handler to be called is posted back to the server on user action, along with the serialized Form object, form data and event parameters.
Event handlers contain business logic and interact with the client side via proxy components.
public class TestForm extends Form { private int numberClicked; public void buttonClicked(EventArgs e) { Button btn = get(Button.class, "someId"); numberClicked++; btn.setText("I'm clicked "+ numberClicked + " time(s)"); } }
Event handlers may also use and modify Form instance variables. The Form-object gets serialized at the end of every request and sent with the AJAX-response to the client, so that the object can be exactly restored to handle subsequent events.
Form Views
Every Form always has one associated main view. This view gets rendered when the Form has to be visualized.
Every main view is implicitly rendered within its own form component, which is responsible for providing necessary context for every event.
Method prepareModel
of the Form is used to obtain the model for the main view.
Apart from the main view, partial views can be defined for a Form. These partial views can be rendered on actions and used to dynamically overwrite particular fragments of the main view.
Finding and rendering of the views depends on the underlying template technology and is bridged by the Form Engine.
Client-side components and proxies can be bound into the views by implementing custom tags. Custom tag typically renders some HTML code (an outer tag or a placeholder tag) and javascript initialization for the component.
Underlying Form-object serves for the associated views also as a naming provider. It automatically ensures uniquity of the component ids to avoid clashes in case of several simultaneous Forms displayed.
Object-based navigation
Since Form-class together with the views represents a cohesive and stateful unit, Form object can be directly used for navigation within the web application. For instance, every Form can replace itself with another form:
public void detailsClicked(EventArgs e) { replaceWith(new DetailsForm(e.get("data"))); }
Another important scenario is the modal navigation in inline and popup modes.
Examples
See examples for Java/Servlets, Groovy/Grails and .NET/MVCExtensibility Points
Web framework exposes some extensibility points in form of interfaces and their default implementations, which can be overridden to customize behavior. Some of them
Interface FormPersister
This interface is used to abstract out the persistence and lookup of the form objects. The state descriptor, generated by persist
is sent to the
client and stored on the client side. On every user interaction the state descriptor is posted to the server along with form data. To restore the form object, lookup
is called with the sent state descriptor. After event is processed, the form is persisted again and a new state descriptor is sent back to the client and so on.
Possible implementation for this interface are:
- Form state on the client (default implementation). State descriptor is the serialized form itself.
- Form state in the session. State descriptor could be a unique id to resolve the form in the memory. Form object disposal needs to be implemented
- Form state in the database. State descriptor could be a unique id to read the form from the database. Disposal of the stored form objects needs to be implemented
Additional functionality to be implemented could be:
- Encrypting/decrypting serialized form object for the client side form persister
- Encrypting/decrypting state descriptor for server side store mode (or use UUIDs)
- Implement XSRF (Cross-Site-Request-Forgery) protection by modifying state descriptor (or of the special instance variable in case of client side form persister)
Interface MethodInvoker
Implement specific processing around event handler invocation. For example:
- Open Hibernate session and expose it e.g. on the current thread. Close session after the call is finished
- Custom exception processing and handling
- Custom population of the method parameters
Interface ParameterProvider
This interface abstracts out the obtaining of request parameters. The default implementation is normally sufficient.
Interface ViewRenderer
The implementation is dependent on the underlying view engine in the particular technology (e.g. JSP, GSP, WebForms, Razor etc.)
- implements lookup of the main and partial views for the specified form
- renders resolved views to strings, applying the specified model object
Authoring Components
Component GUI model
Bundling UI functionality in client-side components is a pretty obvious idea. The reference implementation of the component library uses just trivial stateless function maps, with the simple inheritance.
jasty.Button = jasty.extend(jasty.Control, { init: function(self, opts) { self.text(opts.text); self.click(function() { jasty.raiseEvent(self, opts.onClick, {srcId: self.attr("id"), data: opts.data}); return false; }); }, text: function(self, value) { self.text(value); } });
Method init
is reserved for component initialization. I would generally recommend to put
the whole rendering code for the component into this method. So that the only thing, rendered
on the server is the outer tag with nothing but an id specified. The remaining initialization
occurs in the init
-method.
<button id="someId"></button> <script> jasty.Button.init($("#someId"), { text: "initial text", onClick: "buttonClicked"}) </script>
Behavioural methods may substantially depend on the DOM-representation of the component. That's why it's reasonable to keep everything in one place - in the component code - only having a single DOM-tag to be rendered by the server.
To facilitate calling methods on complex selectors, as well as to provide a common interface to various component writing styles, dispatching plugin may be used:
$("#someId").jasty("Button", "text", ["some text"])
This jQuery-plugin resolves the component and calls the specified method sequentially for every selected element, applying parameters from the array in the 3rd parameter.
For the components designed in a different way, alternative dispatching plugins might be necessary. But the calling interface should be kept the same:
$(<selector>).<dispatcher>(<component>, <method>, <parameters array>)
Sticking to the common interface for call dispatchers allows for easy code generation by the server-side component proxies.
Server-side proxy components
Proxy components are used to enable interaction with the client components from the server-side code.
public class Button extends Component { @InitProperty private String text; @InitProperty private String onClick; public void setText(String text) { this.text = text; invoke("text", text); } @Override public String getHtmlTag() { return "button"; } }
By calling invoke
method, component method (setText
) generates javascript code,
which is then included in the AJAX-response and executed in the browser.
For example, these calls
Button btn = new Button(); btn.setId("someId"); btn.setText("new text");
produce following script:
$("#someId").jasty("Button", "text", ["new text"])
Emitting executable javascript is the only method of AJAX-response in the framework, and it perfectly covers all needs.
The proxy components are actively used in the event handlers of the Forms.