Habdul Hazeez I teach and write code with interests in web development, computer security, and artificial intelligence.

JavaScript Reference Guide: JS module ecosystem

41 min read 11751

JavaScript Guide for JS Module Ecosystem

Introduction

JavaScript modules are individual parts of a JavaScript program that can be tested on their own and reused in other projects. They’re used to improve the organization of code.

In this article, you’ll learn how to create and use modules in your JavaScript applications using the module systems available in the JavaScript ecosystem.

What is a module system?

In its early days, JavaScript was used mainly to add interactivity to a webpage. However, JavaScript evolved and was used by developers to create complex web applications running on the server-side and client side (thanks to projects like Helma and Jaxer).

As many of us who have worked on server-side code know, this code in particular can be difficult to maintain, test, and debug if not structured well because of the amount of interaction that takes place among the code itself.

This was a problem for JavaScript as the application grew because it did not have a standard way to structure code into distinct, functional parts that could be tested and maintained single-handedly, nor did it include other JavaScript code in projects.

That is, of course, until modules came along.

However, even though modules separate and structure code, there was still a need for an ecosystem within which to share these modules.

The inspiration to create such an ecosystem was further fueled by the fact that programming languages like Python, Java, and Ruby all have ecosystems for sharing software packages, which can range from small-scale to full-blown applications.

To that end, Kevin Dangoor created the ServerJS group in hopes of making the collection of publicly available JavaScript code more valuable. Today, ServerJS is known as CommonJS.

Why you need modules in your applications

When your application contains a few lines of code, you might not need modules, but if the Line of Code (LOC) starts reading in the hundreds spanning multiple files, it’s best to split your code base into modules. The benefits are enormous among which are:

  • Reusability
  • Readability
  • Easier maintenance

Reusability

When your application is organized into modules, it makes it convenient to reuse these modules in other projects eliminating the need for a full rewrite, thereby speeding up your development workflow.

Readability

Your application will definitely have multiple functionalities. If you design all the application functionality in a single file, there is a high possibility that your future self will find it difficult to read.

When you design your application using modules, each functionality can reside in a module therefore, when an error occurs, you’ll know the root cause of the problem, and you can trace and fix it in complete isolation from the remaining code base.



Easier Maintenance

Among the beauty of software is evolution. In the long run, your application will need certain features which you think is necessary or your users asked for it, when the architecture of your application is structured into modules, you can easily add or remove features.

In addition, we humans are not perfect and our application will have bugs. Fixing bugs is also part of software maintenance. With modules, you’ll know where to look in your code base.

Module systems in JavaScript

The following are the module systems available in JavaScript at the time of writing:

  • CommonJS (more popular)
  • Asynchronous Module Definition (AMD) (less popular)
  • Universal Module Definition (UMD) (less popular)
  • ES Modules (more popular)

CommonJS

CommonJS is a module system that aims to provide a standard way of writing your JavaScript applications, especially server-side applications. It is the module format used in Node.js; a JavaScript runtime environment written (mostly) in C++.

In CommonJS, you can create and use modules, thanks to two keyword exports and require. The exports keyword is used when you want to expose specific code of the module, then require is used by files which use such a module.

Furthermore, Node.js has a module.exports property that you can assign a new value such as a function or object (discussed later).

Now that you know CommonJS uses export and require when managing modules, it’s best to see a practical way of how these keywords play out in an application. To do this, we’ll simulate a prototype shopping cart that we’ll test on Node.js which, luckily for us, implements CommonJS module format.

Sample project: Simulating a shopping cart

You’ll find a shopping cart on most e-commerce websites, we can not implement the full functionality of a shopping cart in this article, that is beyond the scope of this article, but instead, we’ll demonstrate one popular thing related to shopping carts: adding an item to a cart.

Before we proceed, you’ll need the following:

  • Node.js
  • Your favorite code editor

If you need help installing Node.js, kindly check the following resources depending on your Operating System:

In addition, if you are on Windows, after installing Node.js, you can install GitBash which provides color highlighting on its command line compared to Windows Command Prompt..

With that out of the way, let’s proceed. Open your code editor, and type the following code:

// Save as cart.js
 
/**
 * Define an array of items.
 * Mind you this variable is private to this
 * module.
 */
var items = [];
// The function to add item to the cart
function addItem (name, price) 
    item.push({
    name: name,
    price: price
  });
}
// This ensures that the function is usable
// outside this module
exports.total = function () {
    return items.reduce(function (a, b) {
    return a + b.price;
  }, 0);
};
// This also expose the addItem function
// outside this module 
exports.addItem = addItem;

Take a look at the code above, the first indication that it is a module is the exports keyword. As stated in the code comments, the exports keyword allows you to expose part of the code outside the module.

The shopping cart module is set up nicely. If you need to test it, you’ll need another JavaScript file, or you can use Node.js command line. For now, we’ll use the latter.

Now, do the following:

  • Navigate to the directory where you saved cart.js.
  • Open your command line (or terminal)

Launch a Node console by type the following:

node

After that, type the following:

let cart = require('./cart');

This will require the cart.js file. For a moment, let’s explain how it works.


More great articles from LogRocket:


When you read the previous code block, you’ll observe two things:

  • We added ./ before the file name.
  • We did not write the file extension when requiring the file.

Why? If you might ask. Well, cart.js is a module that you’ve created locally and is available somewhere on your system, you have to pass a relative path so that Node.js knows it’s not looking for a module inside a folder called node_modulesthat is the job of ./.

Secondly, you leave off the file extension and require() will look for a file named cart.js, which is the name of our file.

Now, back to the command line, let’s check the cart object:

cart
#Expected output: { total: [Function], addItem: [Function: addItem] }

Next, add some items:

cart.addItem('Intro to TypeScript', 50);
#Expected output: undefined

You can check the total:

cart.total()
#Expected output: 50

Go ahead and add another item:

cart.addItem('Intro to CSS', 45);

The total is now up to 95:

cart.total();
#Expected output: 95

This is how to create a basic module: you can create your functionality, and you can only expose the part of the code that you want other files (that require this file) to consume. The part is called an API (Application Programming Interface).

This is good, but we have a problem: there is only one shopping cart.

This is because you can’t require the cart again to get a second cart because when you later require an already-required file, the exports object is cached and reused meaning there is only one instance of the module, therefore, you only get one copy.

Let me show you what I mean. Switch back to your terminal and type the following:

second_cart = require('./cart');

Now you might think this will create a new shopping cart, but that is not the case. When you check the total, it’s still 95:

second_cart.total();
#Expected output: 95

When you need multiple instances of the shopping cart, you’ll need to create a constructor function inside the module. Why a constructor function?

That’s because it’s a function that’s created when you invoke a function with the new keyword in JavaScript, this means we’ll get a new shopping cart instance every time we create a shopping cart with new.

Create another file or re-write cart.js file to match the following:

// cart.js

// Create the constructor function
function Cart () {
    this.items = [];
}
// Add the functions to its property
Cart.prototype.addItem = function (name, price) {
    this.items.push({
        name: name,
        price: price
    });
}

Cart.prototype.total = function () {
    return this.items.reduce(function(a, b) {
        return a + b.price;
    }, 0);
};
// Export the function
module.export = Cart;

Now, when you require this module, the cart function is what gets returned instead of an object with the cart function as a property. Awesome!

Switch back to your console, then type the following:

Cart = require('./second_cart');
#Expected output: [Function: Cart]

Next, create two shopping carts by entering the command one after the other:

new_shopping_cart = new Cart();
#Expected output: { items: [] }

new_shopping_cart_2 = new Cart();
#Expected output: { items: [] }

