A Little Background Recently at work we've been in heavy development mode on a new thick-client application architecture. If you aren't familiar with the concept, thick-client essentially means putting most of the work onto the client's processor, and removing it from the server. The benefits of this are many, not the least of which is a much faster site response time for your users, and a much lighter load for your server.
In combination with jQuery (we are already using it heavily), and Require.js (a dynamic resource loader which has also been in use for quite a while), we had the perfect trinity of tools to get the job done.
Basic App Structure
There are a lot of things to consider when architecting a large application. For HMS, this is compounded by the fact that a) we have a lot of users with a lot of different site configurations, b) we have different variations on the main backend application, c) there is a lot of legacy code and newer front-end logic that needs to maintain compatibility so that pieces can be added one at a time. This is where Require and its support of AMD (asynchronous module definition) really shines.
All of our thick-client application code is managed through AMD, and there are two things about it that are very awesome:
- It allows seamless integration on a module-by-module basis with the current application code
- Even portions of modules can be broken out and used before their parent module is complete - for example, we already have the autocomplete module live, even though the search module is still in development. When the time comes, it will be trivial to move it back into its proper place.
The general application structure goes like this:
- Event Aggregator Outlined here, we have a single global object, and its job is to manage communication between modules.
- Outer Router We have two routers. This helps keep the application router a lot simpler, and keeps module related logic inside the module. The outer router manages routes for the entire application, and determines which modules to load.
- User Module Right now, this is the only module that sits everywhere in the application. As such, it is loaded by the outer router.
- Modules Each module contains its own router as well as templates and views. The router loads the views, the views load the templates (we're using Hogan). When we compile with require, each module (including templates) becomes a single file (the module router).
- Models The models are the glue between the client and the server, and they sit in their own folder because multiple modules may use the same models.
- Form Mediator This is actually one of the modules, but its worth describing here. We use a special Backbone view to handle every form, enhancing functionality as we go. This view simplifies forms immensely, managing the model, validation, and state with ease. Its special power is to allow multiple modules to combine into a single form definition - most usefully for search / advanced search.
- UI Modules Most DOM manipulation (outside of insertions/removals) and all UI effects are offloaded into separate UI modules. The reasons for this are a) it separates presentation concerns and b) it allows for a much easier time should we decide to switch from jQuery for future manipulation (one can dream of full standards support across browsers)
- Backbone.Store (localStorage) Of course we're using this for our app. Currently it persists forms, helps maintain the user session client-side, and caches model data.
Obviously, there are many ways to architect a thick-client application. Our approach intends to prevent module dependency (outside of UI Modules), separate DOM manipulation from data logic, keep communication centralized in an event aggregator, load quickly (through smart module loading and use of localStorage, among other things), and preserve the global namespace through the use of AMD. There are a lot of details I've left out (for instance reuse for mobile), but I do hope to dive a little deeper in future posts.