Improving Client-Side Modularity

24 Nov 2011 | By Alex Young | Tags frameworks tutorials lmaf testing

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.

When I started writing Turing I said I wanted to keep the modules decoupled so parts of the framework could be used in isolation. Most popular web frameworks package a large chunk of core functionality that includes support for everything from a selector engine to event handling and ajax.

I think it’s fair to say that this convention has now changed, and people want to be able to cherry pick parts of a framework alongside other libraries. And what’s more, this can’t just be addressed by a build process because some projects might load scripts with a RequireJS style library, or by concatenating and minifying all of their client-side JavaScript using a server-side framework.

With that in mind, can Turing truly be considered modular? If it is indeed modular, then this should be possible:

<!DOCTYPE html>
<html>
  <head><title>Let's Make a Framework: Modularity</title></head>
  <body>
    <div id="test"><p>Test Content</p></div>

    <script src="turing-test/lib/assert.js" type="text/javascript"></script>
    <script src="../turing.core.js" type="text/javascript"></script>
    <script src="../turing.dom.js" type="text/javascript"></script>
    <script type="text/javascript">
assert.equal(turing('#test p').html(), 'Test Content');

turing('#test')
  .css({ backgroundColor: 'green', color: 'white' })
  .find('p')
  .html('Test Passed');
    </script>
  </body>
</html>

I tried this little test and it worked. Each Turing module is split into Immediately-Invoked Function Expressions (IIFE) that depend on the existence of a ‘turing’ global variable:

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

// turing.dom.js
(function() {
  var dom = {};

  turing.dom = dom;
}());

This pattern isn’t ideal however, it’s simply what I used to keep the initial tutorials focused on their specific subjects. A better solution would be to embrace the Asynchronous Module Definition (AMD). Rather than trying to deal with CommonJS modules in browsers, or by using a framework-specific solution, embracing AMD yields several advantages.

Supporting AMD

I’ve already demonstrated how client-side projects support CommonJS, and supporting AMD projects is very similar. Some well-known projects already support it, and the RequireJS site has a great post with a list of these projects here: Why AMD?.

jQuery does it like this:

(function( jQuery ) {

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

if ( typeof define === "function" && define.amd && define.amd.jQuery ) {
	define( "jquery", [], function () { return jQuery; } );
}

})( jQuery );

Let’s break that down to see how it works. If define exists, then set up a module named “jquery” with no dependencies. AMD is based around the module pattern, so a function is used as define‘s third parameter that returns the jQuery function. I’ve edited the original file for the purpose of publication, but the original (exports.js) has a lengthy comment explaining just about everything here. For example, the module is registered as “jquery” because AMD derives module names from file names, and jQuery’s file name is usually lowercase.

Conclusion

From a client-side developer’s perspective, AMD is a more friendly way to manage JavaScript modules when compared to CommonJS Modules. It helps us to avoid creating unnecessary global variables, and helps support client-side script loading libraries.

It may even be worth considering structuring entire frameworks around it, so modularity goes right into the core of the project. As the interest grows in projects like jquip, it seems obvious that there is a need for more granular client-side frameworks, and AMD gives us the tools to make that happen in an open and reusable way. If you take a look at Dojo source, you’ll see that it already works this way. This was documented in Asynchronous Modules Come to Dojo 1.6, which explains AMD’s implications for a large project like Dojo.

References


blog comments powered by Disqus