Then add an item to new_shopping_cart as such:

new_shopping_cart.addItem('', 50);
new_shopping_cart.total(); #Expected output: 50

When you check the other shopping cart, the total remains zero.

new_shopping_cart_2.total();
#Expected output: 0

To recap:

  • Variables local to the module are private
  • In CommonJS, you use exports to expose the necessary functionality from a module, which is basically a JavaScript file
  • It’s clear Node.js implements CommonJS modules format, but it also has modules.export, which, is also used for exporting module functionality
  • Any file that intends to use a CommonJS module will use the require() function to read the file
  • Modules created with exports can only be required once

Advantages of CommonJS

  • It’s simple to use
  • Most tooling systems and packages that you’ll encounter are built around CommonJS
  • It’s implemented in Node.js; a popular JavaScript runtime environment

Disadvantages of CommonJS

  • You can have one module in a JavaScript file
  • Additional tooling is required if you want to use it in web browsers
  • Synchronous in nature and not suitable in some situations in web browsers

Asynchronous Module Definition (AMD)

One of the downsides of CommonJS is that it is synchronous. AMD aims to solve this by allowing the module and its dependencies to be loaded asynchronously via an API defined in the specification.

The AMD API is defined below:

define(id?, dependencies?, factory);

Where:

  • id: An optional argument which specifies the id of the module being defined. This is the path of the module
  • dependencies: This is an array of the modules that are dependencies required by the module being defined
  • factory: A function that is executed to instantiate the module or object. It creates a JavaScript closure, and it’s called once per module

Furthermore, when you want to use this defined module, you would need the require function defined in the specification as:

require(dependencies?, callback);

Where:

  • dependencies: An array of dependencies
  • callback: A callback function that’s executed when the modules are loaded

For a more detailed explanation of the AMD API, kindly check the AMD API specification on GitHub.

Now, this is all theory (including the specs), but I’ll like you to get a good understanding of how this theory works in code. The best way to do this is to explore actual implementations of AMD of which there are various, and it would be impractical to discuss them all.

Instead, we’ll explain a popular one among them: RequireJS; which describes itself as a “JavaScript file and module loader”.

Therefore, we will set up RequireJS, and I’ll show you (in code) how to use AMD with RequireJS.

Here is a quick rundown of what you’ll learn in subsequent sections:

  • How to set up RequireJS
  • Difference between AMD require() and CommonJS require().
  • How does require() work in AMD?
  • How to define an AMD module
  • How to define AMD modules with direct path names
  • How to define AMD modules with Common-JS require function
  • How to define AMD modules with CommonJS code
  • Other uses of the factory function
  • Precautions when defining modules in AMD
  • Advantages of AMD
  • Disadvantages of AMD

How to set up RequireJS

The set-up for RequireJS is hassle-free, all you have to do is navigate to RequireJS website and download it, then perform the following steps:

  • Create a project folder (with a name of your choice).
  • Create a subfolder named js.
  • Move the downloaded RequireJS file to the js. folder.
  • Create an index.html. file in root directory.

Then type the following code in index.html:

<!DOCTYPE html>
<html lang="en">
    <head>
     <meta charset="utf-8" />
     <title>Introduction to RequireJS</title>
    </head>
    <body>
        <!-- Your HTML code for the page -->

     <!-- include config and RequireJS file from the js folder-->
     <script data-main="js/config" src="js/require.js"></script>
    </body>
</html>

Where:

  • data-main="js/config": This is the entry point to RequireJS. It’s also where we configure it, in this case the config file resides in a folder named js
  • src="js/require.js": The normal way you load a script on a page here, it will load require.js

At this point, we have not created the config.js file, so, perform the following steps to create the config file:

  • Create a blank file with your editor
  • Save the file as config.js inside the js folder

In the previous code block, you’ll notice we did not include the file extension of the config file that is because in RequireJS we do not include the file extension of the JavaScript files because it’s designed to work with JavaScript files.

Therefore, when it sees config (and other JavaScript files) it knows it’s a JavaScript file.

Finally, we initialize the RequireJS by writing the following below the <script> tag which included the require.js (check previous code block):

<!-- index.html -->

<!-- Previous code remains the same -->
<script>
    require(['config'], function() {
        // Your code here.
    })
</script>

After your page loads the configuration file, the code in require() would run.

When you read the code above, I think you might ask the following questions:

  • I see require() here; How is it different from CommonJS require()?
  • How does require() work with the config file?

Let’s answer these questions.

Difference between AMD require() and CommonJS require()

The differences are given below:

  • AMD require() accepts an array of dependencies and a callback function, CommonJS require() accepts a module ID.
  • AMD require() is asynchronous while CommonJS require() is synchronous

Are we good? Let’s proceed.

How does require() work in AMD?

Before that, it’s best to explain the tag that loads RequireJS, here is it again:

 <script data-main="js/config" src="js/require.js"></script>

This script tag is an asynchronous call, this means when RequireJS loads via src="js/require.js, it will asynchronously load the config file specified in the data-main attribute therefore, any JavaScript code below this tag can execute while RequireJS fetches the config file.

To see this in action, switch back to your code editor, open index.html, and perform the following actions:

  • Comment the require() function
  • Add a script tag below the script tag that loads RequireJS
  • Add alert(); within the script tag
  • Save your file

Now, your index.html should look like the following

<!-- index.html -->
<!-- Other code remains the same -->
<script data-main="js/config" src="js/require.js"></script>
<script>
    alert();
</script>

When you load the file in your browser, the alert() function declared in the second <script> tag will execute on initial page load, this is before the config file is loaded.

Now before you click OK, please confirm the config file has not loaded by launching the browser Developer tools and switch to the Network tab. You’ll observe that the config file is yet to load, when you click OK, the config file gets loaded.

All this is depicted in the GIF image below:

Config File Loading

With that out of the way, here is how require() works with the config file (and other JavaScript code) behind the scenes:

  • It will create a <script> block with the src set to the file path of config.js as defined in the data-main attribute
  • Put the <script> block in the HTML <head> section
  • Wait for it to load. When it does, it fires the callback function

You can confirm this by saving your file; load it in your browser; then check the browser developer tools:

Browser Developer Tools

And you can verify that the function in require() is a callback function by modifying our code as such:

<!-- index.html -->
<!-- Remaining code remains the same -->

<script data-main="js/config" src="js/require.js"></script>

<!-- Add this script tag-->
<script>
    alert("Before config loads");
</script>

<!-- Add this script tag-->
<script>
    require(["config"], function () {
     alert("After config loads")
    });
</script>

Save your file, and load it in your browser. You’ll observe the following:

  • The alert(); function before require() will execute first.
  • The alert(); function within require() will execute after the config file has loaded.

This is shown in the GIF image below:

Alert Function Showing Before Config File Loads

How to define an AMD module

The following is a basic module definition in AMD:

define(['dependency1', 'dependency2'], function() {
    // Your module code goes here
});

This module definition clearly shows two dependencies and a function.

These dependencies are modules themselves that were created with the define() function, and they can also have dependencies themselves.

While the function is the factory function, and if it returns anything, that will be the exported value of the module. Mind you, as you’ll learn later, it does not have to return anything.

I know this might be confusing, so, here is what we will do:

  • Create a module with no dependency
  • Use the created module as a dependency of another module

Before that, you need to do some housekeeping. Please, do the following:

  • Create 2 folders in your js folder named customScripts and lib
  • Move require.js into lib folder
  • Switch to your index.html file and update the script tag src attribute value to point to the new location of require.js. Now it should be: src="js/lib/require.js"

Switch to your code editor, and type the following code and save it in your customScripts directory:

// js/customScripts/addition.js
/**
 * The module returns a function which
 * adds two numbers.
 */
define(function() {
    return function(a, b) {
        alert(a+b);
    }
});

Next, create another file in your customScripts directory and type the code below, save it as calculator.js.

// js/customScripts/calculator.js
/**
 * This module depends on the addition.js
 * module.
 */
define(['addition'], function(addition) {
    addition(7, 9);
});

When RequireJS sees the previous code block, it will go and look for the dependency and automatically inject it into your module by passing them as arguments into the function.

Behind the scenes, it creates a <script> tag for the addition.js and calculator.js file and places it in the HTML <head> element, waits for them to load, and it will run the function, this is similar to the behavior of require(). If you’d like to confirm, check the browser developer tools.

Browser Developer Tools Script Tag

Now, save your files, navigate to the index.html file, and update the require() function to match the following:

// index.html
require(['config'], function() {
    require(['calculator']);
});

Now, load your index.html file in your browser, nothing happens. How come? Well, this is what happened:

  • RequireJS attempts to find the calculator.js module
  • It did not find it because it had no idea where to look for it

How do we fix this? We have to tell RequireJS where to find calculator.js (and other modules) via the config file because it is the entry point of your application.

Before we write the code that’ll make our application work, it’s best if we explain the basic structure of the config file as shown in the code block below:

requirejs.config({
    baseURL: "string",
    paths: {},
    shim: {},
});

Where:

  • baseURL : This tells RequireJS where to find your modules
  • paths : These are names of the modules that’ll be used with define(). Inside the paths , you can also use a CDN version of the file, when you do this RequireJS will attempt to load the CDN version of your module before the one available locally
  • shim: This allows you to load libraries that are not written as AMD modules and lets you load them in the correct order

We’ll use the baseURL and paths in our application and no shim.

Armed with the knowledge on what to write in the config file, switch to your config file and update it as such:

// js/config.js
requirejs.config({
    // The folder which contains your js scripts and modules.
    baseURL: "js",
    paths: {
        /**
        * Location of your modules, In this case,
        * these modules are in the customScripts folder. Note,
        * there is no need for the "js" extension.
        */
        addition: "customScripts/addition",
        calculator: "customScripts/calculator",
        /**
        * If the "addition" module (or any module) is being
        * retrieved from a CDN (Content Delivery Network,
        * and also, you have a local copy, you'll write
        * it as such. Don't forget to leave off the "js"
        * extension
        addition: [
            "https://your-cdn-domain/addition",
            "customScripts/addition"
            ],
        */
    },
});

That’s it. Now, reload your browser, you should get an alert of the result.

Page Says 16 Alert

Well, that’s how you define and work with modules in AMD with RequireJS, but it’s one of the ways to define modules in AMD with RequireJS, there are others. Read on.

Define modules with direct path name

You can also define modules by specifying its path name, which is the location of the module file in your project(s) directory. An example is giving below:

/**
 * The "path/to/module" is the direct location
 * that tells RequireJS where to find the module.
 */
define("path/to/module", function() {
    // Your module code here.
})

This approach is discouraged by RequireJS because when you move your module to another location in your project you need to go back and change the path name in the module by hand.

Define AMD modules with Common-JS-like require function

In AMD, you can write your modules in a syntax similar to CommonJS using the require() function as a dependency therefore, we can rewrite the previous calculator.js as:

/**
 * This is known as dynamic loading, which
 * means the code executes at runtime.
 */
define(function (require) {
    const additionModule = require("addition");

    additionModule(7, 9);
});

Define AMD modules with CommonJS code

In previous paragraphs you’ve learned how CommonJS modules expose their code via the exports object. You might find it surprising that it’s possible to use this syntax to define your AMD modules. RequireJS describes this technique as “simplified CommonJS wrapper“.

The syntax of the wrapper is:

define(function(require, exports, module) {
    // CommonJS code.
})

RequireJS docs states that exports and module can be omitted if they are not used, but, if you’ll use them, they should be written in this same order otherwise “trouble will ensue“. On the other hand, require can not be omitted.

As an example, let’s take the shopping cart example implemented earlier in this article, and we’ll rewrite it to work as an AMD module.

// customScripts/AMDShoppingCart.js

// Define AMD module with CommonJS code.
define(function (require, exports, module) {
    // CommonJS code.
    var items = [];

    function addItem(name, price) {
            items.push({
              name: name,
              price: price,
            });
    }

    function total() {
            return items.reduce(function (a, b) {
             return a + b.price;
          }, 0);
    }

    exports.addItem = addItem;
    exports.total = total;
    
    /**
    * In RequireJS, you can also use the following
    * to export the module functionality. It works the
    * same.
     
    return {
            addItem,
            total,
    }; */
    
});

Then, you can use AMDShoppingCart as such:

// customScripts/UseShoppingCart.js

// Use AMD module with CommonJS code.
define(function(require) {
    // CommonJS code.
    const shoppingCart = require("AMDShoppingCart");
    
    // Add items to the shopping cart
    shoppingCart.addItem("first", 20);
    shoppingCart.addItem("second", 90);
    shoppingCart.addItem("third", 5);

    alert(shoppingCart.total()); // 115
});

At this stage of the article, I believe you already know how to set up RequireJS to use and work with these files, if not, please refer to the section: How to define a module.

Moving on, save your files and launch your browser, you’ll get an alert of 115 as the total of the items in the shopping cart.

You should know there is only one instance of the shoppingCart. If you would like to see this in action, modify UseShoppingCart.js as such:

// customScripts/UseShoppingCart.js

// Use AMD module with CommonJS code.
define(function(require) {
    // Add this below shoppingCart
    const shoppingCart2 = require("AMDShoppingCart");
    
    // Code truncated, check previous code block
    
    // check its total
    alert(shoppingCart2.total()); // 115
});

As demonstrated earlier, if you need multiple instances of the shopping cart, you can use constructor functions.

Modify AMDShoppingCart.js to use constructor functions.

// customScripts/AMDShoppingCart.js

// Define AMD module with CommonJS code using
// constructor functions.
define(function (require, exports, module) {
   function Cart() {
        this.items = [];
    }

    Cart.prototype.addItem = function (name, price) {
        this.items.push({
            name: name,
            price: price,
        });
    };

    Cart.prototype.total = function () {
        return this.items.reduce(function (a, b) {
            return a + b.price;
        }, 0);
    };

    module.exports = Cart;
});

Then modify UseShoppingCart.js:

// customScripts/UseShoppingCart.js

// Use AMD module with CommonJS code.
define(function(require) {
    // CommonJS code.
    const shoppingCart = require("AMDShoppingCart");
    
    let a = new shoppingCart();
    let b = new shoppingCart();

      a.addItem("first", 20);
      a.addItem("second", 90);
    
    // Check the carts total
      alert(a.total()); // 110
    alert(b.total()); // 0
});

Other uses of the factory function

In the last two sections, you learned how the factory function was leveraged to mimic and create CommonJS code, well, it does not end there.

Here are other things you can do with the factory function:

  • Return an Object
  • Return a function (as seen in our examples)
  • Return constructors
  • Create private variables and functions because the factory objects basically create a closure

Precautions when defining module in AMD

Humans are not perfect, but still, we strive to be, which, in reality does not prevent us from making mistakes. When you are defining modules in AMD, please be aware of the following:

  • Whatever you list in your array of dependencies must match the assignment in the factory function
  • Do your best not to mix asynchronous code with synchronous code. This is the case when you write other JavaScript code on the index.html

Everything created by man has its good and bad, let’s have a look at some of AMDs.

