Skip to content

Commit

Permalink
Heavy refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
fgnass committed Mar 27, 2011
1 parent a8dfc21 commit 3cd3d97
Show file tree
Hide file tree
Showing 27 changed files with 11,849 additions and 2,338 deletions.
182 changes: 127 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
Server-Side DOM, express.js-style
=================================
The express-jsdom module provides an alternative approach to building web applications with [express](http://expressjs.com/).

The express-jsdom module brings the power of DOM manipulation and CSS selectors to the server.
Instead of using templates to create markup, it uses the same object model as the browser to build documents. Once the document has been assembled on the server, it is serialized and sent to the client as HTML.

Example
=======

var express = require('express'),
dom = require('express-jddom'),
app = express.createServer();
app = express.createServer(),
dom = require('express-jsdom')(app);

app.serve('/hello', function(document) {
dom.get('/hello', function(document) {
document.title = 'Hello World';
});

Expand All @@ -16,81 +18,151 @@ JQuery Support

With express-jsdom you may also use your [jQuery](http://jquery.com/) skills on the server:

dom.use(dom.jquery);

app.serve('/hello', function($) {
dom.use('jquery');
dom.get('/hello', function($) {
$('body').append('<h1>Hello world</h1>');
});

You can also execute the _same code_ on both client and server. This is especially useful for tasks like form validation. Here's an example that uses the official [jQuery validation plugin](http://docs.jquery.com/Plugins/Validation):
Seamless Event Handling
=======================

The best thing about having a server-side representation of the client's DOM is that it allows you to handle browser events on the server. The browser then opens a websocket connection which is used to keep the server and client side DOM in sync.

The server can subscribe to any client-side event. When such an event is dispatched on the client, it is forwarded to the server where it gets re-dispatched. All modifications made to the server-side DOM are captured and replayed on the client.

dom.get('/', 'relay', function($) {

$('<h1>Hello</h1>')
.appendTo('body')
.relay('click', function() {
$(this).after("<h2>world</h2>");
});

$('h2').liveRelay('click', function() {
$(this).remove();
});
});

DOM Aspects
===========

Before we go into detail with server-side event handling, let's take a look at some basic concepts. A big advantage of having a server-side DOM is that it allows you to horizontally separate crosscutting concerns. In express-jsdom this is done using _aspects_, which are similar to connect's middleware stack, as they provide common functionality that can be either globally applied or on a per-route basis.

/**
* Serve /form.html and validate it upon submit.
*/
app.serve('/form', validation, function($) {
$('form').validate();
dom.use(foo); // Global Aspect

// Route Aspects
dom.get('/', bar, baz, function(document) {
//...
});

View Aspects
============
Aspects may be defined in several ways. The simplest form of an aspect is a function with an arbitrary argument list. The arguments are populated _by name_, hence `function(window)`, `function(document)`, `function(req, window, $)`, `function($)` are all valid signatures.

The example above works by passing an _aspect_ as second parameter. Aspects are similar to connect's middleware stack, as they provide common functionality that can be either globally applied or on a per-route basis.
Commonly used groups of aspects can be passed as an array:

Here's the code of the validation aspect:
var a = [aspect1, aspect2],
b = [aspect3, aspect4],
all = [a, b];

var validation = {
dom.get('/', a, aspect3, function(){});
dom.get('/', a, b, function(){});
dom.get('/', all, function(){});

Note that also the last function argument, which usually contains the route-specific logic, is nothing else but an _inline aspect_.

Another way to define an aspect is to create an object with an _apply_ method. This is useful for more complex aspects with dependencies, or aspects that provide assets.

Aspect Dependencies
===================

Each aspect may define dependencies to other aspects. Here's an example that depends on the built-in _jquery_ aspect to set a target on all absolute links so that they are opened in a new window:

module.exports = {
depends: 'jquery',
assets: {
css: __dirname + '/assets/form.css',
js: __dirname + '/assets/jquery.validate.js', // Location of the plugin
server: true // Load the plugin on the server, too
},
onInit = function($) {
// Intercept calls to the validate() method and execute it on client and server
$.clientAndServer('validate');
apply: function($) {
$('a[href^=http]').attr('target', '_blank');
}
};

The express-jsdom module comes with a number of built-in aspects, which for example allow you to populate forms with HTTP parameters, handle client-side events (like clicks) on the server, implement server-side state saving or automatically send redirects after post request.
Multiple dependencies can be specified using an array:

Stylus & UglifyJS
=================
module.exports = {
depends: ['jquery', require('./bar'), 'foo'],
apply: function($) {
// ...
}
};

As shown in the previous example, express-jsdom also manages assets like client-side JavaScript libraries or CSS files.
Assets can be preprocessed (stylus, less, sass) and minified (uglify, cssmin). The asset manager does not only serve the files, it also handles the injection of the link/script elements into the DOM.
In an aspect is specified using a string, express-jsdom uses the directory of the file that declares the dependency to resolve the given string to an absolute path which is then loaded with `require()`.

API
===
Asset Management
================

The `app.serve()` call in the first example is actually a shortcut. We could also write:
An aspect may also define assets like client-side JavaScripts or stylesheets.

app.all('/hello', dom.serve('/hello.html', function(document) {
document.title = 'Hello World';
});
dom.use({css: 'assets/default.css'});

This will include `default.css` in all pages. The built-in asset manager does not only inject a link tag into the document's head, it also handles the serving of the referenced file. You can also use [stylus](http://learnboost.github.com/stylus/), [less](http://lesscss.org/) or [sass](http://sass-lang.com/) to preprocess the stylesheet. To do so, just give your file the appropriate extension:

The `/hello.html` parameter is the location of a HTML file (relative to the application's base directory). The file is parsed, in order to create the initial DOM. Instead of parsing an existing HTML file, you may also create a document from scratch:
dom.use({css: 'assets/default.styl'});

app.all('/hello', dom.serve(function(document) {
document.body.innerHTML = '<h1>Hello World</h1>';
});
An aspect to load jQuery UI could look like this:

module.exports = {
depends: 'jquery',
js: 'assets/jquery-ui-1.8.11.js',
css: 'assets/jquery-ui-18.11.custom.css'
};

We could also use the [Google-hosted CDN version](http://code.google.com/apis/libraries/devguide.html#jqueryUI) with a fallback to our local copy:

module.exports = {
depends: 'jquery',
js: {
file: 'assets/jquery-ui-1.8.11.js',
cdn: '//ajax.googleapis.com/ajax/libs/jqueryui/1.8.11/jquery-ui.min.js',
test: 'jQuery.ui'
},
css: 'assets/jquery-ui-18.11.custom.css'
};

Parsing HTML Documents
======================

In the previous examples the complete documents were built programmatically. Instead of building the whole DOM from scratch, you may also parse an existing HTML file.

dom.get('/', dom.parse('/home.html'), function() {})

You can also use jQuery's DOM builder functions:
This will load `<baseDir>/views/home.html`. If the file you want to load equals the route-mapping, you can also write:

dom.get('/home', dom.parse, function() {})

app.all('/hello', dom.jQuery, dom.serve(function($) {
$('body').append($('<h1>').text('Hello world'));
});
__Note:__ If the path doesn't contain a dot, _dom.parse_ will append `.html` as file extension.

You may have noticed, that the second example takes `$` as argument, whereas the first one takes `document`. In fact you may declare arbitrary arguments, including `window`, `req`, `res` or `options`, as well as all properties of the window object. Hence `jQuery` and `$` are valid argument names, as jQuery defines `window.$` and `window.jQuery`.
JQuery Event Relay
==================

Performance
===========
Let's take a closer look at the _seamless events_ example from above:

dom.get('/', 'relay', function($) {

Jsdom is often said to be slow. In fact the JavaScript execution itself is surprisingly fast. Most processing time is spent with memory allocation and garbage collection.
$('<h1>Hello</h1>')
.appendTo('body')
.relay('click', function() {
$(this).after("<h2>world</h2>");
});

A pure express-jsdom app without any 3rd party libraries (like jQuery) can handle ~260 requests/sec on a MacBook Pro. The number of requests drops down to ~50 when you throw in JQuery. It gets even worse with each jQuery plugin that is loaded.
$('h2').liveRelay('click', function() {
$(this).remove();
});

});

The built-in _relay_ aspect provides the jQuery `.relay()` plugin, which calls `.bind()` on the client to register an event handler that forwards the event to the server via a websocket.

In out example, every time the `<h1>` element is clicked, a new `<h2>` is inserted on the server. The resulting server-side DOM mutation event is captured and translated into a jQuery DOM mutation function: `$('h1').after('<h2>world</h2>')`. This operation is sent back to the client via the websocket connection where it gets executed.

The reason again is garbage collection. Libraries like Sizzle and jQuery have been designed for a single global document which is referenced everywhere throughout the code. Hence the only possibility to use these libraries is to execute the complete code in a new context for each request. Therefore the V8 engine has to create and dispose huge amounts of code blocks and closures.
There's a second plugin method called `.liveRelay()` which does a similar thing, but instead of calling [`.bind()`](http://api.jquery.com/bind/) it uese jQuery's [`.live()`](http://api.jquery.com/live/) method to register the event handler. This way clicks to all newly inserted `<h2>` element are also automatically forwarded to the server.

To solve this problem, express-jsdom ships with custom versions of Sizzle and jQuery. They have been refactored to use object instances and prototypes to separate state and logic, which greatly improves the garbage collection characteristics.

Furthermore all the browser-feature detection code was removed from jQuery, so that the same test don't have to be performed over and over again.
110 changes: 0 additions & 110 deletions example/assets/default.less

This file was deleted.

Loading

0 comments on commit 3cd3d97

Please sign in to comment.