State of JavaScript Modules 2017

Before modules

Historically JavaScript had neither a module system, nor facilities to load sources from within the code. In order to understand what have now, let's take a retrospective look. I remember in the past a number of libraries where the code-base was split into a separate files which were concatinating during the build process. As simply as that it didn't help with module isolation, but improved maintainability by organizing the code in files of meaningful names. Isolation though could be achieved by using so-called immediately invoked function expressions (IIFE):

(function(){
var foo = "FOO";
}());
typeof  foo === undefined

It can be extended for an import:

var module = (function () {
    //private
    return {
    //public
    }
}());

Synchronous vs Asynchronous Module Systems

As you can see it's clumsy solution. The industry needed needed a standardized module system. So in 2009 a group of enthusiasts came up with a specification under name CommonJS, which describes module APIs for JavaScript outside the browser. Simply put, it implies a variable exports as namespace for any objects exported from the module and a function require to access the exported members of a module:

// module foo.js
exports.myExport = function() { return "My export!"; };

// module main.js
var func = require( "foo.js" ).myExport;
console.log( func() ); // My export!

CommonJS became popular with the advance of Node.js. As for browsers, we used to have plenty of solutions to encapsulate and load the modules. Eventually the best approach transformed to a specification known as AMD. The spec implies modules declared with define function, which return values considered as export:

// module foo.js
define( function(){
 return "foo";
});

// module main.js
define([ "./foo" ], ( foo ) => {
  console.log( foo() );
});

You likely know AMD though such implementations as RequireJS or curl.js. If comparing both approaches CommonJS has slightly simpler syntax and designed for synchronous loading and servers, while AMD is designed for asynchronous loading and browsers. AMD libraries load modules on demand when the dependency requested and that's surely an advantage. Yet, well-grained application may consist or hundreds modules. So the loader eventually needs to create hundreds of HTTP requests. If client or server doesn't support HTTP2 the application performance will be awful. So many developers prefer to bundle CommonJS modules in a single JavaScript file rather than going with AMD. In the past the most popular bundle tool was Browserfy. So we just aimed it to the entry script (the module requested from the browser with script tag) and the utility resolved all the imports/exports by following require statements recursively:

browserify main.js -o main.bundle.js

Nowadays an alternative bundler Webpack is taking over Browserfy and there good reasons for that. But will talk about it later.

What is more, I would like to point out a solution known as Rollup.js. While bundling CommonJS modules it optimizes the resulting script by eliminating unused code.


ES2015 Modules

With the advance of ES2015 we have a native modules sytem for JavaScript. It provides both the simplicity of the syntax similar to CommonJS and asynchronous loading like in AMD. Let's take a close look. We create a JavaScript file representing a module:

my-module.js

export const foo = "foo";
export function bar() { return "bar"; };

This module will be evaluated as requested. It declares a constant foo and a function bar and export both, meaning those two variables we can access from outer module.


Now we make another module that consumes the first one:

main.js

import { foo, bar } from "./my-module.js";
console.log( foo, bar() );

Here we import foo and bar from module my-module.js. Curly brackets make sense here when you think of destructuring the variables from the module pretty much the same as we do it from an object.

When requesting this script from HTML we have to specify it's module by using attribute type:

<script type="module" src="./main.js"></script>

Well, at the moment modules are decently implemented only in Google Chrome 60+. To make cross-browser we can bundle the modules with Webpack, or we can load them asynchronously with SystemJS

<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.20.17/system-production.js"></script>
<script>
  SystemJS.import( "./main.js" )
    .then(() => console.log( "modules loaded" ) )
    .catch( error => console.error( error ) );
</script>


Unlike CommonJS the exports are named, so if used var>foo and bar for export we go with the same names for import. However we still can alias the names on import:

main.js

import { foo as myFoo, bar as myBar } from "./my-module.js";
console.log( myFoo, myBar() );

I assume it's quite clear from the code: we import foo as myFoo and afterwards use the last one for referring. Moreover, we can import all available exports of a module at once in a plain object:

main.js

import * as ns from "./my-module.js";
console.log( ns.foo, ns.bar() );

Syntax for inline named exports:

  // Variable declarations:
  export var foo;
  export let foo;
  export const foo;

  // Function declarations:
  export function myFunc() {}
  export function* myGenFunc() {}

  // Class declarations:
  export class MyClass {}

If you have the only or the main object to export you can apply default export:

my-module.js

export default class Foo {
  static says(){
     return "Hello";
  }
}

We've declared here that Foo is not just an export, but default one. Now we can import it like that:

main.js

import Foo from "./my-module.js";
console.log( Foo.says() );

However we still can have inline named export next to the default one:

my-module.js

export const bar = "bar";
export default class Foo {
  static says(){
     return "Hello";
  }
}

main.js

import Foo, { bar } from "./my-module.js";
console.log( Foo.says(), bar );

It can be also achieved this way:


main.js

import { default: Foo, bar } from "./my-module.js";
console.log( Foo.says(), bar );

Syntax for default export:

    // Function declarations:
    export default function foo() {}
    export default function () {}

    // Class declarations:

    export default class Foo {}
    export default class {}

    // Expressions:

    export default foo;
    export default "Hello!";
    export default 40 + 2;
    export default (function () {});


In real-world sometime modules have no exports. For example module can subscribe for global events and perform some action during evaluation:

subscribe-listeners.js

console.log( "Doing something" );

So we request the module without importing from it:

main.js

import "subscribe-listeners.js";

ES2015 modules are also provided with loader API:

System.import( "./my-module.js" )
.then( { foo, bar } => {
    // go on
})
.catch( error => {
    ···
});