Advantages of AMD

  • Asynchronous loading leading to better startup times
  • Ability to split modules into multiple files
  • Support for constructor functions
  • Works in the browser with no additional tooling

Disadvantages of AMD

  • The syntax is complex
  • You need a loader library like RequireJS to use AMD

We did not cover everything related to AMD with RequireJS, but what we’ve covered is enough to give you a strong foundation. If you would like to learn more about RequireJS, I’ll only point you to one place: RequireJS official documentation.

Universal Module Definition (UMD)

CommonJS was designed for server-side JavaScript therefore, its code does not play nice in Web browsers without some tooling or bundling. On the other hand, AMD was designed to work on the client side (Web browsers) with no tooling involved. In addition, some implementations of AMD notably RequireJS allow you to convert your CommonJS code to AMD code using a tool like r.js.

Hence, the need arises for a “universal” approach to get everyone on board, so UMD was born. UMD stands for Universal Module Definition.

UMD is a set of patterns that supports both AMD and CommonJS, it is designed to make your code work irrespective of the environment executing the code. This could be on the client side with RequireJS or the server side with Node.js.

At first sight, you might find UMD’s syntax overwhelming or as someone called it “ugly”, but still, as you’ll learn later, you can take a UMD module and run it successfully in the browser, RequireJS and Node.js (with no tooling or bundling).

UMD achieves this feat by checking the environment that is implementing (or calling) the module before deploying the module functionality, in Web browsers, it makes the module functionality available as a global variable.

As I mentioned earlier, reading a UMD module can be overwhelming, therefore, I won’t throw a complete UMD module at you, so that you’ll go figure it out yourself, instead, I’ll break it down to show you what makes it tick, and how to use it.

Defining a UMD module

A UMD module consists of two parts:

  1. An Immediately Invoked Function Expression (IIFE): This checks the environment that wants to use the module. It takes two arguments: root and factory. root is a this reference to the global scope and factory is the function where we define your module
  2. An anonymous function: This creates our module, and it’s passed as the second argument to the IIFE discussed in 1. In addition, this anonymous function is passed any number of arguments to specify the dependencies of the module

In code:

// The Immediately Invoked Function Expression
(function(root, factory) {
    // Here, you check the environment that want's
    // to use the module, and you'll write
    // the necessary code that will make the code
    // work in that environment.
    
    // In this case the environment could be Node.js
    // RequireJS (AMD) or a Web browser.
}(this, function() { // The anonymous function, It'll take arguments which are dependencies of the module being defined.
    // Your module code will reside here
}));

There you go, that’s the structure of a UMD module, but still, it does nothing. Let’s fix that.

The code block below defines a UMD module which simply returns the phrase A UMD module.

// The IIFE
(function(root, factory) {
    // The module made available as a global variable.
    // Here, the module name is myUMDModule
    root.myUMDModule = factory();
}(this, function() { // The anonymous function.
    // Our module code
    return {
            name: 'A UMD module'
    }
}));

Please, the module takes no environment into consideration hence, at this stage, it will work only in a web browser.

The next challenge you might face is: How do I use this module? Please, do the following:

  1. Copy the code above
  2. Launch your web browser
  3. Open your browser developer tools (Ctrl + Shift + I on Windows, Cmd + Shift + I on Mac) and navigate to Console
  4. Paste the code and hit enter on your keyboard

Next, start typing the module name, i.e., myUMDModule in the browser console, you’ll see it’s available as a global variable as seen in the screenshot below:

myUMDmodule Showing as a Global Variable

The next question: How do I use it in an environment that supports AMD e.g. RequireJS?

You can do that by adding a few lines of code that performs some checks for an AMD environment.

// Save as UMDwithAMD.js
(function (root, factory) {
    
    // Add the following
    if (typeof define === "function" && define.amd) { // checks for an AMD environment
     
            // If true, create the module using the define
            // function, and use the function passed as an
            // argument to this IIFE as the factory function.
            define(factory);
     
    } else { // We are in the browser
            // Create the module as a global object
            // which is useful for web browsers
            // without the need for RequireJS.
            root.myUMDModule = factory();
    }
    
})(this, function () { // The anonymous function.
    // Our module code
    return {
            name: "A UMD module",
    };
});

If you want to test this code in RequireJS, you can take two routes. Route 1: create a file that uses this module as a dependency. Route 2: use this module on the index.html page via require(). Let’s see the second route before the first.

Once again, if you’ve read this article up to this point, I believe you can set up your AMD modules in RequireJS. For revision purposes, here is how you’ll set up UMDwithAMD.js:

  1. Save it in the customScripts directory created earlier
  2. Update the config file and specify the location of UMDwithAMD.js

Now, on index.html, modify the require() function to match the following:

// Part of index.html

// The script tag to use RequireJS should come
// before this code.
require(["config"], function () {
    require(['UMDwithAMD'], function(UMDwithAMD) {
            alert(UMDwithAMD.name); // A UMD module
    });
});

Save your files, launch (or refresh) your browser and the output should be: A UMD module.

The second route: Create a UMD module that will depend on UMDwithAMD . Switch to your code editor and save the next code block as UseAMDwithAMD.js.

// Save as UseUMDwithAMD.js
(function (root, factory) {
    if (typeof define === "function" && define.amd) { // checks for an AMD environment
        // Note: The define function now uses
        // the UMDwithAMD as a dependency
     define(["UMDwithAMD"], factory);
    } else { // we are in the browser (root is window)
     root.myUMDModule = factory();
    }
})(this, function (UMDwithAMD) {
    // Use UMDwithAMD
    alert(UMDwithAMD.name);
});

Next, modify the require() function on the index.html to match the following:

require(["config"], function () {
    require(['UseUMDwithAMD']);
});

Save your files and refresh your browser. Take note of output. Still, it should read: A UMD module.

Now, you might ask: Will UseUMDwithAMD.js and its dependency UMDwithAMD.js work in a web browser without RequireJS? Yes! it would. Perform the following actions:

  1. Create a new project folder and call it learningUMD
  2. Create an index.html file with the structure of an HTML5 document and save it in this folder
  3. Copy UseAMDwithAMD.js and UMDwithAMD.js into this folder. (Now, you have 3 files in your project folder)
  4. Create 2 script tags in the head section of index.html
  5. The src attribute of the first script tag must point to UMDwithAMD.js because UseUMDwithAMD.js depends on it, so it needs to load first
  6. The src attribute of the second script tag must point to UseUMDwithAMD.js because it is the dependent
  7. Within the anonymous function inside UseUMDwithAMD.js, you’ll need to change alert(UMDwithAMD.name) to alert(myUMDModule.name) because we want to test the dependent module in a Web browser environment hence, the name property will only be available in the global variable myUMDModule. We can’t use alert(UMDwithAMD.name) because we are not testing in an AMD environment. If you do, you’ll get undefined

In the end, your new HTML file should like such:

<!DOCTYPE html>
<html lang="en">
    <head>
     <meta charset="utf-8" />
     <title>Introduction to UMD</title>
     <script src="UMDwithAMD.js"></script>
     <script src="UseAMDwithAMD.js"></script>
    </head>
    <body></body>
</html>

And UseUMDwithAMD.js will look like:

/**
 * Save as UseUMDwithAMD.js (modified to display an
 * alert in a Web browser environment)
 */
(function (root, factory) {
    // Code truncated
})(this, function (UMDwithAMD) { // You can leave the argument, it does no harm.
    // Use UMDwithAMD
    alert(myUMDModule.name); // Note, we are using myUMDModule and not UMDwithAMD
});

Now, launch your browser and watch the alert display the phrase: A UMD module.

