26
Modules

WHAT'S IN THIS CHAPTER?

  • Understanding the module pattern
  • Improvising module systems
  • Working with pre-ES6 module loaders
  • Working with ES6 modules

WROX.COM DOWNLOADS FOR THIS CHAPTER

Please note that all the code examples for this chapter are available as a part of this chapter's code download on the book's website at www.wrox.com/go/projavascript4e on the Download Code tab.

Writing modern JavaScript essentially guarantees that you will be working with large codebases and using third-party resources. A consequence of these is that you will end up using code broken into different parts and connecting them together in some way.

Prior to the ECMAScript 6 module specification, there was a dire need for module-like behavior even though browsers did not natively support it. ECMAScript did not support modules in any way, so libraries and codebases that wanted to use the module pattern were required to cleverly use JavaScript constructs and lexical features to “fake” module-like behavior.

Because JavaScript is an asynchronously loaded interpreted language, the module implementations that emerged and gained widespread use took on a handful of different forms. These different forms took their shape in order to accomplish different results, but ultimately, they were all implementations of the canonical module pattern.

UNDERSTANDING THE MODULE PATTERN

Splitting code into independent pieces and connecting those pieces together can be robustly implemented with the module pattern. The central ideas for this pattern are simple: break logic into pieces that are totally encapsulated from the rest of the code, allow each piece to explicitly define what parts of itself are exposed to external pieces, and allow each piece to explicitly define what external pieces it needs to execute. There are various implementations and features that complicate these concepts, but these fundamental ideas are the foundation for all module systems in JavaScript.

Module Identifiers

Common to all module systems is the concept of module identifiers. Module systems are essentially key-value entities, where each module has an identifier that can be used to reference it. This token will sometimes be a string in cases where the module system is emulated, or it might be an actual path to a module file in cases where the module system is natively implemented.

Some module systems will allow you to explicitly declare the identity of a module, and some module systems will implicitly use the filename as the module identity token. In either case, a well-formed module system will have no module identity collisions, and any module in the system should be able to reference a different module in the system with no ambiguity.

Exactly how a module identifier is resolved to an actual module will be subject to the identifier implementation in any given module system. Native browser module identifiers must provide a path to an actual JavaScript file. In addition to file paths, NodeJS will perform a search for module matches inside the node_modules directory, and can also match an identifier to a directory containing index.js.

Module Dependencies

The real meat of module systems comes into play when considering how to manage dependencies. A module specifying a dependency is entering into a contract with the surrounding environment. The local module declares to the module system a list of external modules—“dependencies”—that it knows exist and are required for the local module to function properly. The module system inspects the dependencies and in turn guarantees that these modules will be loaded and initialized by the time the local module executes.

Each module also is associated with some unique token that can be used to retrieve the module. Frequently, this is a path to the JavaScript file, but in some module systems this can also be a namespace path string declared within the module itself.

Module Loading

The concept of “loading” a module is borne out of the requirements of a dependency contract. When an external module is specified as a dependency, the local module expects that when it is executed, the dependencies will be ready and initialized.

In the context of browsers, loading a module has several components. Loading a module involves executing the code inside it, but this cannot begin until all the dependencies are loaded and executed first. A module dependency, if it is code that has not yet been sent to the browser, must be requested and delivered over the network. Once the code payload is received by the browser, the browser must determine if the newly loaded external module has its own dependencies, and it will recursively evaluate those dependencies and load them in turn until all dependent modules are loaded. Only once the entire dependency graph is loaded can the entry module begin to execute.

Entry Points

A network of modules that depend upon each other must specify a single module as the “entry point,” where the path of execution will begin. This should make good sense, as the JavaScript execution environment is serially executed and single threaded—so the code must start somewhere. This entry point module will likely have module dependencies, and some of those dependencies will in turn have dependencies. The net effect of this is that all the modules of a modular JavaScript application will form a dependency graph.

Dependencies between modules in an application can be represented as a directed graph. Suppose the dependency graph shown in Figure 26-1 represents an imaginary application:

Block illustration depicting a network of modules, where arrows represent the flow of module dependencies: Module A depends on Module B and Module C, Module B depends on Module D and Module E, and so on.

FIGURE 26-1

Arrows represent the flow of module dependencies: Module A depends on Module B and Module C, Module B depends on Module D and Module E, and so on. Because a module cannot be loaded until its dependencies are loaded, it follows that Module A, the entry point in this imaginary application, must be executed only after the rest of the application loads.

In JavaScript, the concept of “loading” can take a number of different forms. Because modules are implemented as a file containing JavaScript code that will immediately execute, it is possible to request the individual scripts in an order that will satisfy the dependency graph. For the preceding application, the following script order would satisfy the dependency graph:

