Let's Make a Framework: Aliasing and Packaging

13 May 2010 | By Alex Young | Tags frameworks tutorials jake narwhal lmaf

Welcome to part 12 of Let’s Make a Framework, the ongoing series about building a JavaScript framework. This part discusses aliasing and packaging.

If you haven’t been following along, these articles are tagged with lmaf. The project we’re creating is called Turing and is available on GitHub: turing.js.

A User-Friendly API

Turing uses namespaced modules and methods, so calls look like this:

turing.dom.get('#selector');

The reason Turing is designed this way is it allows it to be modular — you don’t need to load every module, which is great for server-side work.

Lots of frameworks use a dollar sign, so we could shorten Turing to $t:

$t.dom.get('#selector');

That’s better, but get is one of those operations that will be commonly used for browser-based work. Let’s take a leaf out of jQuery’s book and accept a parameter. If the parameter is a string, it will be considered a selector:

$t('#selector');

This can be implemented using arguments:

$t = (function() {
  var alias = function() {
    return turing.dom.get(arguments[0]);
  }

  return alias;
})();

Then other parts of the framework can be referenced by the alias object:

if (turing.dom) {
  alias.dom = turing.dom;
}

if (turing.events) {
  alias.events = turing.events;
}

Enumerable Shortcuts

The turing.enumerable module contains a big set of commonly used functions. Rather than retaining the namespace, let’s copy those straight into the alias object:

if (turing.enumerable) {
  turing.enumerable.each(turing.enumerable, function(fn, method) {
    alias[method] = fn;
  });
}

Now using each is much easier: $t.each().

Configuration

BBC’s Glow framework is interesting because it can work alongside different versions of itself:

Also, as many BBC pages are built by separate development teams, we needed a library that could work alongside different versions of itself, without encountering namespace issues or CSS clashes. None of the major libraries were capable of doing this.

I like this idea, and I wish more frameworks used careful namespacing and supported concurrent use. One way to support this is to use a loading system that can load the framework and its modules. We’re not quite there yet, so I decided to allow Turing’s global alias to be changed as an experiment.

At the moment this works using an eval to fetch the framework’s alias from the configuration, but this could be changed in the future:

eval(turing.alias + ' = turing.aliasFramework()');  

In turing.core.js I set some high-level configuration options. This is where I’ve stored a reference to '$t'.

Packaging and Minification

I’ve carefully handcrafted Turing to work minified. Concatenating scripts and running a minifier is boring though, so let’s script it.

To be fully JavaScript and awesome I’m using Jake. It’s a JavaScript build tool that’s easy to work with.

The first thing you need to do is get Narwhal. Installation is pretty easy:

git clone git://github.com/280north/narwhal.git
export PATH=$PATH:~/narwhal/bin

# Check it works
narwhal narwhal/examples/hello

# If that didn't work, check that you don't have any Rhino jars kicking around

# Next, install jake and shrinksafe
tusk install jake
tusk install shrinksafe

With those tools installed we can build Turing into a single file. Jake build scripts are standard JavaScript, usually named Jakefile:

var jake = require('jake'),
    shrinksafe = require('minify/shrinksafe'),
    FILE = require('file'),
    SYSTEM = require('system')

jake.task('concat', function(t) {
  var output = '',
      files = ('turing.core.js turing.oo.js turing.enumerable.js '
              + 'turing.functional.js turing.dom.js turing.events.js turing.alias.js').split(' ')

  files.map(function(file) {
    output += FILE.read(file)
  })

  if (!FILE.exists('build')) {
    FILE.mkdir('build')
  }

  FILE.write(FILE.join('build', 'turing.js'), output)
})

jake.task('minify', function(t) {
  var shrunk = shrinksafe.compress(FILE.read(FILE.join('build', 'turing.js')))
  FILE.write(FILE.join('build', 'turing.min.js'), shrunk)
})

jake.task('build', ['concat', 'minify'])

The task at the end defines the build task which we’ll run with jake build. I’ve used the convention of capitalising libraries like FILE so they don’t clash with common variable names. I’ve hardcoded the Turing source files rather than globbing them because I want to specify the order they appear.

Running jake build will generate build/turing.js and build/turing.min.js.

Conclusion

Designing good APIs isn’t just about avoiding globals, it’s also about using concise yet clear naming conventions. Frameworks that use $() really went a long way to making JavaScript development more accessible.

Most mature projects end up with some kind of build chain. Don’t rush to add one to your project, but if you notice you’re spending a lot of time packaging things, try writing your own Jakefile. You can technically script anything with tools like Jake, just keep an eye out for repeated actions during development.

blog comments powered by Disqus