Writing modular code with ES2015

Another problem that JavaScript professionals have experienced over the years is the lack of a module system in the language. Initially, the community developed different patterns, aiming to enforce the modularity and the encapsulation of the software we produce. Such patterns included the module pattern, which takes advantage of the functional lexical scope and closures. Another example is the namespace pattern, which represents the different namespaces as nested objects. AngularJS introduced its own module system that unfortunately doesn't provide features, such as lazy module loading. However, these patterns were more like workarounds rather than real solutions.

CommonJS (used in node.js) and AMD (Asynchronous Module Definition) were later invented. They are still widely used today and provide features such as handling of circular dependencies, asynchronous module loading (in AMD), and so on.

TC39 took the best of the existing module systems and introduced this concept on a language level. ES2015 provides two APIs to define and consume modules. They are as follows:

  • Declarative API.
  • Imperative API using a module loader.

Angular takes full advantage of the ES2015 module system, so let's dive into it! In this section, we will take a look at the syntax used for the declarative definition and consumption of modules. We will also take a peek at the module loader's API in order to see how we can programmatically load modules in an explicit asynchronous manner.

Using the ES2015 module syntax

Let's take a look at an example:

// ch3/modules/math.ts 
 
export function square(x) { 
  return Math.pow(x, 2); 
};
 
export function log10(x) { 
  return Math.log10(x); 
};
 
export const PI = Math.PI; 

In the preceding snippet, we defined a simple ES2015 module in the math.ts file. We can think of it as a sample math Angular utility module. Inside it, we define and export the square and log10 functions and the constant PI. The const keyword is another keyword brought by ES2015 that is used to define constants. As you can see, what we do is nothing more than prefixing the function's definitions with the keyword export. If we want to export the entire functionality in the end and skip the duplicate explicit usage of export, we can use the following approach:

// ch3/modules/math2.ts 
 
function square(x) { 
  return Math.pow(x, 2); 
};
 
function log10(x) { 
  return Math.log10(x); 
};
 
const PI = Math.PI;
 
export { square, log10, PI }; 

The syntax on the last line is nothing more than an enhanced object literal syntax, introduced by ES2015. Now, let's take a look at how we can consume this module:

// ch3/modules/app.ts 
 
import {square, log10} from './math';
 
console.log(square(2)); // 4 
console.log(log10(10)); // 1 

As an identifier of the module, we use its relative path to the current file. Using destructuring, we import the required functions – in this case, square and log10.

Taking advantage of the modules' implicit asynchronous behavior

It is important to note that the ES2015 module syntax has implicit asynchronous behavior.

Taking advantage of the modules' implicit asynchronous behavior

Figure 2

In the preceding diagram, we have modules A, B, and C. Module A uses modules B and C, so it depends on them. Once the user requires module A, the JavaScript module loader would need to load modules B and C before being able to invoke any of the logic that resides in module A because of the dependencies they have. Modules B and C will be loaded asynchronously. Once they are loaded completely, the JavaScript virtual machine will be able to execute module A.

Using aliases

Another typical situation is when we want to use an alias for a given export. For example, if we use a third-party library, we may want to rename any of its exports in order to escape name collisions or just to have a more convenient naming:

import {
  platformBrowserDynamic as platform
} from '@angular/platform-browser-dynamic';

Importing all the module exports

We can import the entire math module using the following syntax:

// ch3/modules/app2.ts 
 
import * as math from './math';
 
console.log(math.square(2)); // 4 
console.log(math.log10(10)); // 1 
console.log(math.PI); // 3.141592653589793 

The semantics behind this syntax is quite similar to CommonJS, although, in the browser, we have implicit asynchronous behavior.

Default exports

If a given module defines an export, which would quite likely be used by any of its consumer modules, we can take advantage of the default export syntax:

// ch3/modules/math3.ts 
 
export default function cube(x) { 
  return Math.pow(x, 3); 
};
 
export function square(x) { 
  return Math.pow(x, 2); 
}; 

In order to consume this module, we can use the following app.ts file:

// ch3/modules/app3.ts 
 
import cube from './math3';
 
console.log(cube(3)); // 27 

Alternatively, if we want to import the default export and perform some other exports, we can use:

// ch3/modules/app4.ts 
 
import cube, { square } from './math3';
 
console.log(square(2)); // 4 
console.log(cube(3)); // 27 

In general, the default export is nothing more than a named export named with the reserved word default:

// ch3/modules/app5.ts 
 
import { default as cube } from './math3';
 
console.log(cube(3)); // 27 
..................Content has been hidden....................

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