<script src="moduleE.js"></script>
<script src="moduleD.js"></script>
<script src="moduleC.js"></script>
<script src="moduleB.js"></script>
<script src="moduleA.js"></script>

Module loading is “blocking”, meaning further execution cannot continue until an operation finishes. Each module progressively loads after its script payload is delivered to the browser, with all its dependencies already loaded and initialized. This strategy has a number of performance and complexity implications, however: loading five JavaScript files sequentially to do the work of one application is not ideal, and managing the correct load order is not an easy task to do by hand.

Asynchronous Dependencies

Because JavaScript is an asynchronous language, it can also be useful to load modules on demand by allowing JavaScript code to instruct the module system to load a new module, and provide the module to a callback once it is ready. At the code level, pseudocode for this might appear as follows:

// moduleA definition
load('moduleB').then(function(moduleB) {
 moduleB.doStuff();
}); 

Module A's code is using the moduleB token to request that the module system load Module B, and invoke the callback with Module B provided as a parameter. Module B might have already been loaded, or it might have to be freshly requested and initialized, but this code does not care—those responsibilities are delegated to the module loader.

If you were to rework the previous application to use only programmatic module loading, you would only need to use a single <script> tag for Module A to load, and Module A would request module files as needed—no generating an ordered list of dependencies required. This has a number of benefits, one of which is performance, as only one file synchronously loading is required on pageload.

It would also be possible to keep these scripts separated, apply a defer or async attribute to the <script> tags, and include logic that can discern when an asynchronously script is loaded and initialized. This behavior would emulate what is implemented in the ES6 module specification, which is covered later in the chapter.

Programmatic Dependencies

Some module systems will require you to specify all dependencies at the beginning of a module, but some module systems will allow you to dynamically add dependencies inside the program structure. This is distinct from regular dependencies listed at the beginning of a module, all of which are required to load before a module can begin to execute.

The following is an example of programmatic dependency loading.

if (loadCondition) {
 require('./moduleA');
}

Inside this module, it is determined only at runtime if moduleA is loaded. The loading of moduleA might be blocking, or it might yield execution and only continue once the module is loaded. Either way, execution inside this module cannot continue until moduleA is loaded because the assumption is that moduleA's presence is critical for subsequent module behavior.

Programmatic dependencies allow for more complex dependency utilization, but at a cost—it makes static analysis of a module more difficult.

Static Analysis

The JavaScript that you build into modules and deliver to the browser is often subject to static analysis, where tools will inspect the structure of your code and reason about how it will behave without performing program execution. A module system that is friendly to static analysis will allow module bundling systems to have an easier time when figuring out how to combine your code into fewer files. It will also offer you the ability to perform intelligent autocomplete inside a smart editor.

More complicated module behavior, such as programmatic dependencies, will make static analysis more difficult. Different module systems and module loaders will offer varying levels of complexity. With respect to a module's dependencies, additional complexity will make it more difficult for tooling to predict exactly which dependencies a module will need when it executes.

Circular Dependencies

It is nearly impossible to architect a JavaScript application that is free of dependency cycles, and therefore all module systems including CommonJS, AMD, and ES6—support cyclical dependencies. In an application with dependency cycles, the order in which modules are loaded may not be what you expect. However, if you have properly structured your modules in a way that has no side-effects, the load order should not be a detriment to your overall application.

In the following module (which uses module-agnostic pseudocode), any of the modules could be used as the entry point module even though there are cycles in the dependency graph:

require('./moduleD');
require('./moduleB');

console.log('moduleA');
require('./moduleA');
require('./moduleC'); 

console.log('moduleB');
require('./moduleB');
require('./moduleD');

console.log('moduleC');

require('./moduleA');
require('./moduleC');

console.log('moduleD');

Changing the module that is used as the main module will change the dependency load order. If moduleA were to be loaded first, it would print the following, which is indicative of the absolute order of when a module load completes:

moduleB
moduleC
moduleD
moduleA

The load order can be visualized with the dependency graph in Figure 26-2, where the loader will perform a depth-first load of the dependencies:

“Block illustration depicting the absolute load order (moduleB, moduleC, moduleD, moduleA) visualized with the dependency graph, where the loader will perform a depth-first load of the dependencies.”

FIGURE 26-2

If, instead, moduleC were to be loaded first, it would print the following, which is indicative of the absolute order of module loads:

moduleD
moduleA
moduleB
moduleC

The load order can be visualized with the dependency graph in Figure 26-3, where the loader will perform a depth-first load of the dependencies:

