Writing Modular JavaScript With AMD, CommonJS & ES Harmony

When we say an application is modular, we generally mean it's composed of a set of highly decoupled, distinct pieces of functionality stored in modules. As you probably know, loose coupling facilitates easier maintainability of apps by removing dependencies where possible. When this is implemented efficiently, its quite easy to see how changes to one part of a system may affect another.

Unlike some more traditional programming languages however, the current iteration of JavaScript (ECMA-262) doesn't provide developers with the means to import such modules of code in a clean, organized manner. It's one of the concerns with specifications that haven't required great thought until more recent years where the need for more organized JavaScript applications became apparent.

Instead, developers at present are left to fall back on variations of the module or object literal patterns. With many of these, module scripts are strung together in the DOM with namespaces being described by a single global object where it's still possible to incur naming collisions in your architecture. There's also no clean way to handle dependency management without some manual effort or third party tools.

Whilst native solutions to these problems will be arriving in ES Harmony, the good news is that writing modular JavaScript has never been easier and you can start doing it today.

In this article, we're going to look at three formats for writing modular JavaScript: AMD,CommonJS and proposals for the next version of JavaScript, Harmony.

It's difficult to discuss AMD and CommonJS modules without talking about the elephant in the room - script loaders. At present, script loading is a means to a goal, that goal being modular JavaScript that can be used in applications today - for this, use of a compatible script loader is unfortunately necessary. In order to get the most out of this article, I recommend gaining a basic understanding of how popular script loading tools work so the explanations of module formats make sense in context.

There are a number of great loaders for handling module loading in the AMD and CJS formats, but my personal preferences are RequireJS and curl.js. Complete tutorials on these tools are outside the scope of this article, but I can recommend reading John Hann's post about curl.js and James Burke's RequireJS API documentation for more.

From a production perspective, the use of optimization tools (like the RequireJS optimizer) to concatenate scripts is recommended for deployment when working with such modules. Interestingly, with the Almond AMD shim, RequireJS doesn't need to be rolled in the deployed site and what you might consider a script loader can be easily shifted outside of development.

That said, James Burke would probably say that being able to dynamically load scripts after page load still has its use cases and RequireJS can assist with this too. With these notes in mind, let's get started.

The overall goal for the AMD (Asynchronous Module Definition) format is to provide a solution for modular JavaScript that developers can use today. It was born out of Dojo's real world experience using XHR+eval and proponents of this format wanted to avoid any future solutions suffering from the weaknesses of those in the past.

The AMD module format itself is a proposal for defining modules where both the module and dependencies can be asynchronously loaded. It has a number of distinct advantages including being both asynchronous and highly flexible by nature which removes the tight coupling one might commonly find between code and module identity. Many developers enjoy using it and one could consider it a reliable stepping stone towards the module systemproposed for ES Harmony.

AMD began as a draft specification for a module format on the CommonJS list but as it wasn't able to reach full concensus, further development of the format moved to the amdjsgroup.

Today it's embraced by projects including Dojo (1.7), MooTools (2.0), Firebug (1.8) and even jQuery (1.7). Although the term CommonJS AMD format has been seen in the wild on occasion, it's best to refer to it as just AMD or Async Module support as not all participants on the CJS list wished to pursue it.

Note: There was a time when the proposal was referred to as Modules Transport/C, however as the spec wasn't geared for transporting existing CJS modules, but rather, for defining modules it made more sense to opt for the AMD naming convention.

Getting Started With Modules

The two key concepts you need to be aware of here are the idea of a define method for facilitating module definition and a require method for handling dependency loading.define is used to define named or unnamed modules based on the proposal using the following signature:

 
 
   
   
   
   
  1. define(
  2. module_id /*optional*/,
  3. [dependencies] /*optional*/,
  4. definition function /*function for instantiating the module or object*/
  5. );

