JavaScript module — require, exports, import and export

This article gives all you need to know about JavaScript module, including require and exports which are used by CommonJS and AMD, import and export which are used in native ES6 JavaScript module. In this post, you will get a brief history of module system development and a comprehensive usage about ES6 JavaScript module.

Most content are reorganized from [MDN](JavaScript modules – JavaScript | MDN) and the articles list in reference. You can treat this article as a quick handbook for JavaScript module usage.

JavaScript module system development

In early days, JavaScript was pretty small and just used to provide some interactivity. Therefore there is no problem that all JavaScript code will be added into the global scope even they are in different files.

Now JavaScript has been used to complete much more complicated applications. As a result, some frameworks  like AngularReact, and Vue and third-party programs are involved. Then it needs to break up JavaScript program into separate modules for code and namespace scope splitting, dependency reference(import a module on need) and loading, etc.

Before ES6 (the 6th Edition of the ECMAScript standard, short for ES6, also known as ECMAScript 2015) added native module support to the JavaScript language, some module usage have come up, like CommonJS and AMD(Asynchronous Module Definition).

  • CommonJS Synchronous loading

    There is a require() function to reference/import a module. There is an exports object to export.

    It is suited in server-side environment but not well suited in browser environment. Because loading scripts synchronously in the browser kills performance.

    Node.js uses CommonJS module format.

    > Node.js is a cross-platform JavaScript runtime environment. It can be used to build server-side and network applications.

  • AMD Asynchronous loading

    Modules can be loaded asynchronously. This makes it well suited in browser environment.

    It also use require and exports to import and export. There is a call-back style require([modules], callback) function. Once all the modules are available, the callback is called.

    RequireJS(a JavaScript file and module loader) implements AMD module format.

However, because such module usage are not native in JavaScript, it needs tools like Babel(a JavaScript compiler), Webpack(compiling JavaScript modules), or Browserify to use modules in browsers.

Babel, WebPack and Browserify

  • Babel is a JavaScript compiler.

    It is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments.

  • Webpack is used to compile JavaScript modules.

    Now it is a static module bundler for modern JavaScript applications. It internally builds a dependency graph and then combines modules into one or more bundles, which are static assets to serve your content from.

    Besides JavaScript modules, it is able to bundle static assets and has additional features.

  • Browserify is a tool for compiling node-flavored commonJS modules for the browser.

    With Browserify you can write code that uses require in the same way that you would use it in Node.

    The module system that browserify uses is the same as node, so
    packages published to npm that were originally intended for
    use in node but not browsers will work just fine in the browser too.

The ES6 specification introduced modules to the JavaScript language and modern browsers have started to  support module functionality natively.

ES6 JavaScript module overview

A JavaScript module file seems much like a regular JavaScript file except it import or export something. But they behave differently in some aspects. See details on “Differences between Module JavaScript and standard JavaScript” section below.

Note .mjs is a non-standard file extension although some tools support it, but some do not.

.mjs vs .js

 V8’s documentation recommends .mjs extension because:

  1. It make it clear to which files are modules, and which are regular JavaScript.

  2. It ensures that your module files are parsed as a module by runtimes such as https://nodejs.org/api/esm.html#esm_enabling, and build tools such as Babel.

As mentioned in “Troubleshooting > Tips” section, for .mjs extension:

  • Use  text/javascript MIME-type for .mjs file in “ element of a HTML file.

  • Some tools does not support .mjs.

A basic example

A simplified version of basic-modules.

Example structure:

index.html
main.js
modules/
    canvas.js
    square.js

canvas.js

function create() {...}
export { create };

square.js

const name = 'square';
function reportArea(...) {}
function randomSquare(...) {...}

export { name, reportArea };
export default randomSquare;

main.js

import { create } from './modules/canvas.js';
import { name, reportArea } from './modules/square.js';
import randomSquare from './modules/square.js';

let myCanvas = create('myCanvas', document.body, 480, 320);

reportArea(square1.length, reportList);

// Use the default
let square2 = randomSquare(myCanvas.ctx);

index.html

To apply the main.js file in a html file, it needs to set  type="module" in the  element to declare it as a module.

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>Basic JavaScript module example</title>
    <style>
      canvas {
        border: 1px solid black;
      }
    </style>
    <script type="module" src="main.js"></script>
  </head>
  <body>

  </body>
</html>

Export

By default, everything declared in a module is local to the module. If you want something declared in a module to be public, you must export that feature. It is much simple, just put export before the definition of something you want to make it public, or put it in a export list as introduced in the “Basic export” section.

To interoperate with existing CommonJS and AMD modules, it is able to have a default export. As ES6 In Depth: Modules said, all CommonJS and AMD modules are presented to ES6 as having a default export.

Basic export (named export)

You can export functions, var, let, const, and — as we’ll see later — classes. They need to be top-level items; you can’t use export inside a function.

Usage

export const name = 'square';

export function reportArea(...) {...}

or

const name = 'square';
function reportArea(...) {...}

export { name, reportArea };

Default export

