Skip to main content

Understanding ES Modules in JavaScript

Modules are found in every programming language. It is a way to include functionality in one code with another. These modules are where a developer can create code with specific functionality that can be reused elsewhere. Modules provide you with benefits such as code reusability and code that can be split into several chunks.

If you had been working with JavaScript before sometimes back, you would have come to know that JavaScript did not have modules as such. Developers resorted to using HTML <script> tags in order to load JavaScript files into their applications. Later on, several module definition formats started to surface.

  • CommonJS — the module.exports and require syntax used in Node.js
  • Asynchronous Module Definition (AMD)
  • Universal Module Definition (UMD)
  • ES Modules

Let us see why modules are needed in the first place.

Why Do We Need Modules

When you think about how programs work, all they do is manage variables. They assign values to variables, modify them, combine two variables together, etc. But when the number of variables increases with the size of your application, it can be cumbersome to manage them and maintain your code.

The solution to this was to have only a few variables that you should be worried about. JavaScript's way of achieving this was called scope. Because of how scopes work in JavaScript, functions can’t access variables that are defined in other functions.

Although this allows your variables to be inaccessible by other functions, it gave rise to another issue — it was hard for you to share variables between different functions. A common way of overcoming this and sharing variables out of scope was to put them on a scope above such as the global scope.

Although this approach worked, it came with problems. Your script tags should be in the correct order and you must make sure that no one changes that order. If the order does change, your app will throw an error. This made code management tricky. You never know what might break what. Any function can grab anything on the global, so you do not know which functions depend on which scripts.

Another issue was that every part of the code that’s inside of that global scope can change the variable. This would allow malicious and non-malicious code to access and even modify your global variables with or without malicious intent.

And then modules were introduced to help overcome these issues.

How Do Modules Make Things Better

Modules allow you to organize and manage variables and functions better. Usually, functions and variables that are apart of the same functionality are put together in a module. This puts these variables into the module scope. The module scope can be used to share variables between the functions in the module.

Illustration by Lin Clark

This also allows for variables to be available for other modules as well. They can explicitly say which variable, class or function should be available to outsider modules. This is called export. Once you have an export, other modules can explicitly say that they depend on that variable, class or function. Due to this explicit relationship, you will know which modules will break if you remove one.

Once you are able to import and export variables and functions, it is easier for you to split and break up your code into chunks of code that can work independently. You can later build your application by using these modules, similar to building with Lego blocks.

In order to achieve this super useful feature, there have been multiple attempts to add module functionality with JavaScript.

Existing Module Systems

CommonJS

CommonJS is what has been used in NodeJS historically. With Node, you get CommonJS module.exports and require out of the box. However, unlike Node, the browser doesn’t support CommonJS. Moreover, CommonJS loads modules synchronously and therefore it is not an optimal solution for browsers. You can use bundlers such as Webpack or Browserify to overcome this problem.

//    filename: bar.js

// dependencies
var $ = require('jquery');

// methods
function myFunction(){};

// exposed public method (single)
module.exports = myFunction;

Asynchronous Module Definition (AMD)

AMD was born out of a group of developers who did not like the direction of CommonJS. In fact, AMD was split from CommonJS early in its development. The main difference between AMD and CommonJS is that AMD loads modules asynchronously. This was very much popular in browsers as startup times are essential to good user experience.

//    filename: bar.jsdefine(['jquery'], function ($) {
// methods
function myFunction(){};

// exposed public methods
return myFunction;
});

Universal Module Definition (UMD)

As CommonJS and AMD were quite popular in their respective domains, there was a need for a “universal” pattern that supports both styles. But as it turns out, UMD was messy and ugly. Although it did support both AMD and CommonJS, as well as supporting the old-style “global” variable definition.

What are ES Modules

As you can clearly see, JavaScript lacked one proper standard module definition format. Hence a single, native module standard was therefore proposed in ES6.

The static nature of the import and exportdirective allows static analyzers to build a full tree of dependencies without running code.

The result is syntactically pleasing and compatible with both synchronous and asynchronous modes of operation in the browser. ES modules have quickly become available in the browser, but in Node.js it was a bit harder to come up with a solution that is backward-compatible and enables incremental upgrades. In Node.js native ES modules are available behind the experimental-modules flag for a long time.

The following is an example for ES6 modules.

JavaScript

//------ library.js ------
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diagonal(x, y) {
return sqrt(square(x) + square(y));
}
//------ main.js ------
import { square, diagonal } from 'library';
console.log(square(13)); // 169
console.log(diagonal(12, 5)); // 13
const app = document.getElementById("app");
app.innerHTML = "<h1>Demo App for ES Modules</h1>";
const input = document.getElementById("num");
input.addEventListener("change",displaySquare);
function displaySquare(){
var sqrOutput = document.getElementById("sqr");
sqrOutput.value = square(input.value);
}

HTML

<HTML>
<head>
<title>ES Modules Demo</title>
</head>
<body>
<script type="module" src="./main.js" ></script>
<div id="app"></div>
<label>Input</label>
<input id="num" type="number" placeholder="Enter number here"/>
<br>
<label>Output</label>
<input id="sqr" type="number" disabled style="margin- top:20px"/>
</body>
</HTML>