As you can see SystemJS applied this syntax, just namespaced their implementation with SystemJS to avoid collisions.


Sequence loading

Well, we know we can export and import from module to module, but how the nested modules are going to resolve in reality? Let's do a simple prototype to see it in action:

module-bar-baz.js

"use strict";
export const barbaz = `barbaz`;

module-bar.js

"use strict";
import { barbaz } from "./module-bar-baz.js";

export function bar(){
  return `bar + ${barbaz}`;
}

module-foo.js

"use strict";
export function foo(){
  return "foo";
}

main.js

"use strict";
import { foo } from "./module-foo.js";
import { bar } from "./module-bar.js";

console.log( `main + ${foo()} + ${bar()}` );

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script type="module" src="./main.js"></script>
  </head>
  <body>
    <h4>Please find the results in JavaScript Console</h4>
  </body>
</html>

Here we have a bunch of nested modules. From the HTML we load main.js, this module requests for module-foo.js and module-bar.js. When the last one evaluated it imports from module-bar-baz.js. As we run the prototype in Google Chrome in the DevTools we can see the following picture:



Hmm, the modules load exact in the order they requested. If we put this prototype in RequireJS syntax the Network pane would look almost the same:



Combining loaded and compiled modules

Above I told that for HTTP 1.x the cost for requesting a file is relatively high that is why many are bundling CommonJS modules rather than using AMD. The same concern stays true for ES2015 modules. I also mentioned that ES2015 modules can be bundled as well as CommonJS and that ES2015 includes asynchronous loader API. Then why not just combine these both approaches to get the best user response time. I mean we bundle the modules required for core user experience in the bootstrap script and then load on demand other modules as we enhance the UI progressively.

First let's see how we can bundle ES2016 modules with Webpack. So we put in a empty folder the manifest file:


package.json

{
  "name": "demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.html",
  "scripts": {
    "start": "npx http-server . -o  -c-1",
    "build": "npx webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "babel-cli": "^6.24.1",
    "babel-core": "^6.25.0",
    "babel-loader": "^7.1.1",
    "babel-preset-es2015": "^6.24.1",
    "http-server": "^0.10.0",
    "webpack": "^3.5.2"
  }
}

Here we use http-server to start static server and open the default browser for the application. Other dependencies are webpack bundler and Babel components to transpile ES2015 syntax to ES5 supported by legacy browsers. Now we can run npm to install all the dependecnies according to the manifest:


npm i

Now we create the configuration file for Webpack:

webpack.config.js

const { join } = require( "path" );
module.exports = {
    entry: "./main.js",
    output: {
      filename: '[name].bundle.js',
      chunkFilename: '[name].bundle.js',
			path: join( __dirname, "/dist/" ),
      publicPath: "/dist/"
    },

    module: {
			rules: [
        {
          test: /.js$/,
          exclude: /node_modules/,
          use: [{
            loader: "babel-loader",
            options: {
              presets: [ "es2015" ]
            }
          }]
        }
			]
		}

};

Here we set entry script location "./main.js" and specify the location and name for the bundled output script. Moreover we in the transformation ruls we provide the configration for Babel. In simple words it implies that it will run babel with pre-set of plugins named es2015 on the modules during their resolution. If we put now in the directory the modules of our example from above and run build:

npm run build

we get the compiled JavaScript in the file ./dist/main.bundle.js so we can request it from HTML:

<script src="./dist/main.bundle.js"></script>

That was statically resolved modules. What about asynchronous loading? For that we will need an extra plugin babel-plugin-syntax-dynamic-import to introduce dynamic import syntax to Babel:

npm i -D babel-plugin-syntax-dynamic-import


We modify the Webpack configuration accordingly:

webpack.config.js

module.exports = {
...
    module: {
			rules: [
        {
          test: /.js$/,
          exclude: /node_modules/,
          use: [{
            loader: "babel-loader",
            options: {
              presets: [ "es2015" ],
              plugins: [ "babel-plugin-syntax-dynamic-import" ]
            }
          }]
        }
			]
		}

};

Now we create module that we want to load on demand:

module-foo.js

export function foo(){
  return "foo";
}

In the consuming script we use import as a function that returns a Promise:

main.js

import("./module-foo" ).then(({ foo }) => {
  console.log( "module foo loaded, calling exported func", foo() );
}).catch(( e )=> {
  console.error( e );
});

As we build npm run build we observe Webpack created two new files in ./dist directory:

0.bundle.js
main.bundle.js

In order to avoid collision Webpack names dynamic modules like 0.bundle.js. But we can fix it by using so-called "magic comments":

main.js

import(/* webpackChunkName: "module-foo" */ "./module-foo" ).then(({ foo }) => {
  console.log( "module foo loaded, calling exported func", foo() );
}).catch(( e )=> {
  console.error( e );
});

Now we specified the indended name for the chunk and on the build we get it nicelly named:

module-foo.bundle.js
main.bundle.js

Now everything is ready and we just need to add HTML:

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="./dist/main.bundle.js"></script>
  </head>
  <body>
    <h4>Please find the results in JavaScript Console</h4>
  </body>
</html>

Now we start the application:

npm start

In the Console pane of DevTools we can see the following:

module foo loaded, calling exported func foo

The dynamic module resolved and the variable successfully imported from within the bundle compiled with Webpack.

If you need to load multiple module at once you can use Promise.all like:

Promise.all([
  import(/* webpackChunkName: "module-foo" */ "./module-foo" ),
  import(/* webpackChunkName: "module-bar" */ "./module-bar" )
]).then( ([ fooModule, barModule ])  => {
  console.log( "modules loaded... Exports ", fooModule.foo, barModule.bar );
}).catch(( e )=> {
  console.error( e );
});