10 things about AMD

(that you should know)

Jakub Elżbieciak

Info

Software Developer @XSolve

t: @jelzbieciak

m: jelz@post.pl

Short introduction to AMD

  • Asynchronous Module Definition
  • Model API for defining JS modules and its dependencies
  • Don't confuse with require.js

Normal way of doing things



<script src="js/jquery.js"></script>
<script src="js/jquery.payment.js"></script>
<script src="js/jquery.ui.js"></script>
<script src="js/underscore.js"></script>
<script src="js/backbone.js"></script>
<script src="js/app/main-view.js"></script>
<script src="js/app/order-view.js"></script>
<script src="js/app/confirm-view.js"></script>
<script src="js/app/router.js"></script>
<script src="js/app/custom-logic.js"></script>
<script src="js/app/user-collection.js"></script>
<script src="js/startup.js"></script>

                        

Looks pretty organized, huh? :)

Problems

  • Scripts are sequentially loaded one-by-one
  • Order has to be maintained manually
  • Global object gets dirty
  • Things can be easily overridden

AMD-style module



define([
    // define module's dependencies:
    'jquery', 'underscore', 'router', 'some/custom/module'
], function (
    // give them local names:
    $, _, Router, CustomModule
) {
    // ... do someting interesting here ...

    // export some useful things outside:
    return {
        fn1: function() { y(); },
        start: function () { x(); }
    };
});

                        

Using it

Assume that previous example was saved as app.js



require(['jquery', 'app'], function ($, app) {
    $(function () {
        app.start();
    });
});

                        
  • Function passed as second argument will be called with the loaded jQuery function and our custom app object
  • Before loading app, all its dependencies will be loaded too

Gluing with the markup

Assume that previous example was saved as main.js



<script
    src="bower_components/requirejs/require.js"
    data-main="app/main.js"
></script>

                        
  • It will load the whole application (eventually)
  • require.js is a loader (that implements AMD API)
  • As you can see, I use bower for package management

Advantages

  • Modules have its privacy, dependencies are easy to fetch
  • There is no unintended module interference
  • Files are loaded asynchronously, but the order needed to fulfill dependencies is automatically handled
  • One simple build step leads into a compressed bundle (that can be grabbed with one HTTP request)
  • It works nice with modern tools (Grunt.js, bower)

Pay attention...

...to ideas and possibilities

Code is only an illustration

10 complete examples to take a look at on GitHub

1st thing:

Simplified CommonJS Wrapper

Module definition...

...that depends on one module



define(['jquery'], function ($) {
    // use $ here...
});

                        

We also need underscore and moment!



define([
    'jquery', 'underscore', 'moment'
], function (
    $, _, moment
) {
    // use $, _, moment here...
});

                        

Real-life code



define([
    'backbone', 'app', 'moment',
    'view/NavView', 'view/SwitchView',
    'view/DayView', 'view/WeekView', 'view/MonthView'
], function (
    Backbone, app, moment,
    NavView, SwitchView,
    DayView, WeekView, MonthView
) {
    // ...
});

                        
  • And it's still quite simple calendar application
  • When manipulating the list, it's not so hard to split up dependency from its right name

CommonJS Wrapper



define(function (require) {
    var _ = require('underscore'),
        $ = require('jquery'),
        moment = require('moment');
        
    var updateClock = _.throttle(function () {
        var t = moment().format('H:mm:ss');
        $('#clock2').text(t);
    }, 1000);
        
    return {
        start: function () {
            updateClock();
            setInterval(updateClock, 990);
        }
    };
});

                        

Pair variable with require() call – code gets much cleaner

2nd thing:

Cajon

Module loader

Remember module loader on the bottom of your HTML?



<script
    src="bower_components/requirejs/require.js"
    data-main="app/main.js"
></script>

                        

RequireJS is only an inplementation and can be replaced by any other inplementation. Let's use Cajon



<script
    src="node_modules/cajon/cajon.js"
    data-main="app/main.js"
></script>

                        

(as you can see, Cajon is installed using npm)

Result

What else to say – Cajon eliminates need of define() calls totally



var _ = require('underscore'),
    $ = require('jquery'),
    moment = require('moment');

var updateClock = _.throttle(function () {
    var t = moment().format('H:mm:ss');
    $('#clock').text(t);
}, 1000);

