28 Aug 2022 ~ 9 min read

JavaScript Module Types

Let's talk about the various types of modules in the JavaScript Eco system

Listen to something with me 😍

Want to listen somewhere else? 🎧

Table of Contents

Introduction JavaScript Modules

If you’ve been programming in JavaScript for a while, you’ll most likely come across the concept of modules. JavaScript like any modern language has a way to split your code into chunks of modules. But because JavaScript is such an old language and it went through a lot of changes, the module system has been revised and developed by different organizations. MDN has a great article about the module system in JavaScript it’s a great read.

So in this article we’ll cover the module system of JavaScript.

Top

Concepts of the current common Module Systems

So, let’s begin with something you’re likely familiar with NodeJS. NodeJS by default uses a module system known as CommonJS. CommonJS is the require/exports module pattern. There are some things we can clear before heading further, relate these ideas with the NodeJS module system.

  1. Module: A module is collection of code inside a single file.
  2. Export: When exporting we’re making a piece of code, which can be a function, variable or class, visible to the caller of the module.
  3. Default Exports: A default export is the default entity you’ll be exposed to when importing(using) a module. If you’ve not further specified a part of the module you’ll be using this default module export, by default.
  4. Named Export: A part of the module, that the caller of the imported module has to specify in order to get to.
  5. Modules should explicitly expose a piece of code
  6. Modules have a local scope
  7. Modules are singletons: If you’ve imported a module, it’s executed once but further imports will use the first executed code as a module and will not execute the modules code again.

Top

Why are there different module systems

Using Globals in the Browser

If you’ve started out working on JavaScript using something like Bootstrap or jQuery then you might have used to load jQuery or Bootstrap's JavaScript code before having to load you’re Apps code in the html of the page. This is because before we started to use ESM modules we were using globals, and scripts were a way to have globals ready before having to use them. To illustrate this point I’ll use the example provided in the JavaScript for Impatinet Programmers article ESModules.

Initially, browsers only had scripts – pieces of code that were executed in global scope. As an example, consider an HTML file that loads script files via the following HTML:

<script src="other-module1.js"></script>
<script src="other-module2.js"></script>
<script src="my-module.js"></script>

The main file is my-module.js, where we simulate a module:

// IIFE - Immediately Invoked Function Expression
var myModule = (function () { // Open IIFE
  // Imports (via global variables)
  var importedFunc1 = otherModule1.importedFunc1;
  var importedFunc2 = otherModule2.importedFunc2;

  // Body
  function internalFunc() {
    // ···
  }
  function exportedFunc() {
    importedFunc1();
    importedFunc2();
    internalFunc();
  }

  // Exports (assigned to global variable `myModule`)
  return {
    exportedFunc: exportedFunc,
  };
})(); // Close IIFE 

This example is demonstrating how you might work with a dependency for your application if you’re using globals and script tags. Here we have two dependencies in our application, otherModule1 and otherModule2. Before we use them we’re anticipating the dependencies to be available globally, and in the HTML we should ensure the browser loads them before it get’s to our applications code.

For something to exist globally it must be available on the window object of your page as a property. If window.apple is defined, the apple is available globally. Mind you, this is true for browsers, and NodeJS has it’s own way of defining objects globally.

Although the loading dependencies using globals worked, it had one major flaw. It was not tree shakeable, among other things. Tree shaking means removing any unused code in your final bundle. To tackle the issues of using globals and handling dependencies properly there have been a myriad module systems. Briefly, these are the module systems in use today

  1. CommonJS: We’ve looked at this earlier, this is the default way NodeJS handles .js modules. This was mainly designed to be used by Desktop and Server programs.

  2. ESM: ESModules, also known as ES6 Modules, are my favorite module system, they’re simple yet they are not fully supported by browsers and devices. They’re characterized with export/import manner of using modules like so,

    import { doSomething } from './module';
    
    const isDone = doSomething();

    ESM is designed to be used both on the browser and server code.

  3. AMD: AMD, Asynchronous Module Definition, loads modules asynchronously mainly designed for browser and client environments where it’s expensive to wait and block the main thread until scripts get loaded.

  4. UMD: UMD, Universal Module Definition, is an attempt to have a module compatible with both CommonJS and AMD modules. We’ll not be covering this module system since it is offered as a middle ground for the two main module types, CommonJS and AMD.