As you can see above in the HTML file, you need to specify type="module" in the script tag for the browser to treat it as an ECMAScript module.

Backwards Compatibility
For backwards compatibility, you can include nomodule in the script tag (where the JS file loaded is single bundled file). Browsers with ES Modules support will know to ignore that. This solution will work even in the oldest of browsers. Willem’s answer has explained this very well.

In the above scenario, we will include something like this in our HTML.

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

Note:

If you are testing this locally, you will need to run this on a server as you will run into CORS issues. Read more here
Modules are imported with either absolute or Relative references and must start with either “/”, “./”, or “../”.

Dynamic Imports

The latest ES2020 version does come with dynamic imports. To dynamically import modules, the import keyword may be called as a function. When used this way, it returns a promise.

import('/modules/library.js')
.then((module) => {
// Do something with the module.
});
//or using await
let module = await import('/modules/library.js');

ES Modules Browser Compatibility

The use of native modules in the browser depends on the import and exportstatements, the browser compatibility of which is as follows.

Screenshot of MDN Docs
Screenshot of MDN Docs

Should You Choose ES Modules?

For browsers, ES modules are the new standard. With asynchronous module loading out of the box, you can expect faster startup times for better performance. Although you can use CommonJS with some additional plugins in the browser, it is highly advised that you switch to ES modules as they are native in browsers.

ES native modules allow you to get individual modules loaded rather than a single bundle file. This is quite useful and it reduces the size of the data loaded. Browser compatibility for native modules is also important as it decides whether native ES modules will be implemented or whether we will fallback to our module bundler which will load a single bundle file.

One of the issues, when you get a single bundle file, is that when your applications become bigger, the size of the bundle js file will also increase and thereby affecting startup time and performance. You can avoid this by using code splitting which is a feature available in modern bundlers such as webpack.

But there might be instances where we would opt for a module bundler such as webpack over ES modules. If you have assets such as CSS, images, fonts and even data files such as XML, CSV, you might want to opt for a webpack solution as webpack provides asset bundling.

You also should take into account the support for HTTP2 in browsers. When you use native modules, your browser will load those modules individually. But rather than sending several HTTP requests, with the help of HTTP2, we can serve multiple requests simultaneously with a single connection. 96.49% of browsers use HTTP2 according to CanIUse.

But when you are developing an application that should cater even the remaining 3.51%, then you might want to switch to webpack. This is because your application would need to send several HTTP requests to load each individual modules if you stick with native ES modules.

In Node, things are quite different. As the feature is still flagged as experimental, it is better that you stick with CommonJS. You can still give ES modules a try though.


Comments

Popular posts from this blog

How to use Ngx-Charts in Angular ?

Charts helps us to visualize large amount of data in an easy to understand and interactive way. This helps businesses to grow more by taking important decisions from the data. For example, e-commerce can have charts or reports for product sales, with various categories like product type, year, etc. In angular, we have various charting libraries to create charts.  Ngx-charts  is one of them. Check out the list of  best angular chart libraries .  In this article, we will see data visualization with ngx-charts and how to use ngx-charts in angular application ? We will see, How to install ngx-charts in angular ? Create a vertical bar chart Create a pie chart, advanced pie chart and pie chart grid Introduction ngx-charts  is an open-source and declarative charting framework for angular2+. It is maintained by  Swimlane . It is using Angular to render and animate the SVG elements with all of its binding and speed goodness and uses d3 for the excellent math functio...

Understand Angular’s forRoot and forChild

  forRoot   /   forChild   is a pattern for singleton services that most of us know from routing. Routing is actually the main use case for it and as it is not commonly used outside of it, I wouldn’t be surprised if most Angular developers haven’t given it a second thought. However, as the official Angular documentation puts it: “Understanding how  forRoot()  works to make sure a service is a singleton will inform your development at a deeper level.” So let’s go. Providers & Injectors Angular comes with a dependency injection (DI) mechanism. When a component depends on a service, you don’t manually create an instance of the service. You  inject  the service and the dependency injection system takes care of providing an instance. import { Component, OnInit } from '@angular/core'; import { TestService } from 'src/app/services/test.service'; @Component({ selector: 'app-test', templateUrl: './test.component.html', styleUrls: ['./test.compon...

How to solve Puppeteer TimeoutError: Navigation timeout of 30000 ms exceeded

During the automation of multiple tasks on my job and personal projects, i decided to move on  Puppeteer  instead of the old school PhantomJS. One of the most usual problems with pages that contain a lot of content, because of the ads, images etc. is the load time, an exception is thrown (specifically the TimeoutError) after a page takes more than 30000ms (30 seconds) to load totally. To solve this problem, you will have 2 options, either to increase this timeout in the configuration or remove it at all. Personally, i prefer to remove the limit as i know that the pages that i work with will end up loading someday. In this article, i'll explain you briefly 2 ways to bypass this limitation. A. Globally on the tab The option that i prefer, as i browse multiple pages in the same tab, is to remove the timeout limit on the tab that i use to browse. For example, to remove the limit you should add: await page . setDefaultNavigationTimeout ( 0 ) ;  COPY SNIPPET The setDefaultNav...