The story behind ES6 Native Module Standard
By: Akshay Kumar
Tags: Web Development, Programming, Nodejs, Javas, ES6

In today’s world, new-age developers often find it hard to relate to old javascript memes that point out how difficult it was to understand and code in javascript, Atleast I feel this way. The modern-day tools, extensions, transpilers, editors, etc have made it pretty easy to code both server and client-side javascript. But all this change was not achieved in a day, it took several years to reach this point, and until we understand the problems that old age developers faced, we cannot fully appreciate the privileges we have today, this is what serves as a motivation to write this article. So let’s get started

Before Modules (The Dark Age)

Back in the day, before the discovery of fire, when dinosaurs ruled the planet, javascript developers had to go through a hell lot, to make their code work on the browser. They have to manually import a bunch of js files through HTML <script> tag like this

<!DOCTYPE html>

<script src="fileOne.js"></script><html>

<script src="fileTwo.js"></script>

<body>

</body>

</html>

Now there were a lot of problems associated with this approach

There was a different HTTP request, to fetch each js file, which impacted site performance

The files were loaded sequentially, so if there was a function or object declared in fileTwo.js and referred in fileOne.js then the browser will throw an error, for eg:

// fileOne.js

console.log(sum(2, 4)); // fileTwo.js

function sum(x, y) {

return x + y;

}

Browser throws the error

fileTwo.js:1 Uncaught ReferenceError: sum is not defined

at fileTwo.js:1

(anonymous) @ fileTwo.js:1

and if we just reverse the order in which <script> tags are declared we get the pretty output on the console

6

Functions from one file can overwrite the functions of the other, if not imported or managed correctly.

A file is not loaded until its previous files are loaded.

// fileOne.js

let len = 1000000000;

while (true) len--; // fileTwo.js

console.log("File Two");

We can change the value of len and see the delay in the printing of File Two log.

After getting to know all this, I suddenly started relating to those js memes, Modern day developers do not have to deal with all these issues, because of what we are going to discuss next, modules .

Incomes modules (The discovery of fire)

Modules were a game-changer towards the adoption of javascript, now the developer did not have to worry about all the problems mentioned in the previous section, and could focus more on the core logic of their applications. But what is a module exactly?

A module is just a bit of code encapsulated in a file and exported to another file. Modules remain loosely coupled, as one module/file usually contains logic to one type of functionality and that logic can be abstracted from other modules importing it. This gave much freedom to the developers, now they could structurize their code in a better, reuse common logic thus improving the efficiency of their application.

But there was no single module standard that was adopted to achieve this like on the server side there was CommonJs

CommonJs

CommonJS wraps each module in a function called ‘require’, and includes an object called module.exports , which exports code for availability to be required by other modules. All you have to do is add whatever you want access to other files onto the exports object and require the module in the dependent file.

CommonJs is used in Nodejs as a module formatting system, which loads modules synchronously, so the modules that are dependent on other modules are loaded at last in a top-down fashion. For example:

// index.js

console.log("index loaded started")

require('./child.js')()

console.log("index loading finished") // child.js

module.exports = function() {

console.log("Child loaded")

}

when run with the command node index.js gives the following output

index loaded started

Child loaded

index loading finished

But the problem with CommonJs was that due to its synchronous nature, it could not be used as a client-side module formatting system, as it will halt the functioning of the entire page until all js files are successfully loaded, and hence would reduce its efficiency. Comes requireJs to the rescue.

RequireJs

RequireJs was one of the implementations of (Asynchronous Module Definition), in which the modules were not loaded synchronously but rather Asynchronously, so the client will be able to see a button, for example, will be able to interact with it as well, but the corresponding functionality will be attached only when the corresponding js module is successfully loaded. Unlike synchronous module formatting systems, where the site will seem to be in a hanged state until all js is loaded. For Example:

define(['module1', ',module2'], function(module1, module2) {

console.log(module1.setName());

});

