DailyJS

Let's Make a Framework: Library Architecture

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

tutorials frameworks web lmaf

Let's Make a Framework: Library Architecture

Posted by Alex R. Young on .
Featured

tutorials frameworks web lmaf

Let's Make a Framework: Library Architecture

Posted by Alex R. Young on .

Welcome to part 1 of Let's Make a Framework, a series of posts about
building a JavaScript framework. In this part I'm going to discuss
library architectures, and lay down the design for our framework.

This series is designed to be educational, rather than create the next
big thing in JavaScript. To keep the spirit educational, I've decided to
name the framework turing.js. Why? Well, if you don't know much
about Alan Turing then you
might like to learn a bit about him. See? The framework is already
helping teach computer science history!

Required Reading

I'll refer to these frameworks in this part:

Style

If you embark on an involved open source or private project, you're
likely to work with other people. It's important to be upfront about the
goals of the project and the style of development.

These are the practices I will use to develop this framework:

  • Verbose: Variable and method names should be verbose so things are easy to find and understand
  • Portable: Browsers and console should be catered for
  • Explicit: Code should be quick to understand
  • Comments: Let's keep comment noise down. Comments should be succinct. TODO and FIXME are acceptable.
  • Simple: Keep code simple. Let's not bore readers!
  • Indentation: Two spaces
  • Semicolons: People might want to minify this library -- let's keep simicolons!
  • Quality: JsLint and reader comments!
  • Testing: Test first development for both browsers and console
  • Versioning: GitHub to the rescue

High Level Framework Structure

The first question to ask about a JavaScript framework's structure is:
how self-contained is it? In 2005 we were blown away by Ajax and the
yellow fade technique, so people flocked to libraries that made those
techniques easy. Now in 2010 we're writing server-side JavaScript and
creating sophisticated front-end behaviour. We can no-longer afford to
use frameworks that aren't careful about their namespacing.

Take a look at the current stable
prototype.js
. It
modifies the prototypes of a lot of native objects. It also provides a
lot of top-level objects. The BBC specifically designed Glow to avoid
this, and literally everything is namespaced. This feels strange if
you're used to Prototype, because Prototype attempts to simplify
browser-based JavaScript. Prototype makes complex Array manipulation
much easier cross-browser, but with Glow you need to remember to use
glow.lang.toArray and other utility methods.

The lesson here is that you trade off usability to play nice with other
frameworks. Due to the way JavaScript works though, it's possible to use
both approaches -- our library could have configuration options to
extend native objects.

This framework will be more like Glow -- this will remove a lot of
hidden magic when using it. People using it to learn JavaScript should
be able to see the difference between what browsers and CommonJS
provide.

Another interesting point about Prototype is it quickly defines
high-level structural code which it reuses internally. It defines
Object.extend and Class, then reuses these to
build fundamental features:

var Hash = Class.create(Enumerable, (function() {
  function initialize(object) {
    this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
  }

Helper Methods

MooTools, jQuery and Prototype all define helpers to reduce the effort
required to call commonly used functions:

// Prototype
function $H(object) {
  return new Hash(object);
};

// MooTools
function $H(object){
  return new Hash(object);
};

It would be nice to include quick access to helper methods, but as I
said previously where turing.js begins and ends needs to be
clear to the newcomer. Therefore, if these are to be used they should be
succinct but clear.

If you taught someone JavaScript with jQuery, would they even realise
browsers don't have \$()?

Initialisation

Most frameworks have wrappers for initialisation and metadata about the
library. MooTools and Prototype use one broadly similar approach, then
jQuery and Glow use another.

var MooTools = {
  'version': '1.2.5dev',
  'build': '%build%'
};

var Prototype = {
  Version: '<%= PROTOTYPE_VERSION %>',
  ...
}

(function( window, undefined ) {
  var jQuery = function( selector, context ) {
      // The jQuery object is actually just the init constructor 'enhanced'
      return new jQuery.fn.init( selector, context );
    },
    ...
    jquery: "@VERSION",
    ...
  }

  // Expose jQuery to the global object
  window.jQuery = window.$ = jQuery;
})(window);

Glow and jQuery both use an anonymous function, then expose themselves
by writing an attribute to window. This is the approach
I'll use for turing.js.

Modules and Plugins

jQuery, MooTools and Glow have tried hard to be modular. Let's use a
similar approach, with a file naming scheme like this:

  • turing.core.js
  • turing.functional.js

After creating a turing variable that will be exposed to
the global scope, we can define our modules on it as functions or
objects.

Let's Get Coding

I'm going to use my riot.js
library to write unit tests, because it's a simple unit testing library
that is pure JavaScript.

You might think that testing a library framework stub is pointless, but
we still need to make sure it sets things up properly in browsers and in
the console. I'm going to run my tests in
Rhino and Firefox.

The core test should check the following:

  • turing is instantiated
  • turing has properties we can read -- let's set a version number

The test code looks like this:

Riot.context('turing.core.js', function() {
  given('the turing object', function() {
    should('be global and accessible', turing).isNotNull();
    should('return a VERSION', turing.VERSION).isNotNull();
    should('be turing complete', true).isTrue();
  });
});

Putting this together, we get:

(function(global) {
  var turing = {
    VERSION: '0.0.1',
    lesson: 'Part 1: Library Architecture'
  };

  if (global.turing) {
    throw new Error('turing has already been defined');
  } else {
    global.turing = turing;
  }
})(typeof window === 'undefined' ? this : window);

Here's how it looks in Rhino:

js> load('turing.core.js');
js> print(turing.VERSION);
0.0.1
js> print(turing.lesson);
Part 1: Library Architecture

And in a browser:

>>> turing
Object { VERSION="0.0.1", more...}

Until Next Week...

I'll continue this series next Thursday. It took me three hours to
research and write this article, so I think I'm due a few beers now.

The code is on GitHub:
alexyoung/turing.js