run your client-side apps on the server
A server that runs your client-side apps.
Web apps are shifting client-side. More and more logic is moving from server to client, but that often ends up with your server just serving up a JSON API and a blank index.html which gets filled with content client-side. This sucks for two reasons:
- It's slow. You must make at least two round trips to the server before any content is displayed.
A typical solution to this problem is to write server-side code that renders some of what the client would render. This works, but now you're writing everything twice.
But what if we could use client-side APIs on the server? What if we could generate HTML with the DOM and jQuery? Make HTTP requests with XMLHTTPRequest? This would mean you could run your client-side code on the server without modification.
Otter does just that. When a client makes a request to Otter, it loads up your app inside Zombie.js, a Node implementation of the browser APIs. After it has finished loading a page, it renders the DOM to a string and sends it back to the client.
You've now got an application which behaves like it was written in a server-side language, but actually shares its code with the client.
Nope! You won't have to modify your server.
Nope! Unlike other techniques that allow running the same code on the server and in the browser, Otter is framework agnostic. It's an implementation of the browser APIs on the server, so almost any code which runs inside the browser will run inside Otter.
Otter is far more paranoid than a browser, so you won't trip up on common client-side vulnerabilities.
All code runs inside a sandbox. Node's sandboxes aren't perfect though – you must still ensure that you always run trusted code – but to help with that, Otter only allows HTTP requests to the local server by default. If you wish to load data from other domains, you must allow them explicitly.
$ sudo npm install -g otter
(Or use your preferred way of installing npm packages.)
Otter is, at a basic level, an HTTP server. Pointed at a directory, it will serve the files inside it. It only starts doing clever things when it is asked to serve a file that doesn't exist.
Instead of showing a 404, it will open the file
index.html in Zombie.js. When Zombie.js finishes loading the page (all Ajax requests have finished, etc) it sends
document.outerHTML as the HTTP response back to the browser.
To demonstrate how Otter works, an example app is included in
example/. It's a simple Twitter client written in Backbone. The first page load runs server-side, then the client instantiates Backbone's router to handle subsequent requests. It uses backbone-otter to handle caching between server and client.
Run Otter on that directory, allowing requests to
$ otter -a api.twitter.com example/ Server started on port 8000.
Point your browser at http://localhost:8000.
$ otter [options] <path>
Otter is passed a path to a directory which is expected to contain an
index.html file. It takes these options:
A comma-separated list of hosts to allow connections to (e.g.
api.example.com,api.twitter.com). By default, Otter will not allow connections to any host except itself. If you want to allow Ajax connections to your API, for example, you will need to add it to this list.
The port to listen on. Default: 8000
The number of worker processes to spawn, defaulting to the number of CPUs.
Otter provides an API to use inside your apps, exposed as
window.otter on both the server and the client.
false, whether or not the page is running on the server or client.
An object that can be used to pass data from the server to the client.
When your app is running inside Otter, it can set keys on this object. The object is serialised to JSON and injected into the top of the page sent to the browser. When the browser opens the page, the value of
window.otter.cache is restored from the serialised JSON.
See the section Resuming your app on the client for an example of how this object can be used.
Writing an app for Otter is almost the same as writing a single-page app just for the browser, but there are a few things you need to take into account.
Some code only makes sense to run on the client; for example, handling user interactions, drawing to canvases etc. You can use the
window.otter.isServer variable to check if you are running on the server:
if windowotter && windowotterisServer// Running on the serverelse// Running in the browser
Reinstantiating the app on the client after the app has been run on the server, in order that it can handle user interaction and route future pages, is a tricky problem. Otter is framework agnostic, so it doesn't prescribe a solution. It does, however, provide tools, such as
window.otter.cache (see API) and backbone-otter if you're using Backbone.
The brute-force approach is to reroute the URL, completely rebuilding the page client-side. This isn't as scary as it sounds if you cache the data that was fetched on the server, but the downsides of this are obvious inefficiency, and possibly odd side-effects of loading in a new copy of the DOM, if the user has already interacted with the initial DOM.
If you want a more efficient solution, we can work smarter. We can cache data that Otter fetches from your API, and pass it on to the client. If we then rebuild a set of models and views attached to the correct DOM elements that the server has generated, we can "boot up" the application again without having to regenerate the HTML. In Backbone, this is a matter of only rendering a view if it hasn't already been rendered by the server. See the included example app for a simple demonstration of how this can be done.
I am working on some Backbone tools for Otter to make this process easier.
Otter will intercept changes in location (e.g. setting
window.location) and immediately respond with a 302 redirect. This causes the browser to change location as you would expect if the code was running client-side.
Cookies in an HTTP request to Otter will be passed through to
document.cookie so that they are available inside Otter. Similarly, any cookies set in
document.cookie will be sent back with the HTTP response to the browser.
Instead of running Otter standalone, it can also be extended by using it as part of a Node.js app. See
lib/otter/server.coffee for the Express app that is used internally. The renderer used in this file is available externally as
$ npm test