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:
- The server checks through the routes that are registered within the Backbone app instance
- 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) - This causes the server-side app instance to update its DOM hierarchy accordingly
- The server gets an HTML version of the app instance's updated DOM hierarchy
- 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 = ; // Instantiate the app, passing in the base HTML and the path to the Require.js config filevar appInstance = fs 'js/config.js'; // Initialise the app instance, passing a callback that is invoked when initialisation is complete appInstance;
Checking whether a route exists within a Doppelganger app instance
var routeExists = appInstance;
Navigating to a route within a Doppelganger app instance
appInstance;
Getting a Doppelganger app instance's current DOM state as HTML
var html = appInstance;
Interacting with the Backbone app using Require.js
// This assumes 'app' and 'router' are AMD modules defined within the Backbone appappInstance;
Running multiple Doppelganger app instances simultaneously
var mainAppInstance = fs 'main/js/config.js' 'main';var faqAppInstance = fs 'faq/js/config.js' 'faq'; mainAppInstance
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
, andhistory
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
$
,_
andBackbone
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).
- Global browser variables such as
- 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