“Block illustration depicting the absolute load order (moduleD, moduleA, moduleB, moduleC) visualized with the dependency graph, where the loader will perform a depth-first load of the dependencies.”

FIGURE 26-3

IMPROVISING MODULE SYSTEMS

In order to offer the encapsulation required by the module pattern, pre-ES6 modules sometimes used function scope and immediately invoked function expressions (IIFEs) to wrap the module definition in an anonymous closure. The module definition is executed immediately, as shown here:

(function() {
 // private Foo module code
 console.log('bar');
})();

// 'bar'

When the return value of this module was assigned to a variable, this effectively created a namespace for the module:

var Foo = (function() {
 console.log('bar');
})();

'bar'

In order to expose a public API, the module IIFE would return an object whose properties would be the public members inside the module namespace:

var Foo = (function() {
 return {
  bar: 'baz',
  baz: function() {
   console.log(this.bar);
  }
 };
})();

console.log(Foo.bar); // 'baz'
Foo.baz();    // 'baz'

A similar pattern to the previous one, termed the “Revealing module pattern,” only returns an object whose properties are references to private data and members:

var Foo = (function() {
 var bar = 'baz';
 var baz = function() {
  console.log(bar);
 };

 return {
  bar: bar,
  baz: baz
 };
})();

console.log(Foo.bar); // 'baz'
Foo.baz();    // 'baz'

It's also possible to define modules within modules, which can be useful for namespace nesting purposes:

var Foo = (function() {
 return {
  bar: 'baz'
 };
})();

Foo.baz = (function() {
 return {
  qux: function() {
   console.log('baz');
  }
 };
})();

console.log(Foo.bar); // 'baz'
Foo.baz.qux();  // 'baz'

For the module to use external values properly, they can be passed in as parameters to the IIFE:

var globalBar = 'baz';

var Foo = (function(bar) {
 return {
  bar: bar,
  baz: function() {
   console.log(bar);
  }
 };
})(globalBar);

console.log(Foo.bar); // 'baz'
Foo.baz();    // 'baz'

Because the module implementations here are simply creating an instance of JavaScript object, it's entirely possible to augment the module after its definition:

// Original Foo
var Foo = (function(bar) {
 var bar = 'baz';

 return {
  bar: bar
 };
})();

// Augment Foo
var Foo = (function(FooModule) {
 FooModule.baz = function() {
  console.log(FooModule.bar);
 }

 return FooModule;
})(Foo);

console.log(Foo.bar); // 'baz'
Foo.baz();    // 'baz'

It can also be useful to configure the module augmentation to perform the augmentation whether the module is present or not:

// Augment Foo to add alert method
var Foo = (function(FooModule) {
 FooModule.baz = function() {
  console.log(FooModule.bar);
 }

 return FooModule;
})(Foo || {});

// Augment Foo to add data
var Foo = (function(FooModule) {
 FooModule.bar = 'baz';

 return FooModule;
})(Foo || {});

console.log(Foo.bar); // 'baz'
Foo.baz();    // 'baz'

As you might suspect, designing your own module system is an interesting exercise, but is not recommended for actual use, as the result is brittle. The preceding examples do not have a good way of programmatically loading dependencies other than using the maligned eval. Dependencies must be managed and ordered manually. Asynchronous loading and circular dependencies are extremely difficult to add. Lastly, performing static analysis on such a system would be quite difficult.

WORKING WITH PRE-ES6 MODULE LOADERS

Prior to the introduction of native ES6 module support, JavaScript codebases using modules essentially wanted to use a language feature that was not available by default. Therefore, codebases would be written in a module syntax that conformed to a certain specification, and separate module tooling would serve to bridge the gap between the module syntax and the JavaScript runtime. The module syntax and bridging took a number of different forms, usually either a supplementary library in the browser or preprocessing at build time.

CommonJS

The CommonJS specification outlines a convention for module definition that uses synchronous declarative dependencies. This specification is primarily intended for module organization on the server, but it can also be used to define dependencies for modules that will be used in the browser. CommonJS module syntax will not work natively in the browser.

A CommonJS module definition will specify its dependencies using require(), and it will define its public API using an exports object. A simple module definition might appear as follows:

var moduleB = require('./moduleB');

module.exports = {
 stuff: moduleB.doStuff();
}; 

moduleA specifies its dependency on moduleB by using a relative path to the module definition. What counts as a “module definition,” and how a string references that module, is entirely up to the module system's implementation. In NodeJS for example, a module identifier might point to a single file, or it might point to a directory with an index.js file inside.

Requiring a module will load it, and assignment of the module to a variable is extremely common, but assignment to a variable is not required. Invoking require() means the module will load all the same.

console.log('moduleA');
require('./moduleA'); // "moduleA"

