Modules in JS

It is hard to imagine an application made of a single file. It is just as hard to imagine developing such code and maintaining it later. Fortunately, JavaScript supports modules, but it was not always like that.

Modules in JS

From this article you will learn:

  • What are modules in software engineering?
  • How and why was the CommonJS standard created?
  • How do ES Modules work?
  • When can we use a variable in a module specifier?
  • What are the differences between CommonJS and ES Modules?

You live alone in the middle of nowhere and you have a dream, and at the same time a task: build a house, your own place to live. At first your plan is simple. You just do not want rain falling on your head. Looking at a starry sky may be sexy, but sleeping in the rain feels more like diving than resting. So you put up four posts, and from leaves found nearby you build a roof.

It turns out, however, that the project still needs to grow. You need to add walls, put in windows, harden the floor, build a fireplace. Your small house basically meets all your expectations now. But the situation changes. You meet your partner, and they agree to live with you. Suddenly it gets tight. Time to add more modules: a bedroom, a kitchen, a living room, a bathroom. What used to be your entire little house has now become its entrance hall.

The more time passes, the more modules we need. Rooms for children, a storage room, an office, a garage, a library...

The description above is not the story of my life. Although I would probably like to have a library in my own house. The example I gave is an analogy for the applications we build as programmers. At the beginning, our application most often consists of one file that has one or several simple tasks. Over time, however, we start separating features into independent parts. On one hand, we do it to control the chaos that starts to appear. On the other, to separate responsibilities and group them properly. Each new part that we extract is, from a functional point of view, an independent module. Most often, a module is limited to a file from which key functionality is exposed to the outside.

In software engineering, modules are extensions of our program that provide specific functionality. They can be reused many times in different parts of the program, without the need to adapt them depending on where they are used. They also help us organize the code and the application itself better. In addition, with good structure, our code becomes less prone to errors.

Basically, simple and logical. Let us look at it, however, from the perspective of the JavaScript language.

I Have a Dream

It is 2008. I imagine that Kevin Dangoor, like many other programmers at the time, was annoyed by the way additional dependencies were added to websites he was building. I also assume he could not understand how the main language used by browsers could have no organized system for dependency management. Let me remind you that at the end of the first decade of the 21st century, JS did not yet run independently on the server side, and the most popular JavaScript engines available on personal computers were web browsers. This does not mean that JS could not be used for server work at all. It is enough to look at the Helma project, for example. It was a project written in Java, existing long before 2008, that allowed JavaScript to be used on the server side. Still, it was more of an interesting framework than a global solution for JS. If we go even further back in time, in 1996 Netscape made it possible to use JavaScript in its server software. In 1996! Just one year after the language appeared on the market.

The already mentioned Kevin Dangoor was working as an engineer at Mozilla Corporation at the time. In January 2009, he initiated a project he called ServerJS. He described it this way:

What I’m describing here is not a technical problem. It’s a matter of people getting together and making a decision to step forward and start building up something bigger and cooler together.

Kevin Dangoor, 2009

He wanted to create a concept that could be used across different platforms and that would become a standard library for the entire JavaScript world. The vision was something special for those times. In his idea, he talked about standardizing the way modules are attached, creating standard interfaces for solving common problems, and building a place from which different packages could be easily downloaded. Something like today's NPM, which, by the way, did not exist yet. Instead, other solutions were available, such as JSAN, the JavaScript Archive Network.

This led to ServerJS being renamed to CommonJS, to emphasize the broader use of the project. It is worth mentioning that ECMA International never officially implemented CommonJS into the language standard, although some members of TC39 participated in its creation.

In the meantime, the NodeJS project was developing and used the emerging standard. And the dream became reality.

CommonJS Syntax

Each file is treated as a separate module. CommonJS places each module inside the require function and returns the module.exports object. This object exists inside the module. It should contain the code that guarantees the availability of functionality at a level that allows other parts of our application to use it.

js
const ModuleA = require('./moduleA.js');

module.exports = {}

It is worth emphasizing that a module can be any JavaScript code. We do not have to return specific data or functions through module.exports. The decision is ours. CommonJS modules are loaded synchronously, which means we have to preserve the order of modules that depend on each other.

Despite the success of CommonJS, it was never implemented in browsers. So let us try to understand why.

A Lot of Changes