The function is called only when the requested modules are finished loading.

But the presence of different module formatting systems lead to a distinction in the way client and server-side javascript was written, which resulted in the distinctions in skill sets required for writing server and client-side javascript. Slowly and steadily people realized the need for an Isomorphic module management system.

Voila, introducing ES6 Modules

The ES6 modules standard brought Isomorphic javascript into the picture. With ES6 we can import and export modules with both synchronous and asynchronous modes of operations. Thus they can be used on both client and server-side, thus bridging the gap between skillset requirements for frontend and backend developers.

The require statement is used to bring modules into the namespace. It is not dynamic, cannot be used anywhere in the file. This is in contrast with the require and define . The export statement makes the elements public. This static behavior makes the static analyzers build the tree of dependencies while bundling the file without running code. Due to which ES6 modules are pre-parsed in order to resolve further imports before code is executed, while CommonJS modules load dependencies on-demand while executing the code. Let us understand this with a few examples:

console.log("Index Started")

const importedVar = require('./child.js')

console.log("Index Ended")

// index.js console.log("Child Loaded")

module.exports = "fsdfsfsdf"

// child.js

The above code uses commonJs modules and when executed gives the following output.

Index Started

Child Loaded

Index Ended

while on the other hand, if the same logic is written using ES6 modules

console.log("Index Started")

import importedVar from './child.mjs';

console.log("Index Ended")

// index.js console.log("Child Loaded")

export default "fsdfsfsdf"

// child.js

the output is

Child Loaded

Index Started

Index Ended

The above examples exhibit that while using CommonJS modules, the file child.js was loaded on the runtime only when it was required, while in the case of ES6 modules, the file was pre-parsed, before the execution of index.js

Due to this, we cannot use both types of module formats in a single file. A JS file will either use ES6 modules or CommonJS/RequireJS.

Using ES6 Modules on the server-side

NodeJS has added experimental support for ES6 modules, as it cannot make a hard switch as ES6 modules might need more time to mature, and the developers and their codebases will need time to do this migration. Also, ES6 modules won’t be able to execute on very older versions of Node.

To run ES6 modules in Node version ≥ 13 you either need to

Save the file with .mjs extension, or

extension, or Add { "type": "module" } in the nearest package.json .

The same project can have both .js and .mjs files and they can even import each other, just the catch is that one file should contain only one format, based on its extension.

console.log("Index Started");

import importedVar from "./child.js";

console.log("Index Ended"); // index.mjs console.log("Child Loaded")

module.exports = "fsdfsfsdf" // child.js

gives the Output

Child Loaded

Index Started

Index Ended

Incase we mention { type: "module" } in package.json, we can still use CommonJs, for that the files with CommonJS modules must have the extension .cjs For Example:

console.log("Index Started");

import importedVar from "./child.js";

console.log("Index Ended"); // index.js console.log("Child Loaded")

module.exports = "fsdfsfsdf" // child.cjs

With { type: "module" } in package.json, gives the same output as above.

In the case of Node version 8–12 to run ES6 modules, use the extension .mjs and run like

node --experimental-modules index.mjs

Using ES6 Modules on the client-side

Most of the modern-day browsers like Safari, Chrome, Firefox and Edge now support ES6 modules, but some of their very old versions might not. To use ES6 modules in a browser that supports ES6 modules you can add type="module" in the <script> tag.

<script type="module" src="./main.js"></script>

or add logic inline

<script type="module">

import { something } from './somewhere.js';

// ...

</script>

But there are easy solutions available to make your js code work on all browser versions. This can be achieved using transpilers like Babel, Browserify, etc. which we might look into in subsequent stories.

Conclusion

Modern-day javascript that we use day in day out is the by-product of the years of hard work put in by thousands of developers around the globe. ES6 Modules represents a star in the galaxy of changes that javascript has gone through. Reading and understanding these changes gives us the perspective of how far we have come and how the sky is the limit for what the future holds.

References