So far we’ve shown how a single module: UMDwithAMD.js works in the browser, and in AMD with RequireJS with no modifications whatsoever. What is next is to see if it will work in Node.js with no modifications. Are you ready? Let’s find out.

Before that we need to add one more check to UMDwithAMD.js to see if we are in a Node.js environment. Update UMDwithAMD.js to match the following:

// Save as UMDwithAMD.js
(function (root, factory) {
    // Add the following
    if (typeof define === "function" && define.amd) {
            define([], factory);
    } else if (typeof module === "object" && module.exports) { // Check for Node.js environment
        // Does not work with strict CommonJS, but
        // only CommonJS-like environments that support
        // module.exports, like Node.
            module.exports = factory();
    } else {// We are in the browser
            // Create the module as a global object (root is window)
            root.myUMDModule = factory();
    }
})(this, function () {
    // The anonymous function.
    // Our module code
    return {
            name: "A UMD module",
    };
});

Now, switch to the command line and perform the following steps:

  1. Navigate to the directory where you saved UMDwithAMD.js
  2. Type node to launch the Node.js console
  3. Type myModule = require('./UMDWithAMD.js')
  4. Finally, check for the name property

The previous steps (and the result) is shown in the image below:

Name Property in Node.js

There you have it, one UMD module successfully worked in Node.js, AMD with RequireJS, and the web browser which solidifies the word “universal” in the pattern name.

You should know I did not make up the way we checked for multiple environments in the module code. The UMD GitHub repo has some UMD templates you can use depending on the environments and what you are trying to achieve.

Here is a quick rundown of the templates and recommended usage as stated in their code comments:

UMD template Recommended usage
amdWeb.js Useful when you want to create a UMD module with AMD or browser globals
amdWebGlobal.js This is useful if you have some scripts that are loaded by an AMD loader, but they still want access to globals
commonjsAdapter.js This can be used as a common boilerplate for a library module that you only want to expose to CommonJS and AMD loaders. It will not work well for defining browser globals
commonjsStrict.js Used to create a module with CommonJS, AMD or browser globals
commonjsStrictGlobal.js This is useful if you have some scripts that are loaded by an AMD loader, but they still want access to globals
jqueryPlugin.js To create some jQuery plugin
nodeAdapter.js This can be used as a common boilerplate for a library module that you only want to expose to Node and AMD loaders
returnExports.js Create a module with Node, AMD or browser globals
returnExportsGlobal.js Uses Node, AMD or browser globals to create a module. This example creates a global even when AMD is used. This is useful if you have some scripts that are loaded by an AMD loader, but they still want access to globals

When you observe the code for any of the templates listed above, you’ll notice they all support web workers by checking the existence of the self variable else it defaults to this variable. Therefore, we can also include this check in our UMDwithAMD.js code as such:

// UMDwithAMD.js (without comments).
(function (root, factory) {
    if (typeof define === "function" && define.amd) {
            define([], factory);
    } else if (typeof module === "object" && module.exports) {
            module.exports = factory();
    } else {
            root.myUMDModule = factory();
    }
})(typeof self !== "undefined" ? self : this, function () { // Notice the modification on this line
    return {
            name: "A UMD module",
    };
});

To cap it all off, here is the shopping cart implemented earlier in this article, but this time it is written in UMD.

