Understanding The Module Pattern in JavaScript

Understanding The Module Pattern in JavaScript

Exploring the different kind of modules in JS and their usage

What is a Module?

A module is a collection of related data and functions, with a distinct division between hidden private details and the public API which is used to interact with the module. Modules also clearly mention which other modules they require to function, also known as dependencies.

Basically, modules achieve three things:

  1. Colocate data and the functions that act on it.
  2. Restrict direct access to the internals (variables and functions) of a module.
  3. Clearly mention dependencies.

We'll see in a bit how we can achieve these three objectives.

The natural benefit of making modules is better code organisation, developers can work on parallel on different modules once the interface has been decided and it also becomes easier to maintain code.

How to Organise Code into Modules?

There are various ways of organising our code into modules, like CommonJS modules and the ES Modules, but before going to them, let's see how we can step by step achieve the 3 objectives listed above on our own, without using any library or framework. After going through this simple exercise we will gain some invaluable insights on how modules work in JS.

Step1: Colocating Data and Functions

In this step we just have to make sure that we keep related things together. Consider the following code snippet, here we have some data (in a real scenario this data will probably be fetched from a database) and a function that goes through that data or performs some actions on it. Who ever uses this module should not be bothered about what happens internally, they will only use the function getName() to interact with the module.

var cars = {
  records: [
    {id: 1, name: 'civic', manufacturer: 'honda', price: '1500000'},
    {id: 2, name: 'swift', manufacturer: 'maruti', price: '1300000'},
    {id: 3, name: 'golf', manufacturer: 'volkswagen', price: '900000'}
  ],
  getName(id) {
    let car = this.records.find(
      car => car.id == id
    );
    return car ? car.name : null;
  }
}

console.log(cars.getName(2));
//swift

Here we can see that we have co-located our data and functions together, but the internals are still not private, anyone can access the records simply by doing cars.records.

Step 2: Hide Private Details of The Module

Consider the following code:

var cars = (function() {
let records = [
{id: 1, name: 'civic', manufacturer: 'honda', price: '1500000'},
{id: 2, name: 'camry', manufacturer: 'toyota', price: '1300000'},
{id: 3, name: 'golf', manufacturer: 'volkswagen', price: '900000'}
];

function getName(id) {
let car = records.find(
car => car.id == id
);
return car ? car.name : null;
}

const publicApi = {
getName
};

return publicApi;
})();

cars.getName(1);
// civic

Here, by using an Immediately Invoked Function Expression (IFFE), we have successfully hidden the local bindings of our modules inside the scope of the function expression, only the public API has been retuned and can be used to interface with the module.

This style of modules provides isolation but doesn’t specify the dependencies, instead it just puts the interface on the global scope and expects its dependencies, if any, to do the same.

Step 3: Specifying and Importing Dependencies

To be able to require dependencies we need to have the ability to execute string as code. There are different ways to do this, we'll see how to do this using the function constructor.

Function constructor takes two arguments, a string containing a comma-separated list of argument names and a string containing the function body and returns a function object.

let add = Function("a, b", "return a + b");
console.log(add(1, 2));

These are all the ingredients that are required to make a module system, now let's see how some common module systems work.

CommonJS Modules

Prior to 2015 JS didn't have any language level support for modules, so other ways were developed to organise JS code in modules, one of the most widely used ways was CommonJS modules, and you will still come across it at a lot of places, Node.js also uses this module system.

A typical file in Node would look something like this.

const module1 = require('./path/to/module');

let variable1 = ... ;

foo() {
  ...
}

bar() {
 ...
}

module.exports.bar;

Notice how we are specifying our dependencies using require and defining our public API using module.exports. Let's try to understand these two important pieces.

Require according to node website:

Node.js follows the CommonJS module system, and the builtin require function is the easiest way to include modules that exist in separate files. The basic functionality of require is that it reads a JavaScript file, executes the file, and then proceeds to return the exports object. An example module:

The code for require() in its most minimal form would look something like this.

require.cache = Object.create(null);

function require(name) {
  if (!(name in require.cache)) {
    let code = readFile(name);
    let module = {exports: {}};
    require.cache[name] = module;
    let wrapper = Function("require, exports, module", code);
    wrapper(require, module.exports, module);
  }
  return require.cache[name].exports;
}

Notice how require keeps a cache of modules. The required module is evaluated only once, all subsequent calls to that module are returned from the cache, this makes sure that all the modules are singletons, i.e only one instance of a module will exists, this ensures that the state of the module is same across the application.

Before processing, Node effectively wraps such code in a function, so that the variable and function declarations are contained in that wrapping function's scope, not treated as global variables. Node would process our code something like this (illustrative, not actual).

function Module(module,require,__dirname,...) {
    const module1 = require('./path/to/module');

    let variable1 = ... ;

    foo() {
      ...
    }

    bar() {
    ...
    }

    module.exports.bar;
}

It's a common misconception that node defines globals like module and require, but as you can see they are actually passed as arguments to the function.

ES Modules

The JS standard from 2015 introduced its own module system, with language level support, the main concepts of dependencies and interface remain the same but the details differ, instead of calling require you can now use import keyword and to export identifiers you can simply use the export keyword.

Our cars module would look something like this.

let records =  [
    {id: 1, name: 'civic', manufacturer: 'honda', price: '1500000'},
    {id: 2, name: 'swift', manufacturer: 'maruti', price: '1300000'},
    {id: 3, name: 'golf', manufacturer: 'volkswagen', price: '900000'}
  ];

  export function getName(id) {
    let car = records.find(
      car => car.id == id
    );
    return car ? car.name : null;
  }

How to tell the browser to load your file as a module

To tell the browser that a script needs to be treated as a module, we can add the type=module attribute to the script tag.

<script type="module" src="moduleExample.js">

.mjs File Extension

Sometimes you might come across .mjs file extension while working on projects. This extension is used to explicitly specify that the file is an ECMA Script module. You can read more about it here v8.dev/features/modules#mjs.

Build Tools

For the majority of use cases modules would not be used in their raw form. Code is usually processed using tools like webpack which bundles all the modules together in a single file or a few different files depending on how we configure the bundler. A bundler achieves this by analyzing the import and export statements to form a dependency graph and then placing the code in single file, in doing so it also replaces the import/export statements by other functions.

We made a lot of efforts trying to make our applications modular, then why bundle it again before shipping to the browser? The reason is simple, loading a modular application with hundreds of files would cause a lot of problems, Fetching a single big file is considerable faster than fetching hundreds of small files. Apart from this, build tools also minifies our code and applies other optimizations.

Conclusion

In this article we tried to understand the features of a module, then we incrementally implemented our own module system and on the way tried to understand the nuances of modules. In the end we looked at CommonJS Modules and ES Modules. The main aim behind writing this article is not just to make the reader familiar with modules but to also get insights into how they work under the hood.

References

v8.dev/features/modules github.com/getify/You-Dont-Know-JS/blob/2nd.. github.com/getify/You-Dont-Know-JS/blob/2nd.. eloquentjavascript.net/10_modules.html