Named export means each exported item is referred by its name upon export.

Default export is designed to make it easy to have a default function provided by a module, and also helps JavaScript modules to interoperate with existing CommonJS and AMD module systems (as explained nicely in ES6 In Depth: Modules).

To use default export, just add default after export:

export const name = 'square';
export function reportArea(...) {}
export default function randomSquare(...) {...}

or

const name = 'square';
function reportArea(...) {}
function randomSquare(...) {...}

export { name, reportArea };
export default randomSquare;

There is only one default export allowed per module, therefore there is no {} for default export.

To import name which is a named export and randomSquare which is a default export:

import { name, reportArea } from './modules/square.js';
import randomSquare from './modules/square.js';

Export an anonymous function as default

To export an anonymous function, like this:

export default function(ctx) {
  ...
}

Import the default function like this:

import randomSquare from './modules/square.js';

The above line is basically shorthand for:

import {default as randomSquare} from './modules/square.js';

Note:

Sometimes default export can bring confusion. Below is an example.

function.js

export default function sum(x, y) {
    return x + y;
}

Import it like this in main.js:

import difference from './functions.js'

difference(1, 2); // 3

Import

To use a module is simple either, just import it.

Module loading

Speaking of import, you might have thought that how modules are loaded. As ES6 In Depth said, module loading specification was built but is not in the final ES6 standard for inconsistent opinions.

ES6 leaves the details of module loading entirely up to the implementation. The rest of module execution is specified in detail.

But some work is is underway. If you wan to know what will happen to “, Loader describes the behavior of loading JavaScript modules from a JavaScript host environment. It also provides APIs for intercepting the module loading process and customizing loading behavior.

Basic import

Once a module is exported, you can import the module to be able to use it.

import { name, reportArea } from './modules/square.js';


reportArea(square1.length, reportList);

The from is followed by a relative path as you’ve seen in main.js of the previous basic example. It makes the URL portable.

Note: Although imported features are available in the file, they are read only views of the feature that was exported. You cannot change the variable that was imported, but you can still modify properties similar to const. Additionally, these features are imported as live bindings, meaning that they can change in value even if you cannot modify the binding unlike const.

Use * syntax

Instead of importing each item of a module, it is able to to import all items with *. Combined with as, this can provide a good solution to import all exported items of a module into an object (called a module object), giving it its own namespace:

import * as Module from './modules/module.js';

Then you can use an exported item as a member of the object Module:

import * as Module from './modules/module.js';

Module.function1();
Module.function2();

Rename on import and export

It is possible to use an alias for an item on export or import. This can be used to solve naming conflicts.

To rename an item, use as along with import and export statements on import or export. It is similar with the syntax in other languages like Java.

Rename on export:

// inside module.js
export {
  function1 as newFunctionName,
  function2 as anotherNewFunctionName
};

// inside main.js
import { newFunctionName, anotherNewFunctionName } from './modules/module.js';

Rename on import:

// inside module.js
export { function1, function2 };

// inside main.js
import { function1 as newFunctionName,
         function2 as anotherNewFunctionName } from './modules/module.js';

Apply a module in the HTML file

As you’ve seen in the basic example, to use a JavaScript module file in the HTML file, it needs to set type="module" in the  element to declare this script as a module.

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

To embed a piece of inline JavaScript module in the HTML file is similar:

<script type="module">
  /* JavaScript module code here */
</script>

Combine modules into one

If you have multiple levels of dependencies, you might want to simplify things by combing several submodules into parent module.

Take an example at module-aggregation directory, there are  circle.jssquare.js, and triangle.js, they can be combined into a module shapre.js. Then feautres can be imported from that single shapre.js.

The structure of example:

index.html
main.js
modules/
  canvas.js
  shapes.js
  shapes/
    circle.js
    square.js
    triangle.js

Each module like square.js is a normal module like:

class Square { ... }

export { Square };

Inside shape.js:

export { Square } from './shapes/square.js';
export { Triangle } from './shapes/triangle.js';
export { Circle } from './shapes/circle.js';

These statements export Square, Triangle and Circle from individual modules and make them available from shape.js module.

Import from shape.js in main.js:

import { Square, Circle, Triangle } from './modules/shapes.js';

Dynamic module loading

A recent addition to JavaScript modules functionality is dynamic module loading. This allows you to dynamically load modules only when they are needed, rather than having to load everything up front.

This functionality is a bit like the callback-style require([modules], callback) used in AMD mentioned in the first section.

To use dynamic module loading, call import() as a function, passing it the path to the module as a parameter. It returns a Promise, which fulfills with a module object (Like Module object in import * as Module from ... mentioned in “Use * syntax” section) giving you access to that object’s exports, e.g.

import('./modules/myModule.js').then((module) => {
    // Do something with the module.
});

Take the example mentioned in “Combine modules into one” section. There are three modules circle.js, square.js and triangle.js to provide different shape drawing. Here we do not care combining these three modules, but we want to load each module dynamically on need by making some change to main.js and index.html.

A module like square.js:

class Square { ... }

export { Square };

