Let's Make a Framework: Node and Modules

21 Jul 2011 | By Alex Young | Tags frameworks tutorials lmaf

Let’s Make a Framework is an ongoing series about building a JavaScript framework from the ground up.

These articles are tagged with lmaf. The project we’re creating is called Turing. Documentation is available at turingjs.com.

npm

There’s a growing collection of Node modules that started life as front-end frameworks but have found a new lease of life on the server-side through Node and npm. A good example is Underscore which includes many functional-style methods that work well in both environments.

There are different ways to make a project like Turing available to CommonJS environments. Some front-end code might work best as modules for Connect or Express — imagine HTTP middleware that automatically enhances features using server-side code (nib/stylus do this to great effect).

Turing has components that would suit distribution through npm, but other parts of the framework are clearly browser-specific. Some Node modules actually make jQuery available for conveniently querying markup using selectors. This is incredibly useful, particularly for testing or screen scraping.

That gives us three possible avenues for exploration:

  1. Allow people to require modules that provide features suitable for Node. For example: functional helpers, promises, enumerable
  2. Provide a version of the DOM code that can be used outside of a browser
  3. Look into what Connect middleware could be created

Supporting CommonJS Modules

Underscore (GitHub: documentcloud / underscore, License, npm: underscore) from DocumentCloud has a simple package.json that is used to distribute the project through npm:

{
  "name" : "underscore",
  "description" : "JavaScript's functional programming helper library.",
  "homepage" : "http://documentcloud.github.com/underscore/",
  "keywords" : ["util", "functional", "server", "client", "browser"],
  "author" : "Jeremy Ashkenas <jeremy@documentcloud.org>",
  "contributors" : [],
  "dependencies" : [],
  "repository" : {"type": "git", "url": "git://github.com/documentcloud/underscore.git"},
  "main" : "underscore.js",
  "version" : "1.1.7"
}

The main property points at the same file used by browsers. How does this work? Underscore needs to be exported as a CommonJS module:

// Export the Underscore object for **CommonJS**, with backwards-compatibility
// for the old `require()` API. If we're not in CommonJS, add `_` to the
// global object.
if (typeof module !== 'undefined' && module.exports) {
  module.exports = _;
  _._ = _;
} else {
  // Exported as a string, for Closure Compiler "advanced" mode.
  root['_'] = _;
}

Setting a value to module.exports like this will effectively make it return when using require(). Make a file called a.js:

module.exports = function() {
  return 'Hello from a';
};

Now load node and require it:

$ node
> var a = require('./a');
> a()
'Hello from a'

Backbone does things slightly differently:

var Backbone;
if (typeof exports !== 'undefined') {
  Backbone = exports;
} else {
  Backbone = root.Backbone = {};
}

The difference here is Backbone exports an Object rather than an instance of something. This is documented in Node’s Modules documentation.

Wrapping DOM Code

Supporting client-side JavaScript in an environment like Node is far from trivial. Fortunately there’s an extremely popular solution in the form of JSDOM (GitHub: tmpvar / jsdom, License, npm: jsdom) by Elijah Insua. This is a fully native JavaScript implementation of the W3C DOM.

Most projects use it by running jsdom.env over some HTML, but it doesn’t just parse HTML, it’ll load scripts from URLs or files:

var jsdom = require('jsdom');

jsdom.env(
  // Some HTML
  '<p><a class="the-link" href="http://jsdom.org>JSDOM\'s Homepage</a></p>',

  // A script to load
  ['http://code.jquery.com/jquery-1.5.min.js'],

  // A callback
  function(errors, window) {
    console.log('contents of a.the-link:', window.$('a.the-link').text());
  }
);

// contents of a.the-link: JSDOM's Homepage

Amazing, right? Are you reconsidering your career now you can apply your client-side skills straight to the server?

Using JSDOM will allow us to offer most (if not all) of Turing’s functionality in some form on the server.

Connect Middleware

Connect modules take this form:

module.exports = function myModule() {
  return function(req, res, next) {
    if (some condition) {
      next();
    } else {
      // End the request
    }
  }
};

This is a function that returns a function that accepts the standard request and response objects found in all Express (and Connect-based) apps. The next parameter is called when the request should continue on to other middleware.

What can we do with this with a client-side framework? If Turing included something like Raphaƫl it could generate bitmap versions of SVG graphics. Or maybe it could simply load the client-side JavaScript so people can use npm to manage Turing rather than manually downloading the scripts.

Conclusion

Client-side JavaScript is often suitable for use by server-side code. Also, in some cases it’s desirable to run DOM code outside of a browser, particularly for creating fast unit tests.

Next week I’ll explain how to apply some of these techniques.

References


blog comments powered by Disqus