Every year, starting from 2015, ECMA International releases a new version of the ECMAScript language standard. The year 2015 was a breakthrough. A lot of new things were introduced into the standard, and ES Modules were among them. ES Modules are a concept that adds the ability to create modules inside JS code. Before that date, there was no official solution that supported modules in the browser. I would be lying if I said there was nothing that solved this problem. Here we should pay tribute to the RequireJS library, which drew heavily from the AMD, or Asynchronous Module Definition, concept while trying to preserve the spirit of CommonJS.

ES Modules assume that we have two ways to export code from our modules: a default way using export default, or a named way using export {}. We can access modules with import, as in the example below:

js
// module.js
export const moduleA = {};
const moduleB = {};
export default moduleB;

// app.js
import { moduleA } from './module.js';
import moduleB from './module.js';

Default export works similarly to module.exports. It does not matter what name the variable or function exported as default has. From one file, you can export only one default. During import, we can choose any name under which we want to use our module. In practice, it can be written in two ways:

js
import defaultModule from './module.js';
import { default as defaultModule } from './module.js';

If, inside our HTML file, we include JS code that uses modules, we should add the type="module" attribute to the script tag responsible for loading it. We have to make this change to use this functionality. Otherwise, the application will not work. In a backend application, it is enough to change the file extension to mjs.

What Happens Under the Hood?

When we build an application in a modular way, we create a dependency graph. In such a graph, connections between individual dependencies are created based on import statements. Import allows the browser or server to determine what code should be loaded and in what order. Here we need a very important note: the browser cannot use the next module files in their original form. Before they can be used, they are mapped into structures called Module Records. Each such structure contains a full set of information about the module. Once Module Records are created, they can be used to create module instances. Each module instance consists of two parts: state and code.

Only modules prepared in this way will be used by the browser.

Preparing modules consists of three phases: loading, linking, and evaluation. Each of them can be performed separately. All dependencies of a module are loaded in parallel, without pauses between loading individual elements. A mechanism built this way will work asynchronously by default. It is worth adding that the phases themselves do not necessarily have to be asynchronous. They can be run synchronously.

What Is the Difference?

CommonJS does not divide module loading into phases, because by design it works with files that are available much faster than in the browser. In the browser, we have an additional limiting element: the Internet. In the case of CommonJS, Node can block the main thread and wait for a file to be read. This means that, in the end, we go through the entire tree and, in a specific order, load, create instances, and execute dependency code before returning the whole module.

Because of the different way code is read in module specifiers, in CommonJS we can use variables. For example:

js
require(`${path}/module`); // OK
import module from `${path}/module`; // Error

It is worth adding that ES Modules also have dynamic imports, which allow variables to be used in an import. In that case, however, import is treated as a function that we call with the proper path. A dynamic import creates a new dependency graph that is analyzed separately from the main graph.

js
import(`${path}/module`); // OK, dynamic import

The nature of CommonJS is synchronous, while the nature of ES Modules is asynchronous. The latest Node supports both approaches. In the browser, we can still use only ES Modules.

Summary

As you can see, modules were needed in the JS world. Without treating our applications as a set of independent groups of functionality, we would have hit a wall, and further development would have become almost impossible. People have talked about modules since the very beginning of the JS language, but only the last 10 years brought major changes and concrete solutions.

When we talk about modules, it is also worth mentioning tools such as module bundlers, for example webpack, which help solve the most classic problems with modules, especially in browsers. I think it is worth giving this topic more space, so I will stop here for now.

Approaching our code in a modular way gives us a lot of freedom, just like a house made of many rooms. It is hard to sleep, cook, rest, and use the bathroom in one room, just as it is hard to create one function that does everything our program is supposed to do.

Sources

Share this article:

Comments (1)

  • Another_On3

    16 stycznia 2023 o 06:03

    Brakuje kontentu :/

You may be interested in

If this article interested you, check out other materials related to it thematically. Below you will find articles and podcast episodes authored by me, as well as books I recommend that expand on this topic.

Editor by Maica
Article
2020-07-09

How to Build Your Own Text Editor in the Browser?

A programmer's work is about solving problems. Programming languages are our tools. By using them we can create new solutions, but also new tools. Text editors are definitely such tools. Today we will try to write our own.

Read more

Zapisz się do newslettera

Bądź na bieżąco z nowymi materiałami, ćwiczeniami i ciekawostkami ze świata IT. Dołącz do mnie.