// ShoppingCartAMD.js
(function (root, factory) {
    if (typeof define === "function" && define.amd) {
            define([], 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();
    } else {
            // Browser globals (root is window)
            root.shoppingCartUMD = factory();
    }
})(typeof self !== "undefined" ? self : this, function () {
    
    var items = [];

    function addItem(name, price) {
            items.push({
             name: name,
             price: price,
          });
    }

    function total() {
            return items.reduce(function (a, b) {
              return a + b.price;
          }, 0);
    }
    
    // Return the addItem and total functions.
    return {
            addItem,
            total,
    };
    
    // If you like to create multiple instance of the
    // shopping cart, you can use a constructor function.
    // function Cart() {
    //  this.items = [];
    // }
    // Cart.prototype.addItem = function (name, price) {
    //  this.items.push({
    //      name: name,
    //      price: price,
    //  });
    // };
    // Cart.prototype.total = function () {
    //  return this.items.reduce(function (a, b) {
    //      return a + b.price;
    //  }, 0);
    // };
    // return Cart;
});

You can test the code in the browser, and Node.js to confirm it works, and if you’d like to use it in RequireJS, use directly with require() function on the index.html file (created earlier in this article), or create another UMD module that will use it as a dependency. An example is given below:

// UseShoppingCartAMD.js

// Please note, this file use shoppingCartUMD.js
// as a dependency.
(function (root, factory) {
    if (typeof define === "function" && define.amd) {
     // AMD. Register as an anonymous module.
     define(["shoppingCartUMD"], 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("shoppingCartUMD"));
    } else {
     // Browser globals (root is window)
     root.returnExports = factory(root.shoppingCartUMD);
    }
})(typeof self !== "undefined" ? self : this, function (shoppingCartUMD) {

    shoppingCartUMD.addItem("computer", 20);
    shoppingCartUMD.addItem("mouse", 5);
    shoppingCartUMD.addItem("USB drive", 10);
    alert(shoppingCartUMD.total()); // 35
    
    // If you did use constructor functions in
    // calculator.js, you'd use the shopping cart
    // as such.
    
    // let a = new shoppingCartUMD();

    // a.addItem("computer", 20);
    // a.addItem("mouse", 5);
    // a.addItem("USB drive", 10);
    // alert(a.total()); 35
});

That’s UMD for you, JavaScript modules that work everywhere.

Advantages of UMD

  1. It works everywhere (web browser, Node.js, AMD with RequireJS)

Disadvantage of UMD

  1. The syntax can be overwhelming

ES Modules

If you are a frontend developer or just getting started with modules in JavaScript, you might find the code of UMD or AMD (with RequireJS) difficult to work with or understand.

Fortunately, TC39, the committee in charge of ECMAScript specification has got you covered. In 2015, the committee added modules as a built-in feature in JavaScript. Today, these modules are called ES modules short for ECMAScript modules, and if you hear anyone saying modules, there is a high chance they are talking about ES modules.

I’m pretty sure they took the syntax of UMD and AMD module systems into consideration, hence, the syntax of ES modules are approachable. Moreover, if you’ve worked with a programming language like PHP or Python, I think you’ll understand ES modules quickly, and if not, when you read ES module code, you can guess the outcome of the code.

Moving forward, I’ll refer to ES module simply as module.

Defining and ES module

The export keyword is the bedrock for defining a module, and any file that intends to use this module will import the exported module code using the import statement (discussed later). Therefore, a module is nothing more than a JavaScript code that exports its own code.

When you want to export part of your module code, you need to add the word export before it. This part could be a variable, function, or a class. You should know, any part of the code that’s not exported is private to the module and can not be accessed outside that module.

As an example, here is a module which exports a function called getMyName:

// getMyName.js

// Remains private
let myName = "Habdul Hazeez";

// Exposed to the world, or anyone
// who uses this file.
export function getMyName() {
    return myName;
}

Consequently, any JavaScript file that intends to use this module will use it as such:

import { getMyName } from './getMyName.js'

console.log(getMyName); // Habdul Hazeez

Now, for a moment, think of how you’ll define and use this module in UMD or AMD (trust me, I know that feeling). That’s the beauty of a module, the syntax is leaner and easy to learn.

I say “easy”, but we need not get too far ahead of ourselves. The last two code blocks show you how to define a module and how to use it, but as it stands, I’ll like to ask you two questions:

  • Can you explain it to another person or yourself?
  • Can you export and import a module in another fashion?

Maybe, but I am not taking any chances. I’ll break it down and explain the following:

  • How to use modules in your web applications
  • How to export and import a module
  • Misconception about named import
  • Module specifiers
  • Modules are singletons
  • Imports are live
  • How to use modules in Node.js
  • Characteristics of modules
  • Advantage of ES modules

How to use modules in your web applications

In order to use modules in your web applications, you’ll need the normal script tag in your HTML file with an additional attribute. But before that, let’s set up a project folder by performing the following actions:

  • Create a project folder (with a name of your choice).
  • Create a folder named js in this folder.
  • Open your code editor, and create a new index.html file.

Now, type the following in index.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0" >
    <title>Introduction to ES modules</title>
</head>
<body>
</body>
</html>

Still in your code editor, create a new JavaScript file in the js folder, save it as addition.js and type the following code.

// js/addition.js

// A function that adds two numbers
export function addition(a, b) {
    return a + b;
}

The key thing to observe is the export keyword before the function declaration. This means two things:

  1. The file is a module
  2. The function is made available to any file that imports this module

In addition, this style of export is called an inline export. When I said “this style,” that means there is another “style”. We’ll discuss that later.

The module is ready, but we’ll need another JavaScript file to use this module, therefore, create another JavaScript file and save it as useaddition.js, then type the following code:

// useaddition.js
import { addition } from './addition.js';

alert(addition(2, 5));

Switch back to the index.html file, and add a script tag towards the end of the file:

<!-- Code truncated -->
<body>
    <script src="js/useaddition.js"></script>
</body>
<!-- Code truncated -->

At this stage, useaddition.js is the entry point of your application. Save all files, and launch your browser and open index.html. On a normal day, you should get an alert message, reality is: You won’t get any alert message.

When you encounter an error in your JavaScript apps, the first place you should look is the web browser console located in the developer tools, you should see an error similar to the one in the image below, it’ll read Uncaught SyntaxError: Cannot use import statement outside a module.

Uncaught Syntax Error

The question is why? Well, the src attribute of the script tag on index.html points to a JavaScript file (useaddition.js), therefore, when the browser reads useaddition.js it discovers an import statement, which on a normal should occur only in a module, but the browser does not recognize useaddition.js as a module, and it will stop reading the file and report the error.

Do you remember the additional attribute I told you about? It is type="module" which basically tells the browser that the linked JavaScript file is a module. Now, modify the script tag in your index.html as such:

<!-- Code truncated -->
<body>
    <!-- Note the added attribute in the script tag -->
    <script type="module" src="js/useaddition.js"></script>
</body>
<!-- Code truncated -->

Save all files and refresh your browser. No alert message whatsoever, check the Console in the Developer tools, yet another error.

Another Error

This time it’s a CORS error. CORS stands for Cross-Origin Resource Sharing which is an HTTP-header mechanism that allows a server to indicate other origins which a browser should permit loading resources from. These origins could be a domain, scheme, or port.

For security reasons, browsers restrict cross-origin HTTP requests initiated from scripts. This is exactly what useaddition.js is trying to do.

First, useaddition.js is a JavaScript file attempting to fetch a resource (addition.js) via an HTTP request using the file:/// protocol, this is considered a security risk, as a result the browser blocks the request. Furthermore, it states the origin is null, because it does not consider a script file from a browser as an acceptable origin that will request a resource.

The fix is to change the origin, and as stated earlier, this origin could be a domain, scheme or port. In our case, we’ll use a scheme which is another word for a protocol such as http or https, and the only way we can use such protocol is if we put our code on a server.

There are several ways to do this among which are:

  1. GitHub pages (free)
  2. Netlify (free)
  3. localhost (free, but it’s a server on your computer)

If you are like me, go for the last one. You can get a server like Apache when you install XAMPP. Afterwards, do the following

Where:

  • projectfolder is the name of the folder you created earlier.

If you’ve done everything correctly, the module should work, and you’ll get an alert message in your web browser.

Alert Message in Browser

Now, that we have an environment that allowed the module to work (as shown in the image above), please, keep the environment live, and let’s explore other ways we can export and import modules, because as you might have noticed, how we exported and imported the module from addition.js is just one of the ways you can work with modules in your JavaScript applications.

How to export and import a module

In the previous section, you’ve learned how to export and import a module, and I did mention it’s just one of the ways of working with modules in JavaScript. There are others listed below:

  • Named export and Named Import
  • Default export and Default Import
  • Default export as a named export
  • Rename Export and Name Import
  • Export list + Rename and Import List + Rename
  • Named export + Default export and Named import + Default import
  • Namespace import
Named export and Named import

This means whatever you are exporting from your module should have a name. This name could be a variable name, class name or function name. Later, when you import this module, you’ll access this export via the name.

This is what we did in the addition.js file, for more clarity, here is it again (in a truncated version):

// js/addition.js

// "addition" is the name of the export
// and it's what is used by any file
// importing this module.
// This exporting style is known as inline.
export function addition(a, b) {}

When you want to import it, you’ll have to surround the exported name with a pair of curly braces, this means you are reaching into the module to get the exported name. Again, here is the import statement from useaddition.js:

// useaddition.js

// Note the curly braces around the
// function name
import { addition } from './addition.js';

// code truncated
Default export and Default import

As the name implies, this means the value exported from your module is the default one. You add the default keyword before whatever you want to export. It’s recommended to only have one default export per module.

Now, switch to addition.js in your code editor then add default before the function as shown below:

// js/addition.js

// The addition function as a default
// export
export default function addition(a, b) {
    // code truncated
}

Save the file and refresh your browser (preferably hard refresh: ctrl + Shift + R). You should get an error similar to what’s shown in the image below.

Uncaught Syntax Error After a Refresh

The cause of the error is in useaddition.js, since function addition is now a default export, you need to change the syntax of the import statement by removing the curly braces, and you should be left with something as such:

// useaddition.js

// Importing the addition function as a default
// export
import addition from './addition.js';

// code truncated

Save your file and refresh your Web browser. Everything should work.

Default export as a named export

JavaScript allows you to rename an export as an alias using the as keyword. Thereafter, you can import this export using this alias. But when you export part of a module, and you specify it as a default using the default keyword, you will use the export name when importing the module and not the keyword default.

Therefore, you can write the default export in addition.js from the last section as a named default export and everything should work.

// js/addition.js

// Rewritten to used a named export
// on a default export
function addition(a, b) {
    return a + b;
}

// addition as a named default export
export {
    addition as default,
};

The way we exported addition is the other “style” of export I mentioned earlier, it’s called an export clause.

Please note, If you write your import statement in useaddition.js as such it will not work:

// This is an INVALID import statement, and will
// result in an error stating:
// Uncaught SyntaxError: Unexpected token 'default'
import default from "./addition.js";

alert(default(2, 5)); // The code won't execute to this line.
Rename Export and Name Import

As stated in the last section, you can rename your export and use the new name as the exported name when importing it.

// js/addition.js

// Rewritten to used a named export
// on addition
function addition(a, b) {
    return a + b;
}

// This is called an export clause
// The function "addition" is now available as "sum"
// for any file that imports this module.
export {
    addition as addNumbers,
};

Then you’ll use it in useaddition.js as:

// useaddition.js

// This would FAIL because we exported
// addition as "addNumbers".
// import { addition } from "./addition.js"

// Importing a renamed export
import { addNumbers } from "./addition.js";

alert(addNumbers(2, 5));
Export list + Rename and Import List + Rename

You can export and import multiple functionalities of your module with or without renaming them. If you do rename the export, you’ll use the new name when importing them.

To demonstrate this, let’s use our good friend in this article: the shopping cart.

// js/cart_esmodules.js
var items = [];

function addItem(name, price) {
    items.push({
     name: name,
     price: price,
    });
}

function total() {
    return items.reduce(function (a, b) {
     return a + b.price;
    }, 0);
}

// Export multiple items as a list.
// Note, addItem is renamed to addToCart
export {
    total,
    addItem as addToCart,
}

When importing we can use the exported them, or rename them if we want. Create a new JavaScript file and save it with the name use_esshoppingcart.js and type the following code.

// js/use_esshoppingcart.js

// Note, total is renamed to cartTotal.
import {
    addToCart,
    total as cartTotal
} from "./cart_esmodules.js";

addToCart("USB drive", 20);
addToCart("Keyboard", 10);

alert(cartTotal()); // 30

Next, modify the index.html file to make use_esshoppingcart.js as the point of entry to the application.

<!-- Code truncated -->
<body>
    <!-- Note, the src attribute value has changed -->
    <script type="module" src="js/use_esshoppingcart.js"></script>
</body>
<!-- Code truncated -->

Save your files and refresh your browser. Your output should resemble the image below.

Alert Message 30

Namespace import

Though you’ve learned how to import module code using their name or by renaming them, namespace import allows you to import all module exports at once. When you do this, the namespace import becomes an object whose properties are the named exports.

The two key ingredients of a namespace import are:

  • * symbol: Which in this context, means “everything”
  • An alias: This would be the object which is called the namespace

Still using use_esshopingcart.js, modify it to use namespace import.

// js/use_esshoppingcart.js

// Now, we are using a namespace import
// to import all the module exports.
// This line means, "import everything from
// cart_esmodules.js" as ShoppingCart.
import * as ShoppingCart from "./cart_esmodules.js";

// add items to the cart
ShoppingCart.addToCart("USB drive", 20);
ShoppingCart.addToCart("Keyboard", 10);

alert(ShoppingCart.total()); // 30

// You can test ShoppingCart is an object
// by writing the following code.
// alert(ShoppingCart instanceof Object); // true

Save it and test it by refreshing your browser.

Named export + Default export and Named import + Default import

This approach is possible, but it’s not recommended.

// js/cart_esmodules.js

// code truncated

// The addition function is exported
// as a default and the total function
// is exported as a renamed export.
export {
    addItem as default,
    total as cartTotal
};

And in use_esshoppingcart.js you’ll write the import statement as such:

// js/use_esshoppingcart.js

// Importing a default export and a named
// export.
import addItem, { cartTotal } from "./cart_esmodules.js";

// code truncated
Dynamic import

All the import statements that we’ve discussed so far is known as a static import, and it has its limitations. It must be at the top level of the module, and you cannot use it inside a block.

In dynamic import, you can use the import() operator to load code “on demand,” thereby getting around the constraint posed by a static import where you have to put the import statement at the top level of the module.

We’ll use addition.js and useaddition.js for this example. Mind you, we’ll use the last modified version of addition.js which was made in section Rename export and Name import, but here is it again (without the comments):

// js/addition.js
function addition(a, b) {
    return a + b;
}

export {
    addition as addNumbers,
};

Next, we’ll rewrite useaddition.js using dynamic import.

// js/useadditon.js

// The directory of the file specified
// as a constant. In this case it's just
// a relative path because useaddition.js and
// addition.js are in the same directory. i.e. the
// js directory
const dir = "./";

// Concatenate the directory name to the
// module name,
const moduleSpecifier = dir + "addition.js";

function loadAdditionModule() {
    // The "addition" parameter to 'then' is NOT
    // the exported name of the module from
    // addition.js, instead, the exported function
    // is "addNumbers".
    //
    // Note the import() operator below.
    return import(moduleSpecifier).then(function (addition) {
     const result = addition.addNumbers(2, 5);
     return alert(result);
    });
}

loadAdditionModule(); // 7

Update the index.html file to make usadditon.js as the entry point of your application and refresh your browser, you should get an alert message of 7.

Please note, if addition was exported as a default value in addition.js as such:

// js/addition.js

// code truncated

export {
    addition as default,
};

You’ll use default in your dynamic import. Therefore, useadition.js with dynamic import would like such:

// js/useadditon.js (working with a default export)
const dir = "./";

const moduleSpecifier = dir + "addition.js";

function loadAdditionModule() {
    return import(moduleSpecifier).then(function (addition) {
     const result = addition.default(2, 5); // Note the "default" on this line
     return alert(result);
    });
}

loadAdditionModule(); // 7

Personally, I would not recommend this because readability goes out the window when another person or your future self tries to figure out what default means in this context.

Misconception about named import

If you’ve done destructuring in JavaScript before, you might think a named export is destructuring, it’s not, though they look similar.

import { cartTotal } from "./cart_esmodules.js"; // named import
let [a, b] = [20, 30]; // destructuring

In addition, renaming is different:

// A renamed import
import { cartTotal as ShoppingCartTotal} from "./cart_esmodules.js"; // named import

// Destructuring with renaming variables
let myDetails = {first_name: "Habdul", last_name: "Hazeez"};
// Rename first_name and last_name
let {first_name: FirstName, last_name: LastName} = myDetails;

// FirstName: Habdul, LastName: Hazeez

Module specifiers

Module specifiers are the strings that identify modules e.g. ./cart_esmodules.js. Their behavior is different in Web browsers and Node.js. The following are the categories of module specifiers:

  • Relative path: This is how we specified cart_esmodules.js, it starts with a dot. An example is ./cart_esmodules.js
  • Absolute path: It starts with a forward slash (/), followed by a directory name. e.g. /js/cart_esmodules.js
  • URL: You add a protocol to the string that specifies the module. e.g. http://localhost/esmodules/js/cart_esmodules.js
  • Bare path: This starts with no symbol like and consists of a single file name. e.g cart_esmodules.js. Currently, not supported in web browsers
  • Deep imports path: Starts with directory name, followed by at least a slash. e.g js/cart_esmodules.js. Not yet supported in web browsers

In Node.js specifiers works as follows:

  • Relative path: Work the same as in web browsers
  • Absolute path: Not supported. But you can use the file protocol file:///
  • For URL, only file: is supported
  • Bare path is interpreted as a package name and resolved relative to the closest node_modules directory
  • Deep import paths are also resolved to the closest node_modules directory

Modules are singletons

Similar to CommonJS and AMD, modules are singletons. This means if you import a module multiple times, only a single instance of it exists.

To drive this point home, switch to your editor and modify use_esshoppingcart.js to match the code below (do not save it yet, read on):

// js/use_esshoppingcart.js

import * as firstShoppingCart from "./cart_esmodules.js";
import * as secondShoppingCart from "./cart_esmodules.js";

In the previous code block, cart_esmodules.js was imported twice as a namespace import in firstShoppingCart and secondShoppingCart therefore, both shopping carts are objects whose properties are the named exports of the module (refer to section: Namespace import).

Now, you might think firstShoppingCart and secondShoppingCart are different because in JavaScript two objects are not equal, the reality is: firstShoppingCart and secondShoppingCart is the same object.

To confirm that, create two objects in cart_esmodules.js as shown in the code block below:

// js/use_esshoppingcart.js

// Previous import statements remains the same

// create two objects ( not necessarily with my name :) )
let firstObject = {
    first_name: "Habdul",
    last_name: "Hazeez",
};

let secondObject = {
    first_name: "Habdul",
    last_name: "Hazeez",
};

When you test the equality of firstObject and secondObject , you’ll realize they are not the same. Add the following to cart_esmodules.js:

// js/use_esshoppingcart.js
// previous code remains the same

alert(firstObject === secondObject); // false

Save your file, ensure the script tag on inex.html points to the location of use_esshoppingcart.js. Launch (or refresh your browser). The alert message should read false.

Now, it’s time to test for the equality firstShoppingCart and secondShoppingCart.

Add the following code:

// js/use_esshoppingcart.js
// previous code remains the same

alert(firstShoppingCart === secondShoppingCart); // true

That proves modules are singletons. Only one instance exists, as an additional evidence, you can add items to the cart using firstShoppingCart or secondShoppingCart objects, the total will be the same.

Before that, ensure cart_esmodules.js is exporting its functions as addItem and cartTotal, then modify use_esshoppingcart.js as follows:

// js/use_esshoppingcart.js
// Comment the two alert statements then add
// the following

// add some items
firstShoppingCart.addItem("USB drive", 20);
firstShoppingCart.addItem("Keyboard", 10);

// Use the "second" shopping cart
secondShoppingCart.addItem("JoyStick", 30);

// Check the total
alert(firstShoppingCart.cartTotal()); // 60
alert(secondShoppingCart.cartTotal()); // 60

The current state of use_esshoppingcart.js is used in the next section, therefore, leave it as it is, then proceed.

Imports are live

When we say, “imports are live,” this means when you modify an exported value from a module, any other file requesting this module will have access to this modified value.

Create a new module called testLiveImports.js and save the following code:

// js/testLiveImports.js
import { cartTotal } from "./cart_esmodules.js";

alert(cartTotal());

Switch to index.html, then add a script tag points to testLiveImports.js. Now, you’ll have two script tags as shown in the code block below.

<!-- index.html -->
<!-- Preious code remains the same. -->
<script type="module" src="js/use_esshoppingcart.js"></script>
<script type="module" src="js/testLiveImports.js"></script>

When you refresh your browser, you’ll realize cartTotal() in testLiveImports.js has the same value as the cartTotal() in use_esshoppingcart.js. Therefore, you should get 3 alert messages with the same output: 60.

How to use modules in Node.js

As you’ve learned in previous sections, Node.js uses the CommonJS module format, but it also supports ES modules.

Meanwhile, all the modules we’ve created so far we’ve executed in a Web browser, now let’s see how they play out in Node.js.

Now, before testing, please note: prior to Node 13, ES modules were an experimental technology, therefore, you could use modules by saving it with .mjs extension and accessing it via a flag.

On the other hand, from Node 13 upwards you can use modules in two ways. They are:

  • Saving the module with .mjs extension or
  • Creating a package.json file with type="module" in the nearest folder.

To make things more clear: here is what we’ll explain.

  • How to test ES modules in Node.js <= 12
  • How to test modules saved with .mjs extension in Node.js >= 13
  • How to test modules saved with .js extension in Node.js >= 13
How to use ES modules in Node.js <= 12

The current version of Node.js at the time of writing is 15.10.0 and 14.16.0 LTS, but if you have a version of Node.js less than these versions, here is how you’ll test it.

We’ll use use_esshopingcart.js from the last section (as we left it), but before that, please do the following:

  • Make a copy of use_esshopingcart.js and save the file with .mjs extension.
  • Change the alert() functions to console.log() because alert() won’t work in Node.js.

When you are done with that, fire up your console and navigate to the js directory of the project folder we’ve used throughout this section then type the following and hit the enter key on your keyboard:

node --experimental-modules use_esshoppingcart.mjs

You should get an output similar to the image below.

Experimental Warning

How to use modules saved with .mjs extension in Node.js >= 13

If you have Node.js 13 upwards installed, and your module is saved with .mjs extension, type the following and hit enter on your keyboard:

node use_esshoppingcart.mjs

The output should be the same.

esshoppingcart Output

How to test modules saved with .js extension in Node.js >= 13

Node 13 upwards allows you to use modules saved with the .js extension, but you need to add {"type": "module"} to the nearest package.json file.

Switch to your editor, save the following JSON code in your js directory:

// js/package.json
{
    "type": "module",
}

Next, you can use use_esshoppingcart.js. Do not forget to change the alert() functions to console.log() because we only made this change with the .mjs version.

# Kindly note the .js extension.
node use_esshoppingcart.js

As expected, the output should be the same.

Same Output

Characteristics of modules

If you read this article to this point, you already know the characteristics of a module. Here are some:

  • Modules are singletons
  • Module specifiers serve as global IDs
  • No global variables are used.
  • Designed for asynchronous loading

Advantages of modules

  • Reusability: Same modules can be shared across any number of applications
  • The syntax is easy to read
  • Named exports makes the module Tree-Shakable: You can import only what you need from a module resulting in smaller bundle size and better performance.
  • With modules, you can split the code base of your application into smaller files of self-contained functionality.

There you have it. ES modules. For more on ES modules, please, refer to the Further reading section.

Up next: Node Package Manager.

Node Package Manager (NPM)

Node Package Manager, popularly called NPM, is a popular package manager used by developers around the world to share modules (also called packages), and full-blown applications like express.

NPM is coded in JavaScript and was developed by Isaac Z. Schlueter, who took inspiration from PHP Extension and Application Repository (PEAR) and Comprehensive Perl Archive Network (CPAN), used by PHP and Perl programmers respectively.

At this time, NPM is the world’s largest software registry that hosts thousands of software packages. It is also the default package manager for Node.JS. You can find many of those packages on npm website.

NPM comes pre-installed with Node.js and if you’ve Node.js installed, you can use it by typing npm on the command line or GitBash, and you’ll get an output similar to the image below.

NPM Command Result

The image shows you some command options available with npm. The command you’ll find yourself using most often is npm install. This command is used to install packages published on the NPM registry.

As always, be careful with the packages you download and watch out for any package with known vulnerabilities. Fortunately, NPM will notify you when you install such applications.

For more familiarity with NPM, you can watch Brad Traversy’s NPM crash course on YouTube.

Projects using ES Modules

Since ES modules became mainstream, some notable projects have adopted it in their code base, among which are:

  • ReactJS
  • Svelte

ReactJS

React is a JavaScript library used for building user interfaces (UI). It has gained popularity in recent years since its release by Facebook in 2013.

The following is an example of React code using ES modules:

import React from "react";
import logo from "./logo.svg";
import "./App.css";

function App() {
  return (
    <div className="App">
      // UI code here
    </div>
  );
}

export default App;

Svelte

Svelte is another tool in the JavaScript toolbox that allows you to create so-called “Cybernetically enhanced web apps”.

Here is an example of a Svelte code using ES Modules taken from Svelte’s website:

/* App.svelte generated by Svelte v3.32.0 */
import {
    SvelteComponent,
    detach,
    element,
    init,
    insert,
    noop,
    safe_not_equal
} from "svelte/internal";

function create_fragment(ctx) {
    // Code truncated
    // Check https://svelte.dev/tutorial/basics
}

class App extends SvelteComponent {
    constructor(options) {
        super();
        init(this, options, null, create_fragment, safe_not_equal, {});
    }
}

export default App;

Conclusion

This article explained the varieties of module systems available in JavaScript and how you can use them to create modules. In addition, we briefly discussed NPM which is a package manager that is used for sharing modules (packages) among software developers.

In the end, we gave examples of popular projects like React using ES modules in their code base.

Further reading

References

 

LogRocket: Debug JavaScript errors more easily by understanding the context

Debugging code is always a tedious task. But the more you understand your errors the easier it is to fix them.

LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to find out exactly what the user did that led to an error.

LogRocket Dashboard Free Trial Banner

LogRocket records console logs, page load times, stacktraces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!

Try it for free.
Habdul Hazeez I teach and write code with interests in web development, computer security, and artificial intelligence.

Leave a Reply