JavaScript Modules - Introduction

Chapter 6 15 mins

Learning outcomes:

  1. What are modules and why use them
  2. Evolution of modules in JavaScript
  3. About this unit

What are modules?

When programming was in its very early stages, there wasn't any kind of a notion of modules. Programs were written in one large chunk, more or less.

Slowly, with time, as this turned into a less maintainable and tedious approach, came in the idea of modules and modular programming.

So what exactly are modules?

Well, simply put:

Modules are files that contain code with one single concern and with the ability to be reused.

Essentially, modules are the means of breaking down a large piece of code into multiple files and, thus, make development less of a hassle.

The practice of developing software around a system of modules is referred to as modular programming. In today's era, complex software is almost always built using modular programming principles.

There are many benefits of using modules instead of coding everything in a single file:

  • Better maintainability: with smaller modules, it's much easier to maintain code and only touch upon those modules that require patches and updates.
  • Reusability: with a module addressing a specific concern in a given project, it's possible to reuse it in other parts of the project, or even publish it as a third-party module for others to use.
  • Encapsulation: a module effectively hides its internal workings from the outside world which is more than just desirable for large projects where name collisions are highly possible. Thus, we say that modules provide encapsulation for the code that they contain.
  • Team work and collaboration: when a code base is split up into multiple modules, each with its own concern, it becomes possible for multiple teams and developers to work on the code base simultaneously, thus promoting easier and effective team work and better collaboration.

Without any doubt, modules are one of the coolest features of programming languages and one that all programmers should leverage or, at the very least, know how to leverage in their projects.

In languages that support modules, the typical behavior is to have:

  • A means of importing stuff into a module, often known as defining the module's dependencies.
  • A means of exporting stuff from a module, often known as the module's public interface.

Many of the mainstream languages today, such as Java, Python, PHP, Ruby, Go, support modules out of the box.

Now that we know what modules are, let's take a step back and dive deeper into how JavaScript got a standard module system 20 years after its inception.

Evolution of modules in JavaScript

When JavaScript was first released in 1995 in Netscape Navigator 2.0, there was absolutely no concept of modules in the language and, perhaps, even no plans of including one later on.

And rightly so — JavaScript was supposed to be a really simplistic scripting language that could be understood and used by even non-technical people, such as designers, writers, researches, etc. Supporting modules was not really required at all.

JavaScript wasn't imagined to eventually grow into a complex language, handling tons and tons of concerns of the frontend. When this did happen and application code grew by an exponential factor, it became crystal clear that no longer could programmers just rely on the old <script> tag approach for modularization.

A better approach was needed.

Different people came up with differing approaches to implementing modules in JavaScript, obviously in non-standardized ways. Some events are particularly worthwhile discussing here.

CommonJS in Node.js

Somewhere around 2009, Node.js was created to allow JavaScript to be run in the server-side environment.

Since the Node.js runtime was completely different than the browser environment, for example, with utilities to work with the underlying filesystem directly, a dedicated modular system was needed in Node.js. A working group was initiated by Mozilla engineer, Kevin Dangoor, to discuss about a specification for implementing modules in Node.js.

Initially called ServerJS, the group drafted a specification called CommonJS Modules/1.0. It's commonly referred to just as the CommonJS specification.

The proposition was very basic: a require() function would be provided in order to import a given module into another module, while a module object would be provided, with an exports property, to hold the exports of the module.

Later, the group's name was changed to CommonJS to reflect wider applicability to other JavaScript environments as well (and not just the server).

The CommonJS specification quickly became superbly popular by virtue of its simplicity and adoption in Node.js. As more and more people embraced Node.js, so did they embrace CommonJS.

Unfortunately, one issue with CommonJS was that require() was synchronous. That is, until and unless the referred module wasn't loaded, parsed and executed, the function won't exit, effectively blocking the main thread from other activity.

On the server side, this was OK because importing a module was just about loading a particular file from the local filesystem, which is a pretty quick operation.