module.exports = {
    start: function () {
        updateClock();
        setInterval(updateClock, 990);
    }
};

                        

This code can be loaded and executed without any troubles

CommonJS-style

  • As a result, we get CommonJS-style modules
  • require() calls load dependencies
  • assigning to module.exports allows to expose module's API
  • Don't think node.js modules will work out of the box

3rd thing:

Circular dependencies

Alice has a cat



// alice.js file
define(['cat'], function (cat) {
    return {
        name: 'Alice',
        haveWhat: function () {
            console.log('I\'m ' + this.name);
            console.log('I have ' + cat.name);
        }
    }
});

                        

And vice versa



// cat.js file
define(['alice'], function (alice) {
    return {
        name: 'Cat George',
        haveWhat: function () {
            console.log('I\'m ' + this.name);
            console.log('I have ' + alice.name);
        }
    };
});

                        

Won't work

  • In circular dependency, one side gets undefined (to break the infinite loop)
  • To detect circular dependency, we can use madge
  • madge --format amd --image dep-graph.png .

More on madge

  • Blue – has dependencies
  • Green – has no dependencies
  • Red – has circular dependency

(graph healthier than previous one)

Wrapper to rescue (again)



define(function (require, exports, module) {
    var alice = require('alice');
    
    exports.name = 'Cat George';
    exports.haveWhat = function () {
        console.log('I\'m ' + this.name);
        console.log('I have ' + alice.name);
    };
    exports.sayWhatAliceSays = function () {
        console.log('She says:');
        alice.haveWhat();
    };
});

                        
  • When exports is used on both sides, then modules get valid references, that will be loaded, when module function returns
  • So cat module can use alice object in functions that it exports
  • It's still a circular dependency

4th thing:

Almond

When in production

  • Requesting 50 modules, ~2kB each isn't a good idea
  • Requesting one, 5MB übermodule isn't a good idea too
  • What you need is a wise build step
  • r.js -o app/build.js

Building with RequireJS



