Chapter 10. Modules

Images

When providing code to be reused by many programmers, it is important to separate the public interface from the private implementation. In an object-oriented programming language, this separation is achieved with classes. A class can evolve by changing the private implementation without affecting its users. (As you saw in Chapter 4, hiding private features is not yet fully supported in JavaScript, but this will surely come.)

A module system provides the same benefits for programming at larger scales. A module can make certain classes and functions available, while hiding others, so that the module’s evolution can be controlled.

Several ad-hoc module systems were developed for the JavaScript. In 2015, ECMAScript 6 codified a simple module system that is the topic of this short chapter.

10.1 The Module Concept

A module provides features (classes, functions, or other values) for programmers, called the exported features. Any features that are not exported are private to the module.

A module also specifies on which other modules it depends. When a module is needed, the JavaScript runtime loads it together with its dependent modules.

Modules manage name conflicts. Since the private features of a module are hidden from the public, it does not matter what they are called. They will never clash with any names outside the module. When you use a public feature, you can rename it so that it has a unique name.

Images Note

In this regard, JavaScript modules differ from Java packages or modules which rely on globally unique names.

It is important to understand that a module is different from a class. A class can have many instances, but a module doesn’t have instances. It is just a container for classes, functions, or values.

10.2 ECMAScript Modules

Consider a JavaScript developer who wants to make features available to other programmers. The developer places those features into a file. The programmers who use the features include the file in their project.

Now suppose a programmer includes such files from multiple developers. There is a good chance that some of those feature names will conflict with each other. More ominously, each file contains quite a few helper functions and variables whose names give rise to further conflicts.

Clearly, there needs to be some way of hiding implementation details. For many years, JavaScript developers have simulated modules through closures, placing helper functions and classes inside a wrapper function. This is similar to the “hard objects” technique from Chapter 3. They also developed ad-hoc ways of publishing exported features and dependencies.

Node.js implements a module system (called Common.js) that manages module dependencies. When a module is needed, it and its dependencies are loaded. That loading happens synchronously, as soon as the demand for a module occurs.

The AMD (Asynchronous Module Definition) standard defines a system for loading modules asynchronously, which is better suited for browser-based applications.

ECMAScript modules improve on both of these systems. They are parsed to quickly establish their dependencies and exports, without having to execute their bodies first. This allows asynchronous loading and circular dependencies. Nowadays, the JavaScript world is transitioning to the ECMAScript module system.

Images Note

For Java programmers, an analog of a JavaScript module is a Maven artifact, or, since Java 9, a Java platform module. Artifacts provide dependency information but no encapsulation (beyond that of Java classes and packages). Java platform modules provide both, but they are quite a bit more complex than ECMAScript modules.

10.3 Default Imports

Only a few programmers write modules; many more programmers consume them. Let us therefore start with the most common activity: importing features from an existing module.

Most commonly, you import functions and classes. But you can also import objects, arrays, and primitive values.

A module implementor can tag one feature (presumably the most useful one) as the default. The import syntax makes it particularly easy to import the default feature. Consider this example where we import a class from a module that provides an encryption service:

import CaesarCipher from './modules/caesar.mjs'

This statement specifies the name that you choose to give to the default feature, followed by the file that contains the module implementation. For more details on specifying module locations, see Section 10.7, “Packaging Modules” (page 217).

The choice of the feature name in your program is entirely yours. If you prefer, you can give it a shorter name:

import CC from './modules/caesar.mjs'

If you work with modules that provide their services as a default feature, that is all you need to know about the ECMAScript module system.

Images Note

In a browser, the module location must be a full URL or a relative URL that starts with ./, ../, or /. This restriction leaves open the possibility of special handling for well-known package names or paths in the future.

In Node.js, you can use a relative URL that starts with ./, ../, or a file:// URL. You can also specify a package name.

10.4 Named Imports

A module can export named features in addition to, or instead of, the default. The module implementor gives a name to each nondefault feature. You can import as many of these named features as you like.

Here we import two functions that the module calls encrypt and decrypt:

import { encrypt, decrypt } from './modules/caesar.mjs'

Of course, there is a potential pitfall. What if you want to import encryption functions from two modules, and they both call it encrypt? Fortunately, you can rename the imported features:

import { encrypt as caesarEncrypt, decrypt as caesarDecrypt }
  from './modules/caesar.mjs'

In this way, you can always avoid name clashes.

If you want to import both the default feature and one or more named features, combine the two syntax elements:

import CaesarCipher, { encrypt, decrypt } from './modules/caesar.mjs'

or

import CaesarCipher, { encrypt as caesarEncrypt, decrypt as caesarDecrypt} . . .

Images Note

Be sure to use braces when importing a single nondefault feature:

import { encrypt } from './modules/caesar.mjs'

Without the braces, you would give a name to the default feature.

If a module exports many names, then it would be tedious to name each of them in the import statement. Instead, you can pour all exported features into an object:

import * as CaesarCipherTools from './modules/caesar.mjs'

You then use the imported functions as CaesarCipherTools.encrypt and CaesarCipherTools.decrypt. If there is a default feature, it is accessible as CaesarCipherTools.default. You can also name it:

import CaesarCipher, * as CaesarCipherTools . . .

You can use the import statement without importing anything:

import './app/init.mjs'

Then the statements in the file are executed but nothing is imported. This is not common.

10.5 Dynamic Imports

Images

A stage 4 proposal allows you to import a module whose location is not fixed. Loading a module on demand can be useful to reduce the start-up cost and footprint of an application.

For dynamic import, use the import keyword as if it were a function with the module location as argument:

import(`./plugins/${action}.mjs`)

The dynamic import statement loads the module asynchronously. The statement yields a promise for an object containing all exported features. The promise is fulfilled when the module is loaded. You can then use its features:

import(`./plugins/${action}.mjs`)
  .then(module => {
    module.default()
    module.namedFeature(args)
    . . .
  })

Of course you can use the async/await notation:

async load(action) {
  const module = await import(`./plugins/${action}.mjs`)
  module.default()
  module.namedFeature(args)
  . . .
}

When you use a dynamic import, you do not import features by name, and there is no syntax for renaming features.

Images Note

The import keyword is not a function, even though it looks like one. It is just given a function-like syntax. This is similar to the super(. . .) syntax of the super keyword.

10.6 Exports

Now that you have seen how to import features from modules, let us switch to the module implementor’s perspective.

10.6.1 Named Exports

In a module, you can tag any number of functions, classes, or variable declarations with export:

export function encrypt(str, key) { . . . }
export class Cipher { . . . }
export const DEFAULT_KEY = 3

Alternatively, you can provide an export statement with the names of the exported features:

function encrypt(str, key) { . . . }
class Cipher { . . . }
const DEFAULT_KEY = 3
. . .
export { encrypt, Cipher, DEFAULT_KEY }

With this form of the export statement, you can provide different names for exported features:

export { encrypt as caesarEncrypt, Cipher, DEFAULT_KEY }

Keep in mind that the export statement defines the name under which the feature is exported. As you have seen, an importing module may use the provided name or choose a different name to access the feature.

Images Note

The exported features must be defined at the top-level scope of the module. You cannot export local functions, classes, or variables.

10.6.2 The Default Export

At most one function or class can be tagged as export default:

export default class Cipher { . . . }

In this example, the Cipher class becomes the default feature of the module.

You cannot use export default with variable declarations. If you want the default export to be a value, do not declare a variable. Simply write export default followed by the value:

export default 3 // OK
export default const DEFAULT_KEY = 3
  // Error—export default not valid with const/let/var

It isn’t likely that someone would make a simple constant the default value. A more realistic choice would be to export an object with multiple features:

export default { encrypt, Cipher, DEFAULT_KEY }

You can use this syntax with an anonymous function or class:

export default (s, key) => { . . . } // No need to name this function

or

export default class { // No need to name this class
  encrypt(key) { . . . }
  decrypt(key) { . . . }
}

Finally, you can use the renaming syntax to declare the default feature:

export { Cipher as default }

Images Note

The default feature is simply a feature with the name default. However, since default is a keyword, you cannot use it as an identifier and must use one of the syntactical forms of this section.

10.6.3 Exports Are Variables

Each exported feature is a variable with a name and a value. The value may be a function, a class, or an arbitrary JavaScript value.

The value of an exported feature can change over time. Those changes are visible in importing modules. In other words, an exported feature captures a variable, not just a value.

For example, a logging module might export a variable with the current logging level and a function to change it:

export const Level = { FINE: 1, INFO: 2, WARN: 3, ERROR: 4 }
export let currentLevel = Level.INFO
export const setLevel = level => { currentLevel = level }

Now consider a module that imports the logging module with the statement:

import * as logging from './modules/logging.mjs'

Initially, in that module, logging.currentLevel has value Level.INFO or 2. If the module calls

logging.setLevel(logging.Level.WARN)

the variable is updated, and logging.currentLevel has value 3.

However, in the importing module, the variable is read-only. You cannot set

logging.currentLevel = logging.Level.WARN
  // Error—cannot assign to imported variable

The variables holding exported features are created as soon as the module is parsed, but they are only filled when the module body is executed. This enables circular dependencies between modules (see Exercise 6).

Images Caution

If you have a cycle of modules that depends on each other, then it can happen that an exported feature is still undefined when it is used in another module—see Exercise 11.

10.6.4 Reexporting

When you provide a module with a rich API and a complex implementation, you will likely depend on other modules. Of course, the module system takes care of dependency management, so the module user doesn’t have to worry about that. However, it can happen that one of the modules contains useful features that you want to make available to your users. Instead of asking users to import those features themselves, you can reexport them.

Here, we reexport features from another module:

export { randInt, randDouble } from './modules/random.mjs'

