Modular

Javascript:

Today




September, 2013
Espen Hovlandsdal

Who?

Espen Hovlandsdal

What?

Modular JS.
"Modularity is the degree to which a system's components may be separated and recombined."

- Wikipedia

What does this mean for JS?


Stop doing this:


$(document).ready(function(){
    $("ul.menu li span").click(function() {
        $(this).parent().find("ul.level2").slideToggle('slow').show();
    });
    
    startAutoRefresh();
    document.cookie = 'hasvisited=1';
    $('.ad').each(function() {
        OAS_AD($(this).attr('data-pos'));
    });

    $.getJSON('some.thing', function(res) {
        $('.random-dom').text(res.numDays);
    });
    
    // [...]
});

Instead; write modules.


or...

  • Components
  • Packages
  • Classes
Write programs that do one thing and do it well. Write programs to work together.
- Doug McIlroy



Write modules that do one thing and do it well. Write modules to work together.
- Me

Benefits:

  • Reuse of code!
  • Separation of concerns
  • Easier testing
  • Packaging and distribution
  • Maintainability

Separation of concerns


Web developers (should) already know this!

  • HTML
  • CSS
  • Javascript

Is this the best we can do?

// app-specific code...
if (parseInt(document.cookie.replace(/(?:(?:^|.*;\s*)numVisits\s*\=\s*([^;]*).*$)|^.*$/, "$1"), 10) == 0) {
    // first visit
    $('.annoying-we-have-app-dialog')
        .removeClass('hidden')
        .addClass('annoying-slide-in-effect');
        
    document.cookie = 'numVisits=1; path=/; domain=.your-domain.com';
}

// More app-specific code


Does every app need solve cookie-reading?

... and implement the same (or different) bugs?

NO.


Write it once.
Test it.
Reuse it.

Better?


var Cookies   = require('cookie-module')
  , appPrompt = require('app-prompt');

var numVisits = Cookies.get('numVisits');
if (numVisits == 0) { // First visit
    appPrompt.show();
}

Cookies.set('numVisits', ++numVisits, {
    path: '/',
    domain: '.your-domain.com'
});

Test it!

With modularity comes ease of testing.
var Cookie = {
    set: function(name, value) {
        document.cookie = name + '=' + escape(value);
    },
    get: function(name) {
        var regex = new RegExp(
            "(?:(?:^|.*;\\s*)" + name
            + "\\s*\\=\\s*([^;]*).*$)|^.*$"
        );
        return unescape(document.cookie.replace(regex, '$1'));
    }
};

Cookie.set('foo', 'bar');
assert.equal(Cookie.get('foo'), 'bar');

Distribution

  • Put a version number on that module
  • Distribute it using a package manager
    • There are plenty to choose from!


We'll get back to this.

CommonJS

A group with a goal of building up the JavaScript ecosystem for web servers, desktop, command line apps and the browser.
- CommonJS.org

AMD

(Asynchronous Module Definition)

 define(['cookies'], function(Cookies) {
    var cookieName = 'numVisits';

    return {
        getNumberOfVisits: function() {
            var visits = parseInt(Cookies.get(cookieName), 10);
            return isNaN(visits) ? 0 : visits;
        },

        isFirstVisit: function() {
            return this.getNumberOfVisits() === 0;
        },

        incrementVisitCount: function() {
            var numVisits = this.getNumberOfVisits() + 1;
            Cookies.set(cookieName, numVisits);
            return numVisits;
        }
    };
});

AMD (cont.)

  • Works in the browser today
    • Require.JS
    • curljs
  • Kinda sorta almost works in Node.JS
  • Asynchronous!
  • Widely adopted today
    • jQuery
    • (even internally, as of 1.11/2.1 betas)
  • r.js can build single-file package for production

CommonJS modules

var Cookies = require('cookies')
  , cookieName = 'numVisits';

exports.getNumberOfVisits = function() {
    var visits = parseInt(Cookies.get(cookieName), 10);
    return isNaN(visits) ? 0 : visits;
};

exports.isFirstVisit = function() {
    return this.getNumberOfVisits() === 0;
};

exports.incrementVisitCount = function() {
    var numVisits = this.getNumberOfVisits() + 1;
    Cookies.set(cookieName, numVisits);
    return numVisits;
};

CommonJS modules (cont.)

  • In Node.JS: Works flawlessly, today.
  • In the browser: Not today (well...)

  • Arguably prettier API
  • Synchronous requires

    • Build tool for CommonJS packages/apps

    • Specify entry point
    • Walks dependency graph
    • Generates single-file package

    • Adds polyfills for CommonJS/node modules
      • querystring
      • http
      • etc.

      • What about development?
        • Auto-package on change
        • Browserify modules on request
        • Always building: Build works.

      • What about debugging?
        • Sourcemaps through --debug

      Harmony Modules

      import { set as setCookie, get as getCookie } from 'cookies';
      
      export var getNumberOfVisits = function() {
          var visits = parseInt(getCookie(cookieName), 10);
          return isNaN(visits) ? 0 : visits;
      };
      
      export var isFirstVisit = function() {
          return this.getNumberOfVisits() === 0;
      };
      
      export var incrementVisitCount = function() {
          var numVisits = this.getNumberOfVisits() + 1;
          setCookie(cookieName, numVisits);
          return numVisits;
      };

      Harmony Modules (cont.)

      • Does not work in any browsers
      • Does not work in Node.JS
        • Even with --harmony
      • Is the module definition stabilized yet?
      • Give it a go, use:
        • grunt-es6-module-transpiler
        • grunt-traceur
      • Transpiles to CommonJS or AMD

      CoffeeScript? TypeScript?


      • Both can build to AMD
      • You are already building...

      UMD

      Universal Module Definition

      (function (root, factory) {
          if (typeof define === 'function' && define.amd) {
              define(['cookies'], factory);
          } else if (typeof exports === 'object') {
              module.exports = factory(require('cookies'));
          } else {
              root.VisitCounter = factory(root.Cookies);
          }
      }(this, function (Cookies) {
          var cookieName = 'numVisits';
          return {
              getNumberOfVisits: function() { /* ... */ },
              isFirstVisit: function() { /* ... */ },
              incrementVisitCount: function() { /* ... */ }
          };
      }));

      UMD (cont).

      • Pros:
        • Works in node (CommonJS)
        • Works in the browser (AMD)
      • Cons:
        • Boilerplate code (build step?)
        • Less readable
      • Many variations

      Managing modules

      • Package managers are great
      • Search before you create
      • Easy to share between projects
      • Consider open-sourcing modules
      • Lock onto specific versions
        • Avoids breaking API changes

      What's the best format?


      Well....


      • Browserify (with UMD) for non-DOM modules
        • (Arguably) easier to write node/browser
      • AMD for applications
        • Speed of development (no build step)
        • Easier to set up for multiple developers

      • Make modules, don't worry about format

        The Future™


        • ES6: Not in browsers anytime soon
        • Guess we are stuck with AMD/CommonJS
          • Is that really a bad thing?
        • Does it really bring much to the table?

        Questions?

        • Feedback?
        • Let me know!





        Modular Javascript, today (2013)

        By Espen Hovlandsdal

        Modular Javascript, today (2013)

        • 1,603