ES6 Modules with System JS

Using ES6 modules today

With the arrival of the latest version of JavaScript, ECMAScript version 6, we have access to some very useful features, such as the ability to properly modularise our JavaScript.

Being able to use JavaScript modules is important, as it means that we can build applications (both for the browser and that run in the backend, e.g. using Node) that have a number of benefits, including:

  • being more self-contained, and less likely to cause side-effects in other code
  • less reliance on exposing and consuming global variables
  • being easier to test, as we can use dependency injection to mock our dependencies
  • better architecture of our applications
  • better access to code created by other parties

Existing solutions

Because JavaScript has not had native support for modules, a number of solutions have sprung up over the years to provide this, including:

All of these solutions are in wide use, and there's plenty of code out there that has been written to use one or more of the above solutions; particularly, there's a wealth of CommonJS modules available on NPM (more than 120,000 at the time this post was written).

The Problem

Ideally, we'd start using and creating ES6 Modules today; however it's going to be some time before all browsers implement the specification, and for those of us who have to support older browser versions such as IE8, we'd really like a solution that we can use now.

Also, although we may create new code using the ES6 modules format, we may also want to take advantage of the other modules formats, particularly CommonJS, so we can build off the efforts of others.

So, in short we want a solution that will let us use all of the major existing module solutions, along with ES6 modules, in the browsers used by our audience today.

The Solution

SystemJS is a project that meets our needs, in that it acts as a polyfill for the ES6 module specification, but also lets us make use of AMD and CommonJS modules as well as our IIFEs.

SystemJS bills itself as a universal dynamic module loader, and is designed as a wrapper around the es6-module-loader, a project which is also maintained by the SystemJS creator, Guy Bedford.

The good news is that we can make use of SystemJS and the es6-module-loader to provide wide support for all major JavaScript module formats, so that we can start developing applications now that allow us to work flexibly with existing code and third party modules.

Usage

The following is one example of how you'd make use of SystemJS, but there are plenty of others ways that it can be used; in our case we only want ES6 modules, but if you want to make use of other ES6 language features now, you'll want to use Traceur or Babel (both of which SystemJS integrates with nicely). More possibilities are outlined in an article written by Guy Bedford, the SystemJS creator, as well as on the SystemJS site itself.

Building our Bundle

For our example app, we want to create a bundle of modules — that is, a single file that can be downloaded by a browser, but which contains all of our custom modules that we expect to use in our application.

To achieve this, we make use of the SystemJS builder as follows (note that the following presumes that you're familiar with using build tools such as Grunt or Gulp):

Builder = require('systemjs-builder');

gulp.task('modules', function(cb) {

    doModules('manifest', './dist/bundle.js')
    .then(function(){
        console.log('Build of modules complete');
        cb();
    })
    .catch(function(err){
        console.error(err);
    });

});

function doModules(manifest, target) {
    return new Builder({
        baseURL: 'assets/js/'
    })
    .build(manifest, target);
}

The above shows a gulp task that uses a manifest specifying the modules to include in the bundle, like follows:

require('module1');
require('module2');

Note that each of the files in the manifest is expected to be found on disk with the .js extension.

Script Loading

In our example app, we want to be able to use window.matchMedia so we can do media queries in JavaScript, however in IE8 and IE9 this means making use of the media.match polyfill.

Once matchMedia support is available, we'll load the enquire.js wrapper for matchMedia.

And, once enquire.js is available, we can then load our app.

To do all of the above, we'll use SystemJS as a script loader, pulling in the required modules in the correct order. For example, our app.js might look like the following:

;
'use strict';

(function () {

    System.config({
        baseURL: 'assets/js/dist/'
    });

    // We need to let System know it can find module1
    // in bundle.js file.
    System.bundles['bundle'] = ['module1'];

    // Test whether we need the media.match polyfill
    // for older browsers before loading enquire.
    if (!window.matchMedia) {
        // Set up our chained dependencies: 
        // module1 <= enquire.min <= media.match.min
        System['import']('media.match.min')
        .then(function(){
            return System['import']('enquire.min');
        }).then(function(m){
            // We need to force enquire 
            // onto the window object for IE8
            window.enquire = m;

            return System['import']('module1');
        });
    } else {
        // Set up our chained dependencies: 
        // module1 <= enquire.min
        System['import']('enquire.min')
        .then(function(){
            return System['import']('module1');
        });
    }

})();

Some important points to note about the above:

  • Given we're supporting IE8 which uses ES3, we have to use System['import'] rather than System.import, as import is a reserved word in ES3
  • SystemJS uses promises, so the resolved promise will return our loaded module, and in the .then we can load the next module in the chain
  • Both the media.match polyfill and enquire.js are globals (aka IFFEs), which SystemJS can load
  • As the media.match polyfill and enquire.js are not specified as belonging to a bundle, SystemJS expects to find them in the location specified by baseURL
  • For IE8 we need to explicitly force the global onto the window object (hence window.enquire = m;), however this is not needed for other browsers

Importing Modules

Now that our app has been loaded, we can include modules as necessary. For instance, in module1:

;
'use strict';

import module2 from 'module2';

(function () {
    module2.init();
});

In module2.js:

;
var msgText = 'module2 loaded';

var module2  = {

    init: function() {
        function msg() {
            return msgText;
        }

        msg();
    }
};

export default module2;

The ES6 modules specification provides plenty of ways to expose functionality from a given module, though in the above case we take the approach of simply exposing the main object, meaning that our consuming module has access to all methods and variables defined on the object, though in the above example it won't have access to msgText.

Note that the only code we need code in our HTML is the app.js; all other modules are loaded via app.js.

Going Further

If you want to explore an app that demonstrates the above, check out this codepen.

Notes

As some of this technology is still relatively new, there are probably going to be a few wrinkles. In our experience, once we worked things out SystemJS worked well, but we did run across a strange bug in IE8, where at least some of the modules wouldn't load, but then did as soon as the developer tools were opened (and kept loading when the developer tools were closed again).

Also, we got tripped up for a little while with IE8 by a bug that's now been fixed (but not released as of writing this post).