JavaScript Modules

AMD, Require.js & Other Wins

Merrick Christensen

  • I work at Domo as an anti-creative, non-certified, junior developer.
  • Built lots of high profile websites and applications.
  • Open source enthusiast.
  • require-ts creator
  • require-sweet creator
  • squire creator
  • use.js contributor

What I'm Going To Talk About

What are JavaScript Modules?

Distinct pieces of loosely coupled functionality implemented in a gardened environment (a module).

What are JavaScript Modules?

They facilitate easier maintainability of applications.

What are JavaScript Modules?

Not native to JavaScript (yet), not exactly new either...

var Module = (function() {
  var private = "something";
  return function() {
    return {
      someApi: { ... }
    }
  };
})();

AMD - The Browser

  • AMD = Asynchronous Module Definition
  • Modules & dependencies are loaded asynchronously.
  • Has a plugin system for different types of modules.
  • Needs an implementation (script loader) to run.
  • Great Adoption (jQuery, MooTools, Dojo, Firebug)

Require.js

  • The AMD reference implementation.
  • Two methods: require(), define()
  • Not the only AMD Implementation.
  • See Curl.js

Initial Page

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Require.js Application</title>
  <script src="vendor/require.js"></script>
</head>
<body>

</body>
</html>

Require.js - define()

  • Used to define a module.
  • Specifies all module dependencies.
  • Optionally name your module.
  • Export your module interface.

define() API

// naming you module is optional, and in fact, you should avoid it
// naming your modules makes your code less portable
// dependencies listed in an array, same rules as require()
// mapped to arguments in the callback
define('name', ['dependency', 'bar'], function(dependency, bar) {
  // Export the interface for our module.
  return {
    win: dependency.win,
    lose: bar.lose
  };
});

define() API

define(['dependency', 'bar'], function(dependency, bar) {
  // Export the interface for our module.
  return {
    win: dependency.win,
    lose: bar.lose
  };
});

define() API

define(function() {
  // Export the interface for our module.
  return function(){
    //functions can exported to!
  };
});

app/main.js

define(function() {
  var html = '<h1>Hello World!</h1>';
  return {
    start: function() {
      document.body.innerHTML = html;
    }
  }
});
    

Require.js - require()

  • Used to load code in a top level JavaScript file.
  • An entry point of sorts.
  • Used to lazily load code.

require() API

// foo is an external module
// foo can be a path or an alias to a path using require configuration
// the exports or return of foo will be mapped 
// to the corresponding argument.
require(['foo'], function(foo) {
  foo.win()
});

require() API

// Multiple dependencies
require(['foo', 'bar'], function(foo, bar) {
  foo.win();
  bar.lose();
});

require() API

// Dynamic loading of dependencies
define(['require', 'jquery'], function(require, $) {
  // lots of awesome code

  $('#chat-start').on('click', function(){ 
    // dependencies are paths so this will grab features/chat.js
    require(['features/chat'], function(chat) {
      chat.start();
    });
  });
  return //awesome api;
});

require() API

// multiple loaders!
var reqOne = require.config({
  context: "version1",
  baseUrl: "version1"
});

main.js

require(['app/main'], function(app) {
  app.start();
});
  

index.html - Entry Point

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Require.js Application</title>
  <script src="vendor/require.js" data-main="main"></script>
</head>
<body>
</body>
</html>

Require Configuration

  • Configuration of the Require.js Loader
  • Plugin configuration.
  • Paths, shims and more.

Require Configuration - Paths

require.config({
  baseUrl: '/js',
  paths: {
    // ;-)
    'underscore': 'lodash'
  }
});

Require Configuration - Shims

require.config({
  shim: {
    'backbone': {
      // These script dependencies should be loaded before 
      // loading backbone.js
      deps: ['underscore', 'jquery'],

      //Once loaded, use the global 'Backbone' as the
      //module value.
      exports: 'Backbone'
    }
  }
});

Require Configuration - Maps

requirejs.config({
  map: {
    // When some/newmodule requires foo it gets the newer version.
    'some/newmodule': {
       'foo': 'foo1.2'
    },
    // When some/oldmodule requires foo it gets the older version.
    'some/oldmodule': {
        'foo': 'foo1.0' 
     }
  }
});

main.js

requirejs.config({
  paths: {
    'handlebars' : 'vendor/handlebars-1.0.rc.1'
  },

  shim: {
    'handlebars' : {
      exports: 'Handlebars'
    }
  }
});

require(['app/main'], function(app) {
  app.start();
});

Require errbacks

  • Errbacks give you a chance to catch module loading failure and do something else.
  • Or fetching script errors! Huge win!

Require errbacks - CDN Failure!

requirejs.config({
  paths: {
    jquery: 'http://code.jquery.com/jquery-1.8.2.js'
  }
});

require(['jquery'], function ($) {
  //Do something with $ here
}, function (err) {
  //The error has a list of modules that failed
  var failedId = err.requireModules && err.requireModules[0];
  if (failedId === 'jquery') {
    //undef is function only on the global requirejs object.
    //Use it to clear internal knowledge of jQuery. Any modules
    //that were dependent on jQuery and in the middle of loading
    //will not be loaded yet, they will wait until a valid jQuery
    //does load.
    requirejs.undef(failedId);

    //Set the path to jQuery to local path
    requirejs.config({
        paths: {
            jquery: 'local/jquery'
        }
    });

    //Try again. Note that the above require callback
    //with the "Do something with $ here" comment will
    //be called if this new attempt to load jQuery succeeds.
    require(['jquery'], function () {});
  } else {
    //Some other error. Maybe show message to the user.
  }
});