If you’re confused by the different module systems, or you’re lost on whatever this is, I’ll summarize them like this to make it a bit clearer. ESM is the recent attempt to standardize the module system in the JavaScript ecosystem. Prior to ESM, the module systems that emerged are CommonJS and AMD which because of their differences in how they handled dependencies, CJS being synchronous and AMD being async, made them used in either the browser or the server but not both thus ESM.

Top

CommonJS

CommonJS is the NodeJS system on using modules, it is synchronous. It is mainly supposed to be used by servers and not intended for Browsers to use. CJS also requires modules to be compiled before being used. For that reason, browsers don’t use CJS. To illustrate here is a simple example

// path/to/module.js
const sayName = "MyModule";
// Similar to module.exports.sayName
exports.sayName = sayName;

const moduleAction = () => console.log("Performing default action");
// Default Export
module.exports = moduleAction;

Using the module.

// index.js
// you're module's path, just using some fake path here.
const myModule = require("./path/to/module.js");

myModule(); // output: Performing default action
console.log({
    moduleName: myModule.sayName
}) // output: { moduleName: myModule.name }

In this very simple example, we’re exporting a named export called sayName and we’re another default export. This is essentially how most module systems are used, they are very similar but they do have their differences.

Top

AMD, Asynchronous Module Definition

AMD is I think where someone new might get confused, AMD is the module system used in browsers because it worked asynchronously. Thus, not holding the main thread when opening a webpage/app. Unlike CommonJS, AMD modules can be executed directly. The syntax of an AMD module is like so

define(id?, dependencies?, factory);

Where id is the module identifier, dependencies are an array of dependencies and the factory is function used to define parts of the module or export an object literal so it can represent the module. Example

define(["alpha"], function (alpha) {
   return {
     verb: function() {
       return alpha.verb() + 2;
     }
   };
});

Here the module is requiring the dependency alpha. After the module alpha is loaded the module will define itself. Although the module id is not defined, the id will default to the id of module provided when the loader was fetching the module’s code.

Top

ES6 Modules

These are the future of the JavaScript ecosystem, although they are not supported by all the JavaScript runtimes, they are a good alternative to the confusing module system developers have had to face. Since it is asynchronous it retains all the benefits of AMD modules, yet they are available on both the browser and server. Here is a sample demonstrating the module system.

// External Module
export const sumNumbers =
    (...numbers) =>
        numbers.reduce((acc, next) => acc + next, 0);

export const times4 = (arrOfNumbers) => arrOfNumbers.map((num) => num * 4);

export default logAll =
    (obj) =>
        Object
            .enteries()
            .forEach(
                [key, val] =>
                    console.log(`'${Key}' has value of`, { value: val }
            );

And the module caller,

import moduleDefault, { times4, sumNumbers as anotherName } from './module';

moduleDefault({ version: 1.2.3 }); // output: 'version' has value of, { value: 1.2.3 }
const result = times4([2, 4, 5]) // output: [ 8, 16, 20 ]
const anotherNamedResult = anotherName(1, 2, 3) // output: 6

In ESM, when exporting a module it’s either a named or default module. A named export can happen inline, right before defining the function/variable you want to export. If you want to export a default export, you’ll have to explicitly define a default export. When importing a module, it is compact and looks a lot more simplified. You’ll list the named exports inside the braces and the default export is accessible directly after an import or you can access it through the default keyword. You can import defaults in one of these ways

import * as namespacedImport from './module';
import Module from './module';
import { default as moduleDefault } from './module';

Top

Conclusion

Understanding modules provides you with a better insight into how the JavaScript application or library you write works in the different environments. This will provide you with information on how you can optimize your bundle to be used inside a browser or take advantage of the build system you’re using to generate the type of module you need for your program to run in the environments it should run in. If you’re thinking how does this affect performance, you should check out this article on The cost of transpiling es2015 to 2016 if I must say it’s super interesting.

This article is more of an informative topic but I’d love to take your comment on how I approached explaining the concepts. Thank you very much for reading the article.

Top

References

  1. JavaScript Modules, MDN
  2. Understanding JavaScript Modules: Bundling & Transpiling, SitePoint
  3. CommonJS, NodeJS Docs
  4. The cost of transpiling es2015 to 2016, samccone’s GitHub
  5. Before we had modules, we had scripts, exploringjs.com

Top


Headshot of Maxi Ferreira

Hi, I'm Zablon. I'm a software engineer, mostly working in the web space, based in Ethiopia. You can follow me on Twitter, see some of my work on GitHub, or read more about Qebero.dev on here.