A Deep Dive Into JavaScript Modules

by Ashutosh Sharma / @zorder

Senior Computer Scientist

(Web Platform & Authoring)

Quick Poll

Spaghetti code?

  • Need to maintain correct order of <script> tags?
  • Polluted global namespace with clashing symbols?
  • Difficulty in lazy loading scripts?

The Problem

  • Increasing code complexity
  • Developers want discrete JS files
  • Need for optimized deployment
  • Good debugging support

Modular JavaScript

  • encapsulate a piece of code into a unit
  • register its capability
  • refer to other units as dependencies

The Old Way

  • No support for modules in the language
  • Scripts are loaded via <script> tags
  • Dependencies are stated via the order of <script> tags
  • 
    <script src="jquery.js"></script>
    <script src="jquery.color-plugin.js"></script>
                                    
  • Global functions as event handlers

Avoiding Name Clashes

  • Specially named (prefixed) global functions
  • 
    
                                    
  • Employing the object literal or the classic module pattern

Object Literal Pattern


var myModule = {
    foo: function() {
        console.log("foo!");
    },
    bar: function() {
        console.log("bar!");
    }
};
//...
myModule.foo();
                        

Object Literal Pattern: Data


var myModule = {
    options: {
        name: "John Doe", 
        age: 30 
    },
    init: function(name, age) {
        this.options.name = name;
        this.options.age = age;
    },
    foo: function() {
        console.log("foo!");
    }
};
                        

Object Literal Pattern: Nesting


var Util = {
    Logging: { 
        info:  function() { ... },
        debug: function() { ... }
    },
    Data: {
        fetch: function() { ... },
        save:  function() { ... }
    }
};
Util.Data.fetch();
                        

Module Pattern

Anonymous Closures


(function() {
    // private scope!
    // private state!
})();
                        

IIFE (Immediately Invoked Function Expression)

  • The function creates a new scope with access to global variables
  • The closure provides us private state for the application's lifetime

Module Pattern

Importing Globals


(function($, document, undefined) {
    // $ refers to the jQuery global here
})(jQuery, document);
                        

    Cleaner and faster

Module Pattern

Exporting a Module


var myModule = (function() {
    var privateData = 42;
    var privateMethod = function() { ... };

    return {
        name: "John Doe",          // <= public
        foo:  function() { ... },  // <= public
        bar:  function() { ... }   // <= public
    };
})();
                        
  • Return a value from the anonymous function
  • This is the module's public interface
  • We can also maintain private state and have private methods

Object Literal And Module Patterns

Using them together


var Util = {
    Data: (function() {
        var privateData = 42;
        return {
            fetch: function() { ... },
            save:  function() { ... }
        };
    })()
};
Util.Data.fetch();
                        

Object Literal And Module Patterns

Review

  • Encapsulate pieces of code into units
  • Pollute the global namespace
  • Provide no dependency management

What can we do?

CommonJS

  • A volunteer group for standardizing JavaScript APIs
  • Write once, run everywhere
  • e.g. web servers, command-line applications, desktop applications

CommonJS

Specifications

  • Modules
  • Packages
  • Unit testing
  • Binary data
  • Text encoding
  • Console logging
  • File system
  • ...

CommonJS Modules

  • exports
  • require()
  • One module per file
  • Top-level or relative module IDs

CommonJS Modules

Module Structure


var log = require("./logger");

function foo() {
    log.write("foo!");
}

exports.foo = foo;
                        
logger.js:

exports.write = function(msg) {
    console.log(msg);
}
                        

CommonJS Modules

Exporting Constructors

person.js:

function Person(name, age) {
    this.name = name; this.age = age;

    this.sayName = function() { console.log(name); }
    this.sayAge = function() { console.log(age); }
}

exports.Person = Person;
                        
main.js:

var Person = require("./person").Person;

var john = new Person("John", 30);
john.sayName(); // => John
                        

CommonJS Modules

module


exports.write = function(msg) { console.log(msg); }
console.log(module);
                        

{ 
  id: '/Users/ashutosh/app/logger.js',
  exports: { write: [Function] },
  parent: { ... }, loaded: false, children: [],
  filename: '/Users/ashutosh/app/logger.js',
  paths: [ 
     '/Users/ashutosh/app/node_modules',
     '/Users/ashutosh/node_modules',
     '/Users/node_modules',
     '/node_modules'
  ] 
}
                        

CommonJS Modules

Support

  • Node (server-side)
  • Narwhal (server-side)
  • curl.js (in-browser)
  • SproutCore (in-browser)
  • Browserify (in-browser)
  • ...

Issues With CommonJS Modules

  • require() is a synchronous call
  • Only one module per file
  • Additional steps required for modules to work in browsers

What can we do?

Asynchronous Module Definition

  • Async loading of modules and dependencies

Asynchronous Module Definition

  • define()
  • require()

Asynchronous Module Definition

define()


define(
    module_id, /* optional */
    [ dependencies ], /* optional */
    module_definition_function
);
                        

Asynchronous Module Definition

define()


define(
    ['foo', 'bar'], 
    function(foo, bar) {
        var myModule = {
            foo: function() { /* ... */ }
        };

        return myModule;
    }
);
                        

Asynchronous Module Definition

require()


require(
    [ dependencies ],
    callback_function
);
                        

require(
    ['foo', 'bar'],
    function(foo, bar) {
        foo.process(bar());
    }
);
                        

Asynchronous Module Definition

Dynamically Loading Dependencies


