Modular

JavaScript

In Today's Browser-Based Web Applications

Michael K Snead

msnead@paylocity.com



   !function() {
   }();

Modular JavaScript

  • Part 1: Minimalist (IIFEs)
  • Part 2: AMD (require.js)
  • Part 3: Loading at compile time
    (CJS, ES6, gulp, etc.)
  • Part 4: Why write modular JavaScript?

Minimalist Patterns

  • Existing/legacy applications
  • No frameworks/build systems
  • I don't want to change anything!

Part 1

This is the "bare minimum". Avoiding global variables and namespace collisions.

Step 1: All code is IIFE

//Wrap all my code in a closure
!function() {
    function doWork() {
    }

    $(function() {
        //Do something on DOM ready
    });

}();

Step 2: Make dependencies explicit, scoped

!function($, context) { //alias jQuery as a function argument
    function doWork() {
    }

    $(function() {
        //Do something on DOM ready
    });

}(jQuery, window);

Step 3: Expose through 'namespacing'

//You don't have to do things exactly like this, but...
//kitchen.fridge.js
!function($, kitchen) {
    kitchen.fridge = kitchen.fridge || {}; //don't clobber shared namespaces
    kitchen.fridge.storeFood = function storeFood(food) {
        //store some food
    }

    $(function() {
        //Do something on DOM ready
    });

}(jQuery, window.kitchen = window.kitchen || {});

//kitchen.stove.js
!function($, kitchen) {
    kitchen.stove = kitchen.stove || {}; //don't clobber shared namespaces
    kitchen.stove.cookFood = function cookFood(food) {
        //store some food
    }

    $(function() {
        //Do something on DOM ready
    });

}(jQuery, window.kitchen = window.kitchen || {});

Step 3.5: Alternate pattern

//You don't have to do things exactly like this, but...

//kitchen.stove.js
var kitchen = window.kitchen || {};
var kitchen.stove = (function($, kitchen) {
    function cookFood(food) {
        //cook some food
    }
    function clean() {
        //run cleaning routine
    }
    $(function() {
        //Do something on DOM ready
    });

    //revealing module pattern - everything "public" is right here
    return {
        cookFood: cookFood,
        clean: clean
    }
    
})(jQuery, kitchen);

Caveats...

No compile-time checking if dependencies are missing.

If dependencies are loaded out of order, it could break something.

Ceremony (extra horizontal spacing, wrapping code for closure).

Sharing namespaces is possible, but clashing/overwriting is still possible too.

Sharing between modules still requires a global.

Modules with AMD

  • Load modules asynchronously (AMD)
  • Works with everything before this part
  • Modules don't overwrite each other

Part 2

Use require.js

Instead of modules living on top of namespaces, each module has a name.

Module names are either explicit (define calls), or implicit (the name of the file).

Use require.js

//kitchen.stove.js - module name is implicit
define([ 'jquery' ],
function($) {
    //Your IDE will probably push this farther ->

    function cookFood(food) {
        //cook some food
    }
    function clean() {
        //run cleaning routine
    }

    $(function() {
        //do something at DOM ready
    });

    //"Revealing module pattern" ... everything "public" is right here:
    return {
        cookFood: cookFood,
        clean: clean
    };
});

//app.js - it only has a "require", so no module is exported
//         require'ing this file will execute the code and just not return anything.
require([ 'scripts/kitchen.stove' ],
function(stove) {

    stove.cookFood(somefood);
    stove.clean();
});

Pros

  • No build step needed, works purely out of the browser
  • Helpful errors when a dependency isn't found at initialization (early runtime)
  • Dependency loading order is explicit (easier than IIFE alone)

require.js

Cons

  • Production performance should use a build step
  • Require.js sometimes needs some configuration/shims for certain libraries
  • Lots of ceremony (define, require, etc.)
  • Debugging require itself isn't fun (though rare)

Also, though require.js is solid and has been used in popular frameworks, AMD is not the future of JavaScript modules.

Loading modules at compile time

Part 3

Works with IIFE's, but require.js shouldn't be used alongside these methods.

Using browserify or webpack

Browserify and webpack allow you to write modules in AMD or CJS style.

Combine with babel (ES6 transpiler) and you can even use ES6 style modules which transpile down to CJS.

These tools use static analysis to determine which dependencies are required.

Writing modules using CommonJS

//This is the same as writing a node.js application

//baz.js
var foo = require('./foo.js'); //paths are relative to the filesystem, not the routing module

if(needsBar) {
    //bar will already be loaded into memory prior to this
    //(also, hoisting, ya'll!)
    var bar = require('libraries/bar'); //assumes .js extension
}

//This isn't global, because all modules will be loaded in their own function scope
function bazzify() {
    //baz some stuff
}

//Module names are implicit
module.exports = {
    bazzify: bazzify
};

//app.js
var bazz = require('./baz.js');
bazz.bazzify();

Writing modules using ES6

//This is the same as writing a node.js application

//baz.js
//es6 says all 'import' statements must appear at top level!
import foo from './foo.js';
import bar from 'libraries/bar'; //assumes .js extension

function bazzify() {
    //baz some stuff
}

//we have options here. We could export just the function.
//I chose to export an object that has a function.
export default {
    bazzify: bazzify
}

//app.js
import bazz from './baz';

bazz.bazzify();

Also, babel + browserify/etc. will happily use both require() and ES6 simultaneously, but I suggest you stick with one.

Using browserify/webpack

There are modules/methods to break up your app into separate packages/bundles (ie: vendor vs. app code).

 

Similarly you can load some dependencies asynchronously if you really want/need to, but it wont enjoy compile-time checking. The most basic way to do this is through jQuery's getScript() or a library like $script.js.

Pros

  • No ceremony
  • Helpful errors at compile time
  • Relative to filesystem instead of routing (portable)
  • Can leverage other tasks as part of build step (ie: linting, unit tests)
  • Can use existing libraries (AMD, bower, npm)

CommonJS

Cons

  • Build step required - must use gulp/npm
  • CommonJS isn't ES6
  • Some configuration may be needed - ie: break up bundles between vendor/app

Can build-on-save just like TypeScript/CoffeeScript using gulp. Gulp integration through Task Runner Explorer (2013), OOB in 2015

Pros

  • Future of JavaScript + Browsers
  • Gives other ES6 benefits like arrow functions, destructuring, class, spread, array methods, generators, enhanced literals, for..of, default params, etc.
  • Can use any modules CJS can use (import === require)

ES6 (via babel)

Cons

  • Same as CommonJS

Why would I write modular JavaScript?

  • Improves reusability both within and outside of a project. "Calculator" module can be used outside of the "Accounting" app.
  • Improves testability. Can mock dependencies, test individual units.
  • Improves readability/maintainability.
    IE: In C# the opposite of this would be 1000+ line class files, or class files with multiple classes defined in the same file.
  • Allows to separate concerns and responsibilities, allows for encapsulation.

Thank you!

Modular JavaScript

By Michael Snead

Modular JavaScript

  • 1,733