Modules are always singletons, irrespective of how many times a module is referenced inside require(). In the following example, moduleA will only ever be printed once because moduleA is only ever loaded a single time—even though it is required multiple times.

console.log('moduleA');
var a1 = require('./moduleA');
var a2 = require('./moduleA');

console.log(a1 === a2); // true

Modules are cached after the first time they are loaded; subsequent attempts to load a module will retrieve the cached module. Module load order is determined by the dependency graph.

console.log('moduleA');
require('./moduleA');
require('./moduleB'); // "moduleA"
require('./moduleA');

In CommonJS, module loading is a synchronous operation performed by the module system, so require() can be programmatically invoked inline in a module, as well as conditionally.

console.log('moduleA');
if (loadCondition) {
 require('./moduleA');
}

Here, moduleA will load only if loadCondition evaluates to true. The load is synchronous, so any code that precedes the if() block will execute before moduleA loads, and any code that follows the if() block will execute after moduleA loads. All the same load order rules apply, so if moduleA had been loaded previously elsewhere in the module graph, this conditional require() would only serve to allow you to use the moduleA namespace.

In these examples, the module system is implemented inside NodeJS, so ./moduleB is a relative path to a module target in the same directory as this module. NodeJS will use the module identifier string in the require() call to resolve the module reference dependency. NodeJS can use absolute or relative paths to modules, or it can also use module identifiers for dependencies installed in the node_modules directory. These details aren't germane to the subject matter of this book, but it's important to know that the module's string reference might be differently implemented inside different CommonJS implementations. However, common to all CommonJS-style implementations is that modules will not specify their identifier; it is derived from their location in the module file hierarchy.

The path to the module definition might reference a directory, or it might be a single JavaScript file—either way, this local module is unconcerned with the module implementation, and Module B is loaded into a local variable. Module A in turn defines its public interface, the foo property, on the module.exports object.

If another module wanted to use this interface, it could import the module as follows:

var moduleA = require('./moduleA');

console.log(moduleA.stuff); 

Note here that this module does not export anything. Even though it has no public interface, if the module were required in an application, it would still execute the module body on load.

The exports object is extremely flexible, and can take on multiple forms. If you are looking to export only a single entity, you are able to perform a direct assignment to module.exports:

module.exports = 'foo'; 

This way, the entire module interface is a string, which can be used as follows:

var moduleA = require('./moduleB');

console.log(moduleB); // 'foo' 

It is also very common to bundle multiple values into exports, which can either be done with an object literal assignment or a one-off property assignment:

// Equivalent:

module.exports = {
 a: 'A',
 b: 'B'
};

module.exports.a = 'A';
module.exports.b = 'B';

One of the primary uses of modules is to house class definitions (shown here using ES6-style class definition, but ES5-style class definition is also compatible):

class A {}

module.exports = A; 
var A = require('./moduleA');

var a = new A();

It is also possible to assign an instance of a class as the exported value:

class A {}

module.exports = new A(); 

Furthermore, CommonJS supports programmatic dependencies:

if (condition) {
 var A = require('./moduleA');
} 

CommonJS relies on several globals such as require and module.exports to work. For CommonJS modules to be usable in the browser, there needs to be some sort of bridge between its non-native module syntax. There also needs to be some sort of barrier between the module-level code and the browser runtime, as CommonJS code executed without encapsulation will declare global variables in the browser—behavior that is undesirable in the module pattern.

A common solution is to bundle the module files together ahead of time, convert the globals to native JavaScript constructs, encapsulate the module code inside function closures, and serve a single file. Comprehension of the dependency graph is required to bundle the modules in the correct order.

Asynchronous Module Definition

Whereas CommonJS is targeted at a server execution model—where there is no penalty for loading everything into memory at once—the Asynchronous Module Definition (AMD) system of module definition is specifically targeted at a browser execution model, where there are penalties from increased network latency. The general strategy of AMD is for modules to declare their dependencies, and the module system running in the browser will fetch the dependencies on demand and execute the module that depends on them once they have all loaded.

The core of the AMD module implementation is a function wrapper around the module definition. This prevents the declaration of global variables and allows for the loader library to control when to load the module. The function wrapper also allows for superior portability of module code because all the module code inside the function wrapper uses native JavaScript constructs. This function wrapper is an argument to the define global, which is defined by the AMD loader library implementation.

An AMD module can specify its dependencies with string identifiers, and the AMD loader will call the module factory function once all the dependent modules have loaded. Unlike CommonJS, AMD allows you to optionally specify the string identifier for your module.