We include three buttons in the index.html file — “Circle”, “Square”, and “Triangle”. When each of them is pressed, dynamically load the required module and then use it to draw the associated shape.

For “Square” button, create an click event listener for it in main.js:

squareBtn.addEventListener('click', () => {
    import('./modules/square.js').then((Module) => {
        // "Square" is available as members of an object Module.
        let square1 = new Module.Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue');
        square1.draw();
        square1.reportArea();
        square1.reportPerimeter();
    });
});

await in module

The await keyword can be used along within a JavaScript module. This allows modules to act as big asynchronous functions meaning code can be evaluated before use in parent modules, but without blocking sibling modules from loading..

Let us continue the example mentioned above. See dynamic-module-imports for the whole example.

In this example, when a shape button is pressed, we use predefined colors to draw the corresponding shape. The colors are defined in colors.json. We create getColors.js which uses a fetch request to load the colors.json file and export it with export following await.

// fetch request
const colors = fetch('../data/colors.json')
  .then(response => response.json());

export default await colors;

The above export statement with await means any other modules which include this one will wait until colors has been downloaded and parsed before using it.

In main.js, import the getColors.js module to use its exported colors to draw shapes.

import colors from './modules/getColors.js';
import { Canvas } from './modules/canvas.js';

let circleBtn = document.querySelector('.circle');
// draw a circle
circleBtn.addEventListener('click', () => {
    import('./modules/circle.js').then((Module) => {
        let circle1 = new Module.Circle(myCanvas.ctx, myCanvas.listId,
            75, 200, 100, colors.green);
        circle1.draw();
        circle1.reportArea();
        circle1.reportPerimeter();
    })
});

...

In this example, the code within main.js won’t execute until the code in getColors.js has run. However it won’t block other modules being loaded. For instance our canvas.js module will continue to load while colors is being fetched.

Differences between Module JavaScript and standard JavaScript

  • You need to pay attention to local testing — if you try to load the HTML file locally (i.e. with a file:// URL), you’ll run into CORS errors due to JavaScript module security requirements. You need to do your testing through a server.

  • Also, note that you might get different behavior from sections of script defined inside modules as opposed to in standard scripts. This is because modules use strict mode automatically.

  • There is no need to use the defer attribute when loading a module script; modules are deferred automatically.

    > The defer attribute indicates a browser that the script is meant to be executed after the document has been parsed, but before firing DOMContentLoaded (see “ attributes).

  • Modules are only executed once, even if they have been referenced in multiple “ tags.

  • Last but not least, let’s make this clear — module features are imported into the scope of a single script — they aren’t available in the global scope. Therefore, you will only be able to access imported features in the script they are imported into, and you won’t be able to access them from the JavaScript console, for example. You’ll still get syntax errors shown in the DevTools, but you’ll not be able to use some of the debugging techniques you might have expected to use.

Troubleshooting

Tips

  • Use a MIME-type of text/javascript for .mjs file in the HTML file.

    Otherwise you’ll get a strict MIME type checking error like “The server responded with a non-JavaScript MIME type”.

  • Test your HTML file with a server.

    If you try to load the HTML file locally (i.e. with a file:// URL), you’ll run into CORS errors due to JavaScript module security requirements. GitHub pages is ideal as it also serves .mjs files with the correct MIME type.

  • .mjs may can not be recognized by some tools or OS.

    Because .mjs is a non-standard file extension, some operating systems might not recognize it, or try to replace it with something else. For example, we found that macOS was silently adding on .js to the end of .mjs files and then automatically hiding the file extension. So all of our files were actually coming out as x.mjs.js. Once we turned off automatically hiding file extensions, and trained it to accept .mjs, it was OK.

Uncaught SyntaxError: Cannot use import statement outside a module

Firefox may report the error as:

Uncaught SyntaxError: import declarations may only appear at top level of a module.

You need to set type="module" inside “ in the HTML file.

Reference

  • JavaScript modules – JavaScript | MDN

  • ES6 In Depth: Modules 2015

    ES6 In Depth is a series on new features being added to the JavaScript programming language in the 6th Edition of the ECMAScript standard, ES6 for short.

  • Understanding Modules, Import and Export in JavaScript 2020

  • Why AMD?

    This page talks about the design forces and use of the Asynchronous Module Definition (AMD) API for JavaScript modules, the module API supported by RequireJS.

  • Why Web Modules?

    The problems making modules are needed.

    • Web sites are turning into Web apps

    • Code complexity grows as the site gets bigger

    • Assembly gets harder

    • Developer wants discrete JS files/modules

    • Deployment wants optimized code in just one or a few HTTP calls

    The mechanisms to load modules.

    • XMLHttpRequest(XHR).  Find require() calls with a regular expression, make sure we load those scripts, then use eval() or script elements to set the text of the script loaded via XHR. There are issues with cross-domain requests and eval().

    • Web workers. There are issues with cross browser support and eval().

    • document.write(). There are issues too.

    • head.appendchild(). There are issues too.

    • Function wrapping. Construct module loading API with function wrappers.