As you can tell by the inline comments, the module_id is an optional argument which is typically only required when non-AMD concatenation tools are being used (there may be some other edge cases where it's useful too). When this argument is left out, we call the module anonymous.

When working with anonymous modules, the idea of a module's identity is DRY, making it trivial to avoid duplication of filenames and code. Because the code is more portable, it can be easily moved to other locations (or around the file-system) without needing to alter the code itself or change its ID. The module_id is equivalent to folder paths in simple packages and when not used in packages. Developers can also run the same code on multiple environments just by using an AMD optimizer that works with a CommonJS environment such as r.js.

Back to the define signature, the dependencies argument represents an array of dependencies which are required by the module you are defining and the third argument ('definition function') is a function that's executed to instantiate your module. A barebone module could be defined as follows:

Understanding AMD: define()
 
 
   
   
   
   
  1.  
  2. // A module_id (myModule) is used here for demonstration purposes only
  3.  
  4. define('myModule',
  5. ['foo', 'bar'],
  6. // module definition function
  7. // dependencies (foo and bar) are mapped to function parameters
  8. function ( foo, bar ) {
  9. // return a value that defines the module export
  10. // (i.e the functionality we want to expose for consumption)
  11. // create your module here
  12. var myModule = {
  13. doStuff:function(){
  14. console.log('Yay! Stuff');
  15. }
  16. }
  17.  
  18. return myModule;
  19. });
  20.  
  21. // An alternative example could be..
  22. define('myModule',
  23. ['math', 'graph'],
  24. function ( math, graph ) {
  25.  
  26. // Note that this is a slightly different pattern
  27. // With AMD, it's possible to define modules in a few
  28. // different ways due as it's relatively flexible with
  29. // certain aspects of the syntax
  30. return {
  31. plot: function(x, y){
  32. return graph.drawPie(math.randomGrid(x,y));
  33. }
  34. }
  35. };
  36. });

require on the other hand is typically used to load code in a top-level JavaScript file or within a module should you wish to dynamically fetch dependencies. An example of its usage is:

Understanding AMD: require()
 
 
   
   
   
   
  1. // Consider 'foo' and 'bar' are two external modules
  2. // In this example, the 'exports' from the two modules loaded are passed as
  3. // function arguments to the callback (foo and bar)
  4. // so that they can similarly be accessed
  5.  
  6. require(['foo', 'bar'], function ( foo, bar ) {
  7. // rest of your code here
  8. foo.doSomething();
  9. });
Dynamically-loaded Dependencies
 
 
   
   
   
   
  1.  
  2. define(function ( require ) {
  3. var isReady = false, foobar;
  4.  
  5. // note the inline require within our module definition
  6. require(['foo', 'bar'], function (foo, bar) {
  7. isReady = true;
  8. foobar = foo() + bar();
  9. });
  10.  
  11. // we can still return a module
  12. return {
  13. isReady: isReady,
  14. foobar: foobar
  15. };
  16. });
  17.  
Understanding AMD: plugins

The following is an example of defining an AMD-compatible plugin:

 
 
   
   
   
   
  1. // With AMD, it's possible to load in assets of almost any kind
  2. // including text-files and HTML. This enables us to have template
  3. // dependencies which can be used to skin components either on
  4. // page-load or dynamically.
  5.  
  6. define(['./templates', 'text!./template.md','css!./template.css'],
  7. function( templates, template ){
  8. console.log(templates);
  9. // do some fun template stuff here.
  10. }
  11. });

Note: Although css! is included for loading CSS dependencies in the above example, it's important to remember that this approach has some caveats such as it not being fully possible to establish when the CSS is fully loaded. Depending on how you approach your build, it may also result in CSS being included as a dependency in the optimized file, so use CSS as a loaded dependency in such cases with caution.

Loading AMD Modules Using require.js
 
 
   
   
   
   
  1. require(['app/myModule'],
  2. function( myModule ){
  3. // start the main module which in-turn
  4. // loads other modules
  5. var module = new myModule();
  6. module.doStuff();
  7. });
Loading AMD Modules Using curl.js
 
 
   
   
   
   
  1. curl(['app/myModule.js'],
  2. function( myModule ){
  3. // start the main module which in-turn
  4. // loads other modules
  5. var module = new myModule();
  6. module.doStuff();
  7. });
Modules With Deferred Dependencies
 
 
   
   
   
   
  1. // This could be compatible with jQuery's Deferred implementation,
  2. // futures.js (slightly different syntax) or any one of a number
  3. // of other implementations
  4. define(['lib/Deferred'], function( Deferred ){
  5. var defer = new Deferred();
  6. require(['lib/templates/?index.html','lib/data/?stats'],
  7. function( template, data ){
  8. defer.resolve({ template: template, data:data });
  9. }
  10. );
  11. return defer.promise();
  12. });
Why Is AMD A Better Choice For Writing Modular JavaScript?
  • Provides a clear proposal for how to approach defining flexible modules.
  • Significantly cleaner than the present global namespace and 

你可能感兴趣的:(javascript)