// Definition for a module with id 'moduleA'. moduleA depends on moduleB, 
// which will be loaded asynchronously.
define('moduleA', ['moduleB'], function(moduleB) {
 return {
  stuff: moduleB.doStuff();
 };
});

AMD also supports the require and exports objects, which allow for construction of CommonJS-style modules inside an AMD module factory function. These are required in the same way as modules, but the AMD loader will recognize them as native AMD constructs rather than module definitions:

define('moduleA', ['require', 'exports'], function(require, exports) {
 var moduleB = require('moduleB');

 exports.stuff = moduleB.doStuff();
});

Programmatic dependencies are supported using this style:

define('moduleA', ['require'], function(require) {
 if (condition) {
  var moduleB = require('moduleB');
 }
});

Universal Module Definition

In an attempt to unify the CommonJS and AMD ecosystems, the Universal Module Definition (UMD) convention was introduced to create module code that could be used by both systems. Essentially, the pattern defines modules in a way that detects which module system is being used upon startup, configures it as appropriate, and wraps the whole thing in an immediately invoked function expression. It is an imperfect combination, but for the purposes of combining the two ecosystems it is suitable in a surprisingly large number of scenarios.

An example module with a single dependency (based on the UMD repository on GitHub) is as follows:

 (function (root, factory) {
 if (typeof define === 'function' && define.amd) {
  // AMD. Register as an anonymous module.
  define(['moduleB'], factory);
 } else if (typeof module === 'object' && module.exports) {
  // Node. Does not work with strict CommonJS, but
  // only CommonJS-like environments that support module.exports,
  // like Node.
  module.exports = factory(require(' moduleB '));
 } else {
  // Browser globals (root is window)
  root.returnExports = factory(root. moduleB);
 }
}(this, function (moduleB) {
 //use moduleB in some fashion.

 // Just return a value to define the module export.
 // This example returns an object, but the module
 // can return a function as the exported value.
 return {};
}));

There are variations on this pattern that enable support for strict CommonJS and browser globals. You should never be expected to be authoring this exact wrapper by hand—it should be automatically generated by a build tool. Your goal is to be concerned with the content of the modules, not the boilerplate that connects each of them.

Module Loader Deprecation

Ultimately, the patterns shown in this section will become increasingly obsolete as support broadens for the ECMAScript 6 module specification. That being so, it still is quite useful to know what the ES6 module specification grew out of in order to learn why design decisions were chosen. The intense conflict between CommonJS and AMD cultivated the ECMAScript 6 module specification that we now enjoy.

WORKING WITH ES6 MODULES

One of ECMAScript 6's most significant introductions was a specification for modules. The specification in many ways is simpler than its predecessor module loaders, and native browser support means that loader libraries and other preprocessing is not necessary. In many ways, ES6 module system unifies the best features of AMD and CommonJS into a single specification.

Module Tagging and Definition

ECMAScript 6 modules exist as a monolithic chunk of JavaScript. A script tag with type="module" will signal to the browser that the associated code should be executed as a module, as opposed to execution as a traditional script. Modules can be defined inline or in an external file:

<script type="module">
 // module code
</script>

<script type="module" src="path/to/myModule.js"></script>

Even though they are handled in a different way than a conventionally loaded JavaScript file, JavaScript module files do not have a special content type.

Unlike their traditional script counterparts, all modules will execute in the same order that a <script defer> would execute. Downloading a module file begins immediately after the <script type="module"> tag is parsed, but execution is delayed until the document is completely parsed. This applies to both inline modules and modules defined in external files. The order in which <script type="module"> code appears on the page is the order in which it will execute. As is the case with <script defer>, changing the location of the module tags—either in <head> or <body>—will only control when the files load, not when the modules are loaded.

The following is the order of execution for an inline module:

<!-- Executes 2nd -->
<script type="module"></script>

<!-- Executes 3rd -->
<script type="module"></script>

<!-- Executes 1st -->
<script></script>

Alternately, this can be reworked with an external JS module definition:

<!-- Executes 2nd -->
<script type="module" src="module.js"></script>

<!-- Executes 3rd -->
<script type="module" src="module.js"></script>

<!-- Executes 1st -->
<script><script>

It is also possible to add an async attribute to module tags. The effect of this is twofold: the order of module execution no longer is bound to the order of script tags on the page, and the module will not wait for the document to be finished parsing before beginning execution. The entry module must still wait for its dependencies to load.

ES6 modules associated with a <script type="module"> tag are considered to be the entry module for a module graph. There are no restrictions as to how many entry modules there can be on a page, and there is no limit to overlap of modules. No matter how many times a module is loaded in a page, irrespective of how that load occurs, it will only ever load a single time, as demonstrated here:

<!-- moduleA will only load a single time on this page -->