({
    baseUrl: '.',
    mainConfigFile: 'main.js',
    name: '../bower_components/requirejs/require',
    include: 'main',
    wrap: true,
    out: '../built/main.built.require.js'
})

                        
  • You'll end up with one file containing the whole application
  • It also contains XHR-related code (but you don't do AJAX, as build shrunk everything into one file)

Building with Almond



({
    baseUrl: '.',
    mainConfigFile: 'main.js',
    name: '../bower_components/almond/almond',
    include: 'main',
    wrap: true,
    out: '../built/main.built.almond.js'
})

                        
  • Almond provides minimal AMD API
  • In my cases – ~16kB smaller output files
  • It buys you a Ferrari yearly (if you're big enough ;))

5th thing:

text! plugin

On loader plugins

  • There's a standard of loader plugins
  • Loader chooses, if it should include plugin support
  • Both RequireJS and Almond include plugin support
  • Dependency that uses plugin for loading has special format: plugin!dependency

text! plugin

  • text! plugin allows to grab any text file from any accessible location.
  • Its content is passed into module function as normal string.
  • Popular plugin's application is to load templates
  • Having all your templates on the bottom of HTML, as <script> tags is messy and hard to maintain (especially when you do hundreds of atomic views)

Simple template cache



var templates = [
    require('text!tpl/layout.html'),
    require('text!tpl/content.html'),
    require('text!tpl/menu.html')
];

_.each(templates, function (markup) {
    $(markup).appendTo($('body'));
});

                        

Now, when editing, you know where to search each template

But don't forget about optimization!

6th thing:

i18n! plugin

JS internationalization done right

Define root (default) translations



// file "nls/labels.js"
define({
    root: {
        change_lang: 'Change language',
        show_message: 'Show message'
    },
    pl: true
});

                        

pl key indicates that there is also translation for another language



// in file "nls/pl/labels.js"
define({
    change_lang: 'Zmień język',
    show_message: 'Pokaż wiadomość'
});

                        

Using it

  • When you require('i18n!nls/labels'), correct translations will be loaded
  • Correct means "an intelligent guess of user's language, based on environment, falling back to default"
  • You can also set locale key in config

Toggle lang (with localStorage)

Read from localStorage when building config:



require.config({
    config: {
        i18n: {
            locale: localStorage.getItem('locale') || 'en'
        }
    }
});

                        

Read, swap, set, reload on chosen event:



$('#toggle').on('click', function () {
    var current = localStorage.getItem('locale') || 'en';
    var next = 'en' === current ? 'pl' : 'en';
    localStorage.setItem('locale', next);
    window.location.reload();
});

                        

7th thing:

jQuery integration

Look after your jQuery

You should:

  • Use CDN version of jQuery
  • Have fallback version setted up
  • Use local version in development
  • Have custom plugins loaded after loading jQuery
  • Write your plugins as AMD modules
  • Never use old-style jquery-require.js files

SIAF jQuery path config



require.config({
    paths: {
        jquery: (function () {
            var local = window.location.host.match(/localhost/);
            var paths = [
                'http://codeorigin.jquery.com/jquery-2.0.3.min',
                '../bower_components/jquery/jquery'];
            return local ? paths.reverse() : paths;
        }())
    }
});

                        

It simply handles first three points

Shim config

  • Some jQuery plugins don't have their AMD version
  • When requested, jQuery object can be still undefined, so plugin can't be correctly registered
  • Shim config allows to define dependencies for vendors, without modifying its code


require.config({
    paths: {
        jquery: '../bower_components/jquery/jquery'
        'jquery.payment': '../bower_components/jquery.payment/lib/jquery.payment'
    },
    shim: {
        'jquery.payment': ['jquery']
    }
});

                        

When comercial, use AMD

When writing open-source plugins, keep jQuery styleguide.

When writing for you or client, use AMD.



define(function (require) {
    var $ = require('jquery');
    
    $.fn.fart = function () {
        alert('fart!');
    };
});

                        

(very stupid jQuery plugin, AMD style)

8th thing:

Packages

Organize your code even better

  • Package is a bunch of modules, that serves for a common or similar purpose
  • First profit: you get simple prefix for all modules in directory, no matter where it's located
  • Second profit: you can select main file of package, that will be loaded, when package name is requested

Defining packages

When in base directory (it defines util and widget packages in corresponding subdirectories, with main files named main.js)



require.config({
    packages: ['util', 'widget']
});

                        

Package config options used



require.config({
    packages: ['widget', {
        main: 'index',
        location: '../vendor/widget'
    }]
});

                        
Calling require('widget/input') will load ../vendor/widget/input.js, calling require('widget') will load ../vendor/widget/index.js)

Index modules

  • There is a popular pattern, derived from node.js community, to define main file of package as index module
  • Index module returns object with references to all package modules that should be available for developer


define(function (require) {
    return {
        date: require('widget/date'),
        input: require('widget/input')
    };
});

                        

(keep in mind that loading index loads all its modules)

9th thing:

Bootstrap

Twitter Bootstrap, I mean

  • You installed less or sass version with bower
  • You handle compilation on your own
  • But when it comes to Bootstrap JS plugins, you use "customize" page (or just throw everything into your page)
  • But in your vendors you can find nicely splitted source files

Handle it on your own

Create package for Bootstrap



require.config({
    paths: {
        jquery: '../bower_components/jquery/jquery'
    },
    packages: [
        'bootstrap', {
            name: 'bootstrap',
            location: '../bower_components/bootstrap/js'
        }
    ],
    shim: {
        bootstrap: ['jquery']
    }
});

                        

Now it's under your fingers

Result



define([
    'bootstrap/modal', 'bootstrap/tooltip', 'bootstrap/transition'
], function () {
    // play with bootstrap
});

                        
  • You see what you use
  • You don't type too much (you can even name package Bootstrap package "b")
  • When building, with every unused plugin, you loose next kilobytes of code

10th thing:

Know alternative

browserify

  • There is a project called browserify
  • Its parser tracks dependencies of CommonJS-style modules and packs them into one bundle in build process
  • It allows to use npm modules in the browser
  • It requires build step on every change (Grunt's watch task is probably a good idea)

Sample code



var $ = require('jquery-browserify'),
    moment = require('moment');

var updateClock = function () {
    var t = moment().format('H:mm:ss');
    $('#clock').text(t);
};

updateClock();
setInterval(updateClock, 1000);

                        

Bundle with: browserify app/main.js -o bundle.js

XSolve hires!

Gliwice | Katowice | Warszawa

xsolve.pl/kariera

Thank you!

Any questions?

jelz@post.pl