Code Modularity
The concept of modules as a way to organize code has been around for a long time. With the project and its codebase getting bigger, the developers try to break it up into files, each of which describes a separate feature.
Modular code helps with the organization, maintenance, testing and, most importantly, management of dependencies. The most important benefits of modules are maintainability, namespace and reuse.
Maintainability: a well-designed module makes dependency on other code parts as low as possible. This will help you expand the functionality of your application without fear of disrupting its work. Updating a single module is much easier if the module is self-contained.
Namespace: variables that are not in the function scope are global. This usually causes namespace pollution, when global variables are shared between unrelated code. Modules avoid namespace pollution by creating a separate scope for variables.
Reuse: all developers used to copy the finished code into new projects, changing it according to the specifics of the project. This is obviously a huge waste of time. It is much better to have a module that can be reused over and over again without having to know anything about the environment in which it is used.
Module bundling
Module bundling is the process of concatenating a group of modules and their dependencies into a file or a group of files.
Usually code is divided into folders and files; in addition, you need to add
external libraries. As a result, each of these files must be included in the
main HTML file in the <script>
tag, which is then loaded by the browser.
Separate <script>
tags for each file mean that the browser will load each file
separately, which negatively affects page loading speed. To work around this
problem, files are concatenated into one or a pair of files to reduce the number
of requests. But the problem of managing dependencies between modules remains.
If you are using module systems, such as CommonJS
or ESM
, you need a tool to
transform them into well-ordered, browser-accessible code. This is where
Webpack
and other bundlers come into play.
ECMAScript Modules (ESM)
Until recently, the language did not have a built-in modular system. ESM have a compact declarative syntax and the ability of asynchronous loading. An ES module is a reusable piece of JS code that exports certain objects, making them available to other modules.
const helloMessage = "hello!";
const goodbyeMessage = "goodbye!";
export const hello = () => helloMessage;
export const goodbye = () => goodbyeMessage;
import { hello, goodbye } from "./greeter";
console.log(hello()); // "hello!"
console.log(goodbye()); // "goodbye!"
Each JS file stores code in a unique module context, imports any dependencies it
needs and exports whatever needs to be imported by other modules. Export/import
operations are implemented by import
and export
. There are two obvious
advantages to this approach: avoiding pollution of the global namespace and
explicitly specifying dependencies.
The new module system differs from CommonJS and others, primarily in that it is a standard. This means that, over time, it will be natively supported by browsers, without additional tools. However, browser support is not full right now, which is why ESM are used together with module bundling tools such as Webpack, Parcel and others.
ESM were designed with static analysis in mind. This means that, when importing modules, the imports are processed during compilation, that is, before the script is run. This enables you to remove exports that are not used by other modules before running the script, which will make JS files much smaller, reducing the load on the browser. This is called tree shaking and is done by bundlers automatically when bundling JS code.
Named export
A module can export several entities, which differ in their names and are called named exports. To import them into another module, you need to know the names of the exported entities that you want to import.
The first way is to use the keyword export
before all the entities that need
to be exported. They will be added as properties to the exported object. When
importing, you destructure the properties from the imported object.
const sqrt = Math.sqrt;
export const square = x => x * x;
export const diag = (x, y) => sqrt(square(x) + square(y));
import { square, diag } from "./path/to/my-module";
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5
The second way is to explicitly specify the object with properties for export.
const sqrt = Math.sqrt;
const square = x => x * x;
const diag = (x, y) => sqrt(square(x) + square(y));
export { square, diag };
import { square, diag } from "./path/to/myModule";
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5
The following syntax imports all of the module's exports as an object with the specified name. This is called namespace import.
import * as myModule from "./path/to/my-module";
console.log(myModule.square(11)); // 121
console.log(myModule.diag(4, 3)); // 5
Default export
A module often exports only one entity; such export is convenient for import. Default export is the most important exported value, which can be anything: variable, function, class, etc.
export default function myFunc() {
// ...
}
export default class MyClass {
// ...
}
import myFunc from "./path/to/my-func";
import MyClass from "./path/to/my-class";
myFunc();
const inst = new MyClass();
Use named export when you need to export multiple entities, and default export when you export a single entity. While default and named exports can be used in the same file, it is good practice to choose only one style for each module.