doppelganger

Run Backbone.js apps on a Node.js server

npm install doppelganger
12 downloads in the last month

Doppelganger

Run Backbone.js apps on a Node.js server

The motivation behind Doppelganger

Single-page apps are great for interactivity, but they typically face problems with SEO, accessibility and bookmarking, due to the fact that their pages do not exist anywhere as externally accessible resources.

Doppelganger solves these problems by running an instance of the exact same single-page app on the server using Node.js. This server-side app instance is used to generate the HTML that is presented to a web crawler or a user who has JavaScript disabled.

This means that that the HTML that is dynamically generated on the server-side exactly mirrors the DOM structure created by the client side code. All the URL routes within the app are faithfully duplicated, and the content at each URL is identical to the content that is generated on the client-side when the user navigates to that URL route within the app.

The best thing about this is that with Doppelganger, you get all these benefits without needing to perform any modifications whatsoever to the client-side code, so you can be up and running within minutes.

How Doppelganger works

Doppelganger takes a Backbone.js app, and instantiates it 'behind the scenes' on the Node.js server. This gives the server a live copy of the client-side app to work with.

When a request is made to the server, the following steps are typically carried out:

  1. The server checks through the routes that are registered within the Backbone app instance
  2. If it finds a route that matches the requested URL, it calls the app instance's Backbone.history.navigate() method (or if no matching routes are found, the server responds appropriately)
  3. This causes the server-side app instance to update its DOM hierarchy accordingly
  4. The server gets an HTML version of the app instance's updated DOM hierarchy
  5. This HTML is sent back as the response body

These steps mean that with minimal effort, the single-page app will now be fully accessible without relying on any client-side scripting.

Examples

The project at https://github.com/timkendrick/doppelganger-examples provides a demonstration of Doppelganger in action.

View the source of pages generated by the Doppelganger server to see the HTML that has been dynamically inserted on the server-side.

Installation

# Install the latest version of Doppelganger using NPM
npm install doppelganger

Usage

Instantiating a Doppelganger app

The main Doppelganger class is packaged as a CommonJS module, so once it is installed you can use require() to use it in another module:

var Doppelganger = require('doppelganger');

// Instantiate the app, passing in the base HTML and the path to the Require.js config file
var appInstance = new Doppelganger(fs.readFileSync('index.html', 'utf8'), 'js/config.js');

// Initialise the app instance, passing a callback that is invoked when initialisation is complete 
appInstance.init(function() { console.log('App initialised'); });

Checking whether a route exists within a Doppelganger app instance

var routeExists = appInstance.routeExists(route);
appInstance.navigate(route);

Getting a Doppelganger app instance's current DOM state as HTML

var html = appInstance.getHTML();

Interacting with the Backbone app using Require.js

// This assumes 'app' and 'router' are AMD modules defined within the Backbone app
appInstance.require(['app', 'router'], function(app, router) {
    console.log(router.routes);
    app.setUserDetails(userDetails);
});

Running multiple Doppelganger app instances simultaneously

var mainAppInstance = new Doppelganger(fs.readFileSync('main/index.html', 'utf8'), 'main/js/config.js', 'main');
var faqAppInstance = new Doppelganger(fs.readFileSync('faq/index.html', 'utf8'), 'faq/js/config.js', 'faq');

mainAppInstance.init(function() {
    console.log("Main app initialised");
    console.log(mainAppInstance.getHTML());
);

faqAppInstance.init(function() {
    console.log("FAQ app initialised");
    console.log(faqAppInstance.getHTML());
);

Caveats to bear in mind when using Doppelganger

  • The Backbone.js app must use Require.js to load its dependencies
  • The Doppelganger constructor expects to be passed a path to a config JavaScript file that includes a sole require.config() call, in order to initialise Require.js correctly
  • Doppelganger expects Require.js paths to be set for "jquery" and "backbone" in the Require.js config file
  • The app code will not have access to any global variables:
    • Global browser variables such as window, document, and history will not be set. Avoid writing code that depends on the browser environment (although you do have access to jQuery for DOM manipulation, see below)
    • The $, _ and Backbone global variables will not be set. These should instead be accessed using Require.js (e.g. by listing "jquery" amongst a module's dependencies - this is good practice anyway).
  • When running multiple app instances simultaneously, each instance needs its own Require.js context. Be aware of the implications of using Require.js in multiversion mode.
  • JSDOM is used as the server-side DOM library
npm loves you