Require errbacks - CDN Failure 2!

requirejs.config({
  //  To get timely, correct error triggers in IE, force a define/shim exports check.
  enforceDefine: true,
  paths: {
    jquery: [
      'http://code.jquery.com/jquery-1.8.2.js',
      //If the CDN location fails, load from this location
      'lib/jquery'
    ]
  }
});

//Later
require(['jquery'], function ($) {
});

Graceful Module Shutdown - Restart

require(['chat'], function(Chat){
  Chat.start();
}, function(err){
  console.log('Chat broke my heart and failed me.');
  var failedId = err.requireModules && err.requireModules[0];
  requirejs.undef(failedId);
  
  // Log an error, call a service telling your developers
  // to stop being n00bs.
  require(['log'], function(log){
    log.ourDevsAreScrubs();
  });

  // Maybe apologize to user through ui?
  // Finally I hope this is merciful... round 2

  console.log('Ok time to try it again.');
  require(['chat'], function(Chat){});

});

main.js

requirejs.config({
  paths: {
    'handlebars' : 'vendor/handlebars-1.0.rc.1'
  },

  shim: {
    'handlebars' : {
      exports: 'Handlebars'
    }
  }
});

require(['app/main'], function(app) {
  app.start();
}, function(err) {
  console.log('Hey guys, we suck... :-( Time to restart!', err);

  // Restart
  require(['app/main'], function(app){
    app.start();
  });
});

app/lib/chaos.js

define(function() {
  var run = function(odds) {
    var rand = Math.random();

    if (rand > 1 - odds) {
      throw new Error('Chaos Monkey Strikes!');
    }

    console.log('Consider yourself lucky.');
  }

  return run;
});

app/main.js

define(['handlebars', 'app/lib/chaos'], function(Handlebars, chaos) {
  var template = Handlebars.compile('<h1>Hello {{name}}!</h1>');
  return {
    start: function() {
      chaos(0.2);
      document.body.innerHTML = template({ name: 'CascadiaJS' });
    }
  }
});

Using Require Plugins

  • Transparent middleware for your modules!
  • Load different kinds of assets. Assets are dependencies too! (CSS, Templates, etc)
  • Can be used to preprocess module contents or load strategies. (CoffeeScript, Non-AMD Scripts)
  • text!, css!, i18n!, cs!, json!, mdown!, jade! & lots more!

Using Require Plugins

// the <plugin>!<resource> is the syntax to use a plugin 
// plugins are just modules that implement a specific api
define(['foo!bar'], function(bar) {
  // Export the interface for our module.
});

Using Require Plugins

define(['cs!module.coffee'], function(module) {
  // module was compiled for me!
});

Using Require Plugins

define([
  'text!mytemplate.handlebars',
  'handlebars'
], function(template, handlebars){
  // template is just a string
  return handlebars.compile(template);
});

Add Text Plugin

requirejs.config({
  paths: {
    'handlebars' : 'vendor/handlebars-1.0.rc.1',
    'text' : 'vendor/text'
  },

  shim: {
    'handlebars' : {
      exports: 'Handlebars'
    }
  }
});

app/templates/app.handlebars

<h1>Hello {{name}}!</h1>

app/main.js

define([
  'handlebars',
  'app/lib/chaos',
  'text!./templates/app.handlebars'
], function(Handlebars, chaos, text) {
  var template = Handlebars.compile(text);
  return {
    start: function() {
      chaos(0.2);
      document.body.innerHTML = template({ name: 'CascadiaJS' });
    }
  }
});

Making & Admiring Require Plugins

  • We can significantly reduce boilerplate, and further separate our concerns with plugins!
  • The plugins will actually write to file in the build, this means your whole application could be a single request.

They Are Just Modules

That honor a specified API.

write: function (pluginName, name, write) { ... },
load: function (name, parentRequire, load, config) {
  // Require the intended dependency
  parentRequire([name], function (val) {
    // Add extra functionality
    val.extra = function () { alert('extra!'); };
    // resolve the dependency manually
    load(val);
  });
}
require(['plugin!module'], function (module) {
  module.extra(); // alerts 'extra!'
});

Powerful Modules

Plugins can implement a custom load strategy!

require([
  'view!NavigationView',
  'model/NavigationItems'
], function (NavigationView, NavigationItems) {
  // NavigationView is device specific, 
  // imagine swapping out views for devices!
  // Different for Android, iPhone, Browser or Whatever
  new NavigationView({
    model: new NavigationItems
  });
});

r.js - Builds & Performance

  • A CLI implemented with Node.js
  • Traces dependencies
  • Concatenate and uglify your modules for production.
  • This will name unnamed modules.
  • Plugins are supported: (text! inlined for example).
  • You can use a shim like Almond if you don’t lazily load code. Or strip define and require altogether with Closure Compiler.

Plugins are Leveraged At Build Time

There are lots of things your app doesn't need to do at runtime, do it at build time!

load: function (name, parentRequire, load, config) {
  load.asText('hbs!'+name,
    'define(["handlebars"], function (H){ '+
      'return H.template(H.precompile(' + tmplTxt + '));' +
    '})');

  parentRequire(['hbs!'+name], function (val) {
    load(val);
  });
}

Dead Code Removal

Uglify will remove dead code blocks like this, also has closure compiler support.

if (has('something') ) {
  useSomething();
}

// changes to
if (false) {
  doSomething();
}

build.js

{
  "mainConfigFile": "main.js",
  "name": "app/main",
  "out": "app/main-built.js"
}

Run The Build

r.js -o build.js

Optimize and Ship!

Other Interesting Work

thankYou();