But on the client, this would've wreaked havoc on the the browser. As the module's loading would require a network roundtrip, which could take a considerable amount of time, the browser's main thread would remain blocked, thus preventing any other activity from happening there, leaving users with a poor experience.

Henceforth, CommonJS never directly made it to the client. (But indirectly, it did through bundling tools, as we shall learn later on below.)

The birth of AMD

For the client, everyone loved the term 'asynchronous' and thus born a separate module specification called Asynchronous Module Definition, or simply AMD.

The library that first implemented AMD was Require.js.

The idea was once again pretty simple: every module would be encapsulated inside a global function (provided by Require.js) called define(), defining dependencies of the module (via the first argument) and entertaining its exports as well (via a return statement inside the function).

The purpose of encapsulating everything inside a function was so that Require.js could itself run the module once all of its dependencies were loaded.

AMD was a pretty popular approach of its time for client-side developers. After all, it brought forward a fairly simple way of implementing modules on the client. Projects increasingly started to use it until, eventually, another module innovation hit the road.

Browserify with a new approach

Browserify came up with a completely different plan to tackle modules on the client. Instead of putting the module system in action at run time (as was the case in Require.js), it proposed to have a module system at build time and only get a single script in the end to be sent to, and consequently executed by, the browser.

The motivation was to keep from requesting multiple files in the browser, as was the case with AMD, because that came with additional HTTP overhead.

This single script — known as the 'bundled' script, or simply the 'bundle' — would be built by calling the browserify tool in the terminal, which would combine the code in all of the modules and dump it in a given file.

In this regard, Browserify decided to support CommonJS-style modules.

Now frontend code could be written using CommonJS modules, albeit being transformed into a normal JavaScript file, without any modules, before being delivered to the browser.

How amazing would this have been!

Obviously, using Browserify required an extra build step before any changes to code could be witnessed in the browser, but with watchers (to automatically rebuild the bundle as soon as any of its constituent files were changed), the process wasn't very intimidating.

The biggest advantage of this approach was that developers could use third-party modules that were being built for JavaScript by the Node.js community, in the CommonJS style.

Browserify surely brought more hope into the frontend development realm by introducing a win-win approach to modular programming — developers got to work with modules, thus improving development, while the browser only got one file delivered, thus saving from the HTTP overhead.

But soon, another tool was to give it a tough competition — or better to say, a really tough competition.

Webpack emerges

Webpack emerged in 2012, taking the idea of Browserify to the next level.

It supported not just JavaScript modules but also the inclusion of HTML and CSS resources in scripts. And not only this but it also supported parsing other JavaScript module specifications in addition to CommonJS, in particular, AMD and ECMAScript modules (which were about to be standardized in JavaScript).

React, a UI library developed by Facebook (now Meta), came at this very time and adopted Webpack for its building process. That's when things took a turn for Webpack — it rose in popularity and surpassed Browserify in no time.

If you want to learn React the right way, we have an in-depth course on React.

To boil it all down, essentially, everyone was desperately in need of a module system in JavaScript, but without a standardized way, everyone came up with their own unique answers.

However, this was only until 2015, when finally Ecma International TC39 released the first standard specification for ECMAScript 6, including a long-awaited feature: modules.

In this unit

In this unit, we'll explore the standard module system in JavaScript, put forward by ECMAScript 6, often referred to as ECMAScript modules, or sometimes even as ESM.

The term 'ECMAScript' here clearly helps us distinguish that this system is the standardized way of implementing modules in JavaScript, laid out as part of ECMAScript.

Frankly speaking, the term 'JavaScript modules' wouldn't have been able to bring about such clarity — there are many kinds of module systems in JavaScript and this term doesn't help us understand which one are we specifically talking about.

Anyways, coming back to the discussion, in the following chapter of this unit, we'll dive deeper into how ES modules work; in particular, work with the import and export keywords.

We'll take a look at such things as dynamic imports using the import() function, default imports/exports, module scope, implicit strict mode, and circular dependencies.

There is a lot to cover, and being able to effectively work with ES modules is paramount for newbie developers, so let's get learning.