The Single-Page App
Libraries like Backbone.js, Ember.js, and Angular.js are often referred to as client-side MVC (Model-View-Controller) or MVVM (Model-View-ViewModel) libraries. The typical client-side MVC architecture looks something like this:
This is great for the user because once the app is initially loaded, it can support quick navigation between pages without refreshing the page, and if done right, can even work offline.
This is great for the developer because the idealized single-page app has a clear separation of concerns between the client and the server, promoting a nice development workflow and preventing the need to share too much logic between the two, which are often written in different languages.
Trouble in Paradise
In practice, however, there are a few fatal flaws with this approach that prevent it from being right for many use cases.
An application that can only run in the client-side cannot serve HTML to crawlers, so it will have poor SEO by default. Web crawlers function by making a request to a web server and interpreting the result; but if the server returns a blank page, it’s not of much value. There are workarounds, but not without jumping through some hoops.
While the ideal case can lead to a nice, clean separation of concerns, inevitably some bits of application logic or view logic end up duplicated between client and server, often in different languages. Common examples are date and currency formatting, form validations, and routing logic. This makes maintenance a nightmare, especially for more complex apps.
Some developers, myself included, feel bitten by this approach — it’s often only after having invested the time and effort to build a single-page app that it becomes clear what the drawbacks are.
A Hybrid Approach
At the end of the day, we really want a hybrid of the new and old approaches: we want to serve fully-formed HTML from the server for performance and SEO, but we want the speed and flexibility of client-side application logic.
An isomorphic app might look like this, dubbed here “Client-server MVC”:
In this world, some of your application and view logic can be executed on both the server and the client. This opens up all sorts of doors — performance optimizations, better maintainability, SEO-by-default, and more stateful web apps.
We launched an isomorphic library of our own earlier this year. Called Rendr, it allows you to build a Backbone.js + Handlebars.js single-page app that can also be fully rendered on the server-side. Rendr is a product of our experience rebuilding the Airbnb mobile web app to drastically improve pageload times, which is especially important for users on high-latency mobile connections. Rendr strives to be a library rather than a framework, so it solves fewer of the problems for you compared to Mojito or Meteor, but it is easy to modify and extend.
Abstraction, Abstraction, Abstraction
That these projects tend to be large, full-stack web frameworks speaks to the difficulty of the problem. The client and server are very dissimilar environments, and so we must create a set of abstractions that decouple our application logic from the underlying implementations, so we can expose a single API to the application developer.
We want a single set of routes that map URI patterns to route handlers. Our route handlers need to be able to access HTTP headers, cookies, and URI information, and specify redirects without directly accessing window.location (browser) or req and res (Node.js).
Fetching and persisting data
We want to describe the resources needed to render a particular page or component independently from the fetching mechanism. The resource descriptor could be a simple URI pointing to a JSON endpoint, or for larger applications, it may be useful to encapsulate resources in models and collections and specify a model class and primary key, which at some point would get translated to a URI.
Whether we choose to directly manipulate the DOM, stick with string-based HTML templating, or opt for a UI component library with a DOM abstraction, we need to be able to generate markup isomorphically. We should be able to render any view on either the server or the client, dependent on the needs of our application.
Building and packaging
It turns out writing isomorphic application code is only half the battle. Tools like Grunt and Browserify are essential parts of the workflow to actually get the app up and running. There can be a number of build steps: compiling templates, including client-side dependencies, applying transforms, minification, etc. The simple case is to combine all application code, views and templates into a single bundle, but for larger apps, this can result in hundreds of kilobytes to download. A more advanced approach is to create dynamic bundles and introduce asset lazy-loading, however this quickly gets complicated. Static-analysis tools like Esprima can allow ambitious developers to attempt advanced optimization and metaprogramming to reduce boilerplate code.
Composing Together Small Modules
Being first to market with an isomorphic framework means you have to solve all these problems at once. But this leads to large, unwieldy frameworks that are hard to adopt and integrate into an already-existing app. As more developers tackle this problem, we’ll see an explosion of small, reusable modules that can be integrated together to build isomorphic apps.
To demonstrate this point, I’ve created a sample app called isomorphic-tutorial that you can check out on GitHub. By combining together a few modules, each that can be used isomorphically, it’s easy to create a simple isomorphic app in just a few hundred lines of code. It uses Director for server- and browser-based routing, Superagent for HTTP requests, and Handlebars.js for templating, all built on top of a basic Express.jsapp. Of course, as an app grows in complexity, one has to introduce more layers of abstraction, but my hope is that as more developers experiment with this, there will be new libraries and standards to emerge.
The View From Here