Whoever imports this module will have the features randInt and randDouble from the './modules/random.mjs' module available, as if they had been defined in this module.

If you like, you can rename features that you reexport:

export { randInt as randomInteger } from './modules/random.mjs'

To reexport the default feature of a module, refer to it as default:

export { default } from './modules/stringutil.mjs'
export { default as StringUtil } from './modules/stringutil.mjs'

Conversely, if you want to reexport another feature and make it the default of this module, use the following syntax:

export { Random as default } from './modules/random.mjs'

Finally, you can reexport all nondefault features of another module.

export * from './modules/random.mjs'

You might want to do this if you split up your project into many smaller modules and then provide a single module that is a façade for the smaller ones, reexporting all of them.

The export * statement skips the default feature because there would be a conflict if you were to reexport default features from multiple modules.

10.7 Packaging Modules

Modules are different from plain “scripts”:

  • The code inside a module always executes in strict mode.

  • Each module has its own top-level scope that is distinct from the global scope of the JavaScript runtime.

  • A module is only processed once even if it is loaded multiple times.

  • A module is processed asynchronously.

  • A module can contain import and export statements.

When the JavaScript runtime reads the module content, it must know that it is processing a module and not a plain script.

In a browser, you load a module with a script tag whose type attribute is module.

<script type="module" src="./modules/caesar.mjs"></script>

In Node.js, you can use the file extension .mjs to indicate that a file is a module. If you want to use a plain .js extension, you need to mark modules in the package.json configuration file. When invoking the node executable in interactive mode, use the command-line option --input-type=module.

It seems simplest to always use the .mjs extension for modules. All runtimes and build tools recognize that extension.

Images Note

When you serve .mjs files from a web server, the server needs to be configured to provide the header Content-Type: text/javascript with the response.

Images Caution

Unlike regular scripts, browsers fetch modules with CORS restrictions. If you load modules from a different domain, the server must return an Access-Control-Allow-Origin header.

Images Note

The import.meta object is a stage 3 proposal to provide information about the current module. Some JavaScript runtimes provide the URL from which the module was loaded as import.meta.url.

Exercises

  1. Find a JavaScript library for statistical computation (such as https://github.com/simple-statistics/simple-statistics). Write a program that imports the library as an ECMAScript module and computes the mean and standard deviation of a data set.

  2. Find a JavaScript library for encryption (such as https://github.com/brix/crypto-js). Write a program that imports the library as an ECMAScript module and encrypts a message, then decrypts it.

  3. Implement a simple logging module that supports logging messages whose log level exceeds a given threshold. Export a log function, constants for the log level, and a function to set the threshold.

  4. Repeat the preceding exercise, but export a single class as a default feature.

  5. Implement a simple encryption module that uses the Caesar cipher (adding a constant to each code point). Use the logging module from one of the preceding exercises to log all calls to decrypt.

  6. As an example of a circular dependency between modules, repeat the preceding exercise, but provide an option to encrypt the logs in the logging module.

  7. Implement a simple module that provides random integers, arrays of random integers, and random strings. Use as many different forms of the export syntax as you can.

  8. What is the difference between

    import Cipher from './modules/caesar.mjs'

    and

    import { Cipher } from './modules/caesar.mjs'
  9. Explain the difference between

    export { encrypt, Cipher, DEFAULT_KEY }

    and

    export default { encrypt, Cipher, DEFAULT_KEY }
  10. Which of the following are valid JavaScript?

    export function default(s, key) { . . . }
    export default function (s, key) { . . . }
    export const default = (s, key) => { . . . }
    export default (s, key) => { . . . }
  11. Trees have two kinds of nodes: those with children (parents) and those without (leaves). Let’s model that with inheritance:

    class Node {
      static from(value, ...children) {
        return children.length === 0 ? new Leaf(value)
          : new Parent(value, children)
      }
    }
    
    class Parent extends Node {
      constructor(value, children) {
        super()
        this.value = value
        this.children = children
      }
      depth() {
        return 1 + Math.max(...this.children.map(c => c.depth()))
      }
    }
    
    class Leaf extends Node {
      constructor(value) {
        super()
        this.value = value
      }
      depth() {
        return 1
      }
    }

    Now a module-happy developer wants to put each class into a separate module. Do that and try it out with a demo program:

    import { Node } from './node.mjs'
    
    const myTree = Node.from('Adam',
      Node.from('Cain', Node.from('Enoch')),
      Node.from('Abel'),
      Node.from('Seth', Node.from('Enos')))
    console.log(myTree.depth())

    What happens? Why?

  12. Of course, the issue in the preceding exercise could have been easily avoided by not using inheritance, or by placing all classes into one module. In a larger system, those alternatives may not be feasible. In this exercise, keep each class in its own module and provide a façade module tree.mjs that reexports all three modules. In all modules, import from './tree.mjs', not the individual modules. Why does this solve the issue?

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

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