define(function(require) {
    var ready = false, count = 0;
    
    require(['foo', 'bar'], function(foo, bar) {
        ready = true;
        count = foo.count() + bar.count();
    });

    return {
        ready: ready,
        getCount: function() { return count; }
    }
});
                        

Asynchronous Module Definition

AMD Plugins


define(
    ['text!../templates/entry.html', 'foo'], 
    function (template, foo) {
        // do something with the template text string.
    }
);
                        

Asynchronous Module Definition

Simplified CommonJS Wrapping


define(function(require, exports, module) {
    // Traditional CommonJS module content here
});
                        

define(function(require, exports, module) {
    var foo = require('foo'),
        bar = require('bar');

    exports.foobar = function() { foo.count() + bar.count() };
});
                        

Asynchronous Module Definition

Advantages

  • Avoids pollution of the global namespace
  • Load modules in the browser without a build process
  • Multiple modules can be bundled in a single file
  • Lazy loading of scripts is possible

Asynchronous Module Definition

Support

  • RequireJS (in-browser)
  • curl.js (in-browser)
  • RequireJS (server-side)
  • PINF (server-side)
  • ...

RequireJS

Basic Usage

index.html:

<script data-main="scripts/main" src="require.js"></script>
                        
scripts/main.js:

require(['helper/util'], function(util) {
    // ...
});
                        

RequireJS

Optimizer Tool

  • Bundle JavaScript files and modules into a single file
  • Minify the result
  • Convert CommonJS modules to the AMD format

Sharing Code Between

Servers and Browsers

  • CommonJS modules → Server-side
  • AMD modules → In-browser

Sharing Code

  • AMD-compatible Wrapper + RequireJS
    • e.g. Used by Adobe Brackets
    • 
      define(function(require, exports, module) {
          /* ... */
          exports.foo = function() { /* ... */ }
      });
                                      
  • Browserify
  • Use globals in the browser
  • 
    (function(exports) {
        /* ... */
        exports.foo = function() { /* ... */ }
    })(typeof exports !== "undefined" ? exports 
                      : this['myModule']={});
                                
  • amdefine

Sharing Code

Universal Module Definition (UMD)


(function (define) {
    define('id', function (require, exports) {
        var a = require('a');
        exports.name = value;
    });
}(typeof define === 'function' && define.amd ? define : function (id, factory) {
    if (typeof exports !== 'undefined') {
        // CommonJS
        factory(require, exports);
    } else {
        // Create a global function.
        factory(function(value) {
            return window[value];
        }, (window[id] = {}));
    }
}));
                        
  • Works in both CommonJS and AMD environments
  • Optionally calls define, if available
  • Optionally uses exports, if available

ES6 Modules

  • Declarative syntax
  • Loader API

ES6 Modules

Exporting


// circle.js
let function sq(n) { return n * n; } // private
export const PI = 3.14159
export function area(r) { return PI * r * r; }
                        

// scripts/circle.js
const PI = 3.14159
function area(r) { return PI * r * r; }

export { PI, area };
                        

export { PI as approximatePI, area };
                        

ES6 Modules

Importing


// main.js
import { area } from 'scripts/circle';
console.log(area(5));
                        

import { area, PI } from 'scripts/circle';
                        

import 'scripts/circle' as circle;
console.log(circle.area(5));
                        

import { area as areaOfCircle } from 'scripts/circle';
console.log(areaOfCircle(5));
                        

ES6 Modules

Inline Modules


module 'scripts/circle' {
    // ...
}
module 'scripts/rectangle' {
    // ...
}
module 'foo' {
    // ...
}
                        

ES6 Modules

Module Loader API


System.import(
    ['foo', 'bar'],
    function(foo, bar) { // success
        // ...
    },
    function(error) {    // failure
        // ...
    }
);
                        

System.load(
    'path/to/jquery.js',
    function() {
        // ...
    }
);
                        
##Further Reading * [Using Objects to Organize Your Code](http://rmurphey.com/blog/2009/10/15/using-objects-to-organize-your-code/) * [JavaScript Module Pattern: In-Depth](http://www.adequatelygood.com/JavaScript-Module-Pattern-In-Depth.html) * [Why Web Modules?](http://requirejs.org/docs/why.html) * [Writing Modular JavaScript With AMD, CommonJS & ES Harmony](http://addyosmani.com/writing-modular-js/) * [CommonJS Modules 1.1.1](http://wiki.commonjs.org/wiki/Modules/1.1.1) * [CommonJS Notes](http://requirejs.org/docs/commonjs.html) * [Asynchronous Module Definition](https://github.com/amdjs/amdjs-api/wiki/AMD) * [Why AMD?](http://requirejs.org/docs/whyamd.html)
##Further Reading * [Universal Module Definition (UMD)](https://github.com/umdjs/umd) * [RequireJS API](http://requirejs.org/docs/api.html) * [AMD Patterns](http://unscriptable.com/code/AMD-module-patterns/) * [Writing for Node and the Browser](http://caolanmcmahon.com/posts/writing_for_node_and_the_browser/) * [Almond AMD Loader](https://github.com/jrburke/almond) * [amdefine](https://github.com/jrburke/amdefine) * [Harmony Modules](http://wiki.ecmascript.org/doku.php?id=harmony:modules) * [ECMAScript 6 Modules](http://www.2ality.com/2013/07/es6-modules.html) * [ES6 Resources For The Curious JavaScripter](http://addyosmani.com/blog/ecmascript-6-resources-for-the-curious-javascripter/)

Q & A




@zorder