Build Better Apps with ES6 Modules

ES6 is packed with features like iterators, generators, maps, sets, symbols, template strings and more. One of the biggest game changers to app architecture is the new modules, and different than the syntactic sugar of class and extends they’re an entirely new construct[1]. In this post I’ll cover ES6’s new module syntax and how it provides better performance, encapsulation, and error handling.

But first a little backstory. Engineers have long defined modules with workarounds on the global window object using object literals, the module pattern, and any combination of IIFEs one can imagine. Each of these comes with a set of challenges from lack of encapsulation to forced singletons. In 2010 RequireJS became a popular tool to define modules client-side, being used on sites like PayPal, Dropbox and The New York Times. CommonJS, it’s counterpart, was adopted server-side by the Node.js community.

So why even use modules?


Why Modules? #

As JavaScript code scales developers often organize it into multiple source files. Makes sense, right? But if we take these files and simply load them in our HTML there are issues.

<body>
   ...

    <script src="lib/angular.js"></script>
    <script src="lib/angular-route.js"></script>
    <script src="lib/lo-dash.js"></script>
    <script src="src/A.js"></script>
    <script src="src/B.js"></script>
    <script src="src/C.js"></script>
    <script src="src/D.js"></script>
</body>

Even though modern browsers load all scripts in parallel, separate network requests are made. And these scripts must execute in sequence, where one loading script will block a subsequent script from executing.

funcobj25.png


parking.jpg

Thankfully, a better approach has arrived.

ES6 Modules #

Modules support a one-module-per-file architecture using export and import declarations to communicate dependencies. They’re fast, encapsulated, catch errors earlier, and avoid the global window object altogether.

A module can export any type of primitive or native object value, whether a string, number, boolean, object, array, function or more. A single module can import/export one primary value, called the default, along with multiple values called named imports/exports. We’ll focus on default first.

Default Imports/Exports #

ES6 prefers default imports/exports over named by giving them the most concise syntax.

Export

Examples
Export Name Module Requested Import Name Local Variable
export default function func() {...} “default”     “func”
export default function() {...} “default”     “default”
export default 42 “default”     “default”

[3]

The default keyword creates the default export. Easy, right? Remember though that each file can have only one default export.

Import

Example
Module Requested Import Name Local Variable Name
import myDefault from "mod"; “mod” “default” “myDefault”

The default exported value from mod.js is imported as local variable myDefault.


Named Imports/Exports #

Named import/export syntax is slightly more verbose. We either export variables inline, use {}, or *.

Export

Examples
Export Name Module Requested Import Name Local Variable
export var num = 100000 “num”     “num”
export { str } “str”     “str”
export { str as myStr } “myStr”     “str”
export * from "someMod"   “someMod” “*”  

1st: Exports declaration var num = 100000 as a named export.

2nd: Curly braces export a previously declared variable. str was declared earlier as a var, function, class, or let.

3rd: Same as 2nd except str variable is renamed as export myStr.

4th: Re-exports all named exports from someMod.js.

Import

Curly braces { } or an * will import a named export.

Example
Module Requested Import Name Local Variable
import { num } from "mod"; “mod” “num” “num”
import { num as myNum } from "mod"; “mod” “num” “myNum”
import * as myObj from "mod"; “mod” “*” “myObj”

1st: The num export from mod.js is imported to local variable num.

2nd: Same as 1st except the local variable myNum references named export num.

3rd: The * imports all named exports from module mod.js into a single object literal called myObj

More examples

myMod.js

var str = 'booyah';
var num = 100000;

// named exports
export str;
export num;
export function func() {
     return 'do stuff';
}
// OR -- shorthand for named exports
// export { str, num, func }

// default export
export default function() {
    return 'default';
}

main.js

// import 2 named exports
import { str, num } from "myMod"

// import default
import myDefault from "myMod"

// OR -- shorthand for default and named exports
// import myDefault, { str, num } from "myMod"

Note named export func wasn’t imported into main.js.

Beyond the syntax, ES6 modules were designed with higher level goals.


1. Declarative Structure #

Module definitions use declarative syntax which is a bit foreign at first but becomes quite readable.

/** 
*    import named exports myNum, myObj from mod.js 
*/
import { myNum, myObj } from 'mod';

Notice there’s no assignment. Most importantly, this simple syntax sets the stage for compile-time resolution.


2. Compile-Time Resolution #

When a page loads:

  1. JS text is parsed, and the “import”, and “export” syntax is found.
  2. Any dependencies are fetched and parsed.
  3. Once the dependency tree has been fetched the ES module loader maps all dependency exports to the module’s imports. [4]

All this occurs at compile-time, before any runtime code executes. Benefits include:


3. Collections of Code #

ES6 modules are defined broader than objects with properties. They’re instead “collections of code” where both default and named exports can be exported. But by allowing pieces of modules to be imported critics think it defies the essence of a module as a single unit of functionality. Do they merely pave the way for bags-of-methods modules?

Isaac Schlueter, who created NPM, echoed this concern.

If the author wants [the module] to export multiple things, then let them take on the burden of creating an object and deciding what goes on it. The syntax should suggest that a “module” ought to be a single “thing”, so that these util-bags appear as warty as they are. [2]

Dave Herman, the lead architect of ES6 modules replied.

If you want to export multiple functions, for example, you can export a single object with multiple function properties. But an object is a dynamic value. There’s no way to say create a compile-time aggregation of declarative things analogous to an object. Such as a collection of macros, or a collection of type definitions, or a collection of sub-modules that themselves contain static things. It’s one thing to encourage single-export modules, it’s another to require it. [2]

At least ES6 favors default imports/exports, giving them the best syntax compared to named exports.


4. Cyclic Dependancies & Async Loading #

Modules support cyclic dependencies, allowing module A to import module B while module B imports module A. Cyclic dependancies are usually a hallmark of poor design. But they may be useful in edge cases. For example, tree structures can use cyclic dependancies when child nodes refer to their parents (e.g. the DOM).

Browsers will likely be adding a Loader with .get() functionality for asynch loading but it’s only in a draft phase[5].

Modules are one of the best features of ES6. Despite the clunky syntax they provide better performance, encapsulation and error handling, while setting the stage for greater things like macros and types to come. I’d enjoy any questions or comments. Please feel free to reach out on
LinkedIn
.

References #

[1] ES6 Interview with David Herman

[2] Static Module Resolution by David Herman

[3] ECMAScript 6 spec, 15.2 Modules

[4] ES Modules Suggestions For Improvement

[5] Loader - A Collection of Interesting Ideas


 
135
Kudos
 
135
Kudos

Now read this

Events, Concurrency and JavaScript

Modern web apps are inherently event-driven yet much of the browser internals for triggering, executing, and handling events can seem as black box. Browsers model asynchronous I/O thru events and callbacks, enabling users to press keys... Continue →