<script type="module">
 import './moduleA.js'
<script>
<script type="module">
 import './moduleA.js'
<script>
<script type="module" src="./moduleA.js"></script>
<script type="module" src="./moduleA.js"></script>

Modules defined inline cannot be loaded into other modules using import. Only modules loaded from an external file can be loaded using import. Therefore, inline modules are only useful as an entry point module.

Module Loading

ECMAScript 6 modules are unique in their ability to be loaded both natively by the browser as well as in conjunction with third-party loaders and build tools. Some browsers still do not natively support ES6 modules, so third-party tooling may be required. In many cases, third-party tooling may, in fact, be more desirable.

A browser that offers full ECMAScript 6 module support will be able to load the entire dependency graph from a top-level module, and it will do so asynchronously. The browser will interpret the entry module, identify its dependencies, and send out requests for its dependent modules. When these files are returned over the network, the browser will parse their contents, identify their dependencies, and send out more requests for those second-order dependencies if they are not already loaded. This recursive asynchronous process will continue until the entire application's dependency graph is resolved. Once the dependencies are resolved, the application can begin to formally load.

This process is very similar to the AMD style of module loading. Module files are loaded on demand, and successive rounds of module file requests are delayed by the network latency of each dependency module file. That is, if entry moduleA depends on moduleB, and moduleB depends on moduleC, the browser will not know to send a request for C until the request for B has first completed. This style of loading is efficient and requires no outside tooling, but loading a large application with a deep dependency graph may take too long.

Module Behavior

ECMAScript 6 modules borrow many of the best features from CommonJS and AMD predecessors. To name a few:

  • Module code is only executed when it is loaded.
  • A module will only ever be loaded a single time.
  • Modules are singletons.
  • Modules can define a public interface with which other modules can observe and interact.
  • Modules can request that other modules be loaded.
  • Circular dependencies are supported.

The ES6 module system also introduces new behavior:

  • ES6 modules by default execute in strict mode.
  • ES6 modules do not share a global namespace.
  • The value of this at the top-level of a module is undefined (as opposed to window in the case of normal scripts).
  • var declarations will not be added to the window object.
  • ES6 modules are loaded and executed asynchronously.

The behavior described here characterizing an ECMAScript 6 module is conditionally enforced by the browser's runtime when it knows to consider a certain file to be a module. A JavaScript file is designated as a module either when it is associated with a <script type="module"> or when it is loaded via an import statement.

Module Exports

The public exports system for ES6 modules is very similar to CommonJS. The exports keyword is used to control what parts of a module are visible to external modules. There are two types of exports in ES6 modules: named exports and default exports. Different types of exports means they are imported differently—this is covered in the following section.

The export keyword is used to declare a value as a named export. Exports must occur in the top-level of the module; they cannot be nested inside blocks:

// Allowed
export …

// Disallowed
if (condition) {
 export …
}

Exporting a value has no direct effect on JavaScript execution inside a module, so there is no restriction on the locality of the export statement relative to what is being exported, or what order the export keyword must appear in the module. An export may even precede the declaration of the value it is exporting:

// Allowed
const foo = 'foo';
export { foo };

// Allowed
export const foo = 'foo';

// Allowed, but avoid
export { foo };
const foo = 'foo';

A named export behaves as if the module is a container for exported values. Inline named exports, as the name suggests, can be performed in the same line as variable declaration. In the following example, a variable declaration is paired with an inline export. An external module could import this module, and the value foo would be available inside it as a property of that module:

export const foo = 'foo';

Declaration does not need to occur in the same line as the export; you can perform the declaration and export the identifier elsewhere in the module inside an export clause:

const foo = 'foo';
export { foo };

It is also possible to provide an alias when exporting. An alias must occur inside the export clause bracket syntax; therefore, declaring a value, exporting it, and providing an alias cannot all be done in the same line. In the following example, an external module would access this value by importing this module and using the myFoo export:

const foo = 'foo';
export { foo as myFoo };

Because ES6 named exports allow you to treat the module as a container, you can declare multiple named exports inside a single module. Values can be declared inside the export statement, or they can be declared prior to specifying it as an export:

export const foo = 'foo';
export const bar = 'bar';
export const baz = 'baz'; 

Because exporting multiple values is common behavior, grouping export declarations is supported, as is aliasing some or all of those exports:

const foo = 'foo';
const bar = 'bar';
const baz = 'baz';
export { foo, bar as myBar, baz }; 

A default export behaves as if the module is the same entity as the exported value. The default keyword modifier is used to declare a value as a default export, and there can only ever be a single default export. Attempting to specify duplicate default exports will result in a SyntaxError.

In the following example, an external module could import this module, and the module itself would be the value of foo:

const foo = 'foo';
export default foo;

Alternately, the ES6 module system will recognize the default keyword when provided as an alias, and will apply the default export to the value even though it uses the named export syntax:

const foo = 'foo';

// Behaves identically to "export default foo;"
export { foo as default };

Because there are no incompatibilities between named exports and default exports, ES6 allows you to use both in the same module:

const foo = 'foo';
const bar = 'bar';

export { bar };
export default foo; 

The two export statements can be combined into the same line:

const foo = 'foo';
const bar = 'bar';

export { foo as default, bar }; 

The ES6 specification restricts what can and cannot be done inside various forms of an export statement. Some forms allow declaration and assignment, some forms allow only expressions, and some forms only allow simple identifiers. Note that some forms utilize semicolons, and some do not.

// Named inline exports 
export const baz = 'baz'; 
export const foo = 'foo', bar = 'bar';
export function foo() {}
export function* foo() {}
export class Foo {} 

// Named clause exports
export { foo };
export { foo, bar };
export { foo as myFoo, bar };

// Default exports 
export default 'foo';
export default 123;
export default /[a-z]*/;
export default { foo: 'foo' };
export { foo, bar as default }; 
export default foo
export default function() {} 
export default function foo() {}
export default function*() {}
export default class {}

// Various disallowed forms that will cause errors:

// Variable declarations cannot occur inside inline default exports
export default const foo = 'bar';

// Only identifiers can appear in export clauses
export { 123 as foo }'

// Aliasing only can occur in export clauses
export const foo = 'foo' as myFoo;

Module Imports

Modules can use exports from other modules using the import keyword. Like export, import must appear in the top level of a module:

// Allowed
import …

// Disallowed
if (condition) {
 import …
}

import statements are hoisted to the top of the module. Therefore, like the export keyword, the order in which import statements appear relative to the use of the imported values is unimportant. It is recommended, however, that imports be kept at the top of the module.

// Allowed
import { foo } from './fooModule.js';
console.log(foo); // 'foo'

// Allowed, but avoid
console.log(foo); // 'foo'
import { foo } from './fooModule.js';

A module identifier can be either the relative path to that module file from the current module or the absolute path to that module file from the base path. It must be a plain string; the identifier cannot be dynamically computed, for example, concatenating strings.

If the modules are being natively loaded in the browser via the path in their module identifier, a .js extension is required for the correct file to be referenced. However, if the ES6 modules are being bundled or interpreted by a build tool or third-party module loader, you may not need to include the file extension of the module in its identifier.

// Resolves to /components/bar.js
import … from './bar.js'; 

// Resolves to /bar.js
import … from '../bar.js';

// Resolves to /bar.js
import … from '/bar.js';

Modules do not need to be imported via their exported members. If you do not need specific exported bindings from a module, but you still seed to load and execute the module for its side effects, you can load it with only its path:

import './foo.js';

Imports are treated as read-only views to the module, effectively the same as const-declared variables. When performing a bulk import using *, the aliased collection of named exports behaves as if it were treated with Object.freeze(). Direct manipulation of exported values is impossible, although modifying properties of an exported object is still possible. Adding or removing exported properties of the exported collection is also disallowed. Mutation of exported values must occur using exported methods that have access to internal variables and properties.

import foo, * as Foo './foo.js';

foo = 'foo';   // Error

Foo.foo = 'foo'; // Error

foo.bar = 'bar'; // Allowed

The distinction between named exports and default exports is mirrored in the way they are imported. Named exports can be retrieved in bulk without specifying their exact identifier using * and providing an identifier for the collection of exports:

const foo = 'foo', bar = 'bar', baz = 'baz';
export { foo, bar, baz }
import * as Foo from './foo.js';

console.log(Foo.foo); // foo 
console.log(Foo.bar); // bar
console.log(Foo.baz); // baz

To perform explicit imports, the identifiers can be placed inside an import clause. Using an import clause also allows you to specify aliases for the imports:

import { foo, bar, baz as myBaz } from './foo.js';

console.log(foo);  // foo 
console.log(bar);  // bar
console.log(myBaz); // baz

Default exports behave as if the module target is the exported value. They can be imported using the default keyword and providing an alias; alternately, they can be imported without the use of curly braces, and the identifier you specify is effectively the alias for the default export:

// Equivalent
import { default as foo } from './foo.js';
import foo from './foo.js';

If a module exports both named exports and default exports, it's possible to retrieve them in the same import statement. This retrieval can be performed by enumerating specific exports, or using *:

import foo, { bar, baz } from './foo.js';

import { default as foo, bar, baz } from './foo.js';

import foo, * as Foo from './foo.js';

Module Passthrough Exports

Imported values can be piped directly through to an export. You are also able to convert default to named exports, and vice versa. If you wanted to incorporate all the named exports from one module into another, this can be accomplished with a * export:

export * from './foo.js'; 

All named exports in foo.js will be available when importing bar.js. This syntax will ignore the default value of foo.js if it has one. This syntax also requires care around export name collisions. If foo.js exports baz, and bar.js also exports baz, the ultimate export value will be the one specified in bar.js. This "overwrite" will occur silently:

foo.jsexport const baz = 'origin:foo';
bar.js export * from './foo.js'; export const baz = 'origin:bar'; import { baz } from './bar.js'; 
main.js console.log(baz); // origin:bar  

It's also possible to enumerate which values from the external module are being passed through to the local exports. This syntax supports aliasing:

export { foo, bar as myBar } from './foo.js'; 

Similarly, the default export of an imported module can be reused and exported as the default export of the current module:

 export { default } from './foo.js'; 

This does not perform any copy of the export; it merely propagates the imported reference to the original module. The defined value of that import still lives in the original module, and the same restrictions involving mutation of imports apply to imports that are re-exported.

When performing a re-export, it is also possible to alter the named/default designation from the imported module. A named import can be specified as the default export as follows:

export { foo as default } from './foo.js';

Worker Modules

ECMAScript 6 modules are fully compatible with Worker instances. When instantiating, Workers can be passed a path to a module file in the same way you would pass a normal script file. The Worker constructor accepts a second argument allowing you to inform it that you are passing a module file.

Worker instantiation for both types of workers would behave as follows:

// Second argument defaults to { type: 'classic' }
const scriptWorker = new Worker('scriptWorker.js');

const moduleWorker = new Worker('moduleWorker.js', { type: 'module' });

Inside a module-based Worker, the self.importScripts() method is typically used to load external scripts inside a script-based Worker will throw an error. This is because a module's import behavior subsumes that of importScripts().

Backwards Compatibility

Because adoption of ECMAScript module compatibility will be gradual, it is valuable for early module adopters to have the ability to develop for both browsers that support modules and browsers that do not. For users who want to natively use ECMAScript 6 modules in the browser when possible, solutions will involve serving two versions of your code—a module-based version and a script-based version. If this is undesirable to you, utilizing third-party module systems such as SystemJS or transpiling down ES6 modules at build time are better options.

The first strategy involves inspecting the browser's user-agent on the server, matching it against a known list of browsers that support modules, and using that to decide which JS files to serve. This method is brittle and complicated, and is not recommended. A better and more elegant solution to this is to make use of the script type attribute and the script nomodule attribute.

When a browser does not recognize the type attribute value of a <script> element, it will decline to execute its contents. For legacy browsers that do not support modules, this means that <script type="module"> will never execute. Therefore, it is possible to place a fallback <script> tag right next to the <script type="module"> tag:

// Legacy browser will not execute this
<script type="module" src="module.js"></script>

// Legacy browser will execute this
<script src="script.js"></script>

This, of course, leaves the problem of browsers that do support modules. In this case, the preceding code will execute twice—obviously an undesirable outcome. To prevent this, browsers that support ECMAScript 6 modules natively will also recognize the nomodule attribute. This attribute informs browsers that support ES6 modules to not execute the script. Legacy browsers will not recognize the attribute and ignore it.

Therefore, the following configuration will yield a setup in which both modern and legacy browsers will execute exactly one of these scripts:

// Modern browser will execute this
// Legacy browser will not execute this
<script type="module" src="module.js"></script>

// Modern browser will not execute this
// Legacy browser will execute this
<script nomodule src="script.js"></script>

SUMMARY

The module pattern remains a timeless tool for managing complexity. It allows developers to create segments of isolated logic, declare dependencies between these segments, and connect them together. What's more, the pattern is one that has proven to scale elegantly to arbitrary complexity and across platforms.

For years, the ecosystem grew around a contentious dichotomy between CommonJS, a module system targeted at server environments, and AMD, a module system targeted at latency-constrained client environments. Both systems enjoyed explosive growth, but the code written for each was in many ways at odds, and often incurred an unholy amount of boilerplate. What's more, neither system was natively implemented by browsers, and in the wake of this incompatibility rose a deluge of tooling that allowed for the module pattern to be used in browsers.

Included in the ECMAScript 6 specification is a wholly new concept for browser modules that takes the best of both worlds and combines them into a simpler declarative syntax. Browsers increasingly offer support for native module utilization, but also provide robust tooling to bridge the gap between marginal and full support for ES6 modules.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset