DEV Community

Bryan C Guner
Bryan C Guner

Posted on

JS Modules

Javascript Modules:

A module is a reusable piece of code that encapsulates implementation details and exposes a public API so it can be easily loaded and used by other code.

Why are they useful?

Technically we can write code without modules.

In JavaScript, modules should ideally allow us to:

  • abstract code: to delegate functionality to specialised libraries so that we don't have to understand the complexity of their actual implementation
  • encapsulate code: to hide code inside the module if we don't want the code to be changed
  • reuse code: to avoid writing the same code over and over again
  • manage dependencies: to easily change dependencies without rewriting our code

Module patterns in ES5

EcmaScript 5 and earlier editions were not designed with modules in mind. Over time, developers came up with different patterns to simulate modular design in JavaScript.

To give you an idea of what some of these patterns look like, let's quickly look at 2 easy ones: Immediately Invoked Function Expressions and Revealing Module.

Immediately Invoked Function Expression (IIFE)

(function(){
  // ...
})()
Enter fullscreen mode Exit fullscreen mode

An Immediately Invoked Function Expression (IIFE) is an anonymous function that is invoked when it is declared.

Notice how the function is surrounded by parentheses. In JavaScript, a line starting with the word function is considered as a function declaration:

// Function declaration
function(){
  console.log('test');
}
Enter fullscreen mode Exit fullscreen mode

Immediately invoking a function declaration throws an error:

// Immediately Invoked Function Declaration
function(){
  console.log('test');
}()

// => Uncaught SyntaxError: Unexpected token )
Enter fullscreen mode Exit fullscreen mode

Putting parentheses around the function makes it a function expression:

// Function expression
(function(){
  console.log('test');
})

// => returns function(){ console.log('test') }
Enter fullscreen mode Exit fullscreen mode

The function expression returns the function, so we can immediately call it:

// Immediately Invoked Function Expression
(function(){
  console.log('test');
})()

// => writes 'test' to the console and returns undefined
Enter fullscreen mode Exit fullscreen mode

Immediately Invoked Function Expressions allow us to:

  • encapsulate code complexity inside IIFE so we don't have to understand what the IIFE code does
  • define variables inside the IIFE so they don't pollute the global scope (var statements inside the IIFE remain within the IIFE's closure)

but they don't provide a mechanism for dependency management.

Revealing Module pattern

The Revealing Module pattern is similar to an IIFE, but we assign the return value to a variable:

// Expose module as global variable
var singleton = function(){

  // Inner logic
  function sayHello(){
    console.log('Hello');
  }

  // Expose API
  return {
    sayHello: sayHello
  }
}()
Enter fullscreen mode Exit fullscreen mode

Notice that we don't need the surrounding parentheses here because the word function is not at the beginning of the line.

We can now access the module's API through the variable:

// Access module functionality
singleton.sayHello();
// => Hello
Enter fullscreen mode Exit fullscreen mode

Instead of a singleton, a module can also expose a constructor function:

// Expose module as global variable
var Module = function(){

  // Inner logic
  function sayHello(){
    console.log('Hello');
  }

  // Expose API
  return {
    sayHello: sayHello
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice how we don't execute the function at declaration time.

Instead, we instantiate a module using the Module constructor function:

var module = new Module();
Enter fullscreen mode Exit fullscreen mode

to access its public API:

module.sayHello();
// => Hello
Enter fullscreen mode Exit fullscreen mode

This Module pattern offers similar benefits as an IIFE, but again does not offer a mechanism for dependency management.

As JavaScript evolved, many more different syntaxes were invented for defining modules, each with their own benefits and downsides.

We call them module formats.

Module formats

A module format is the syntax we can use to define a module.

Before EcmaScript 6 or ES2015, JavaScript did not have an official syntax to define modules. Therefore, smart developers came up with various formats to define modules in JavaScript.

Some of the most widely adapted and well known formats are:

  • CommonJS
  • ES6 module format

CommonJS format

The CommonJS format is used in Node.js and uses require and module.exports to define dependencies and modules:

var dep1 = require('./dep1');
var dep2 = require('./dep2');

module.exports = function(){
  // ...
}
Enter fullscreen mode Exit fullscreen mode

ES6 module format

As of ES6, JavaScript also supports a native module format.

It uses an export token to export a module's public API:

// lib.js

// Export the function
export function sayHello(){
  console.log('Hello');
}

// Do not export the function
function somePrivateFunction(){
  // ...
}
Enter fullscreen mode Exit fullscreen mode

and an import token to import parts that a module exports:

import { sayHello } from './lib';

sayHello();
// => Hello
Enter fullscreen mode Exit fullscreen mode

We can even give imports an alias using as:

import { sayHello as say } from './lib';

say();
// => Hello
Enter fullscreen mode Exit fullscreen mode

or load an entire module at once:

import * as lib from './lib';

lib.sayHello();
// => Hello
Enter fullscreen mode Exit fullscreen mode

The format also supports default exports:

// lib.js

// Export default function
export default function sayHello(){
  console.log('Hello');
}

// Export non-default function
export function sayGoodbye(){
  console.log('Goodbye');
}
Enter fullscreen mode Exit fullscreen mode

which you can import like this:

import sayHello, { sayGoodbye } from './lib';

sayHello();
// => Hello

sayGoodbye();
// => Goodbye
Enter fullscreen mode Exit fullscreen mode

You can export not only functions, but anything you like:

// lib.js

// Export default function
export default function sayHello(){
  console.log('Hello');
}

// Export non-default function
export function sayGoodbye(){
  console.log('Goodbye');
}

// Export simple value
export const apiUrl = '...';

// Export object
export const settings = {
  debug: true
}
Enter fullscreen mode Exit fullscreen mode

Unfortunately, the native module format is not yet supported by all browsers.

We can already use the ES6 module format today, but we need a transpiler like Babel to transpile our code to an ES5 module format such as AMD or CommonJS before we can actually run our code in the browser.

Module loaders

A module loader interprets and loads a module written in a certain module format.

A module loader runs at runtime:

  • you load the module loader in the browser
  • you tell the module loader which main app file to load
  • the module loader downloads and interprets the main app file
  • the module loader downloads files as needed

If you open the network tab in your browser's developer console, you will see that many files are loaded on demand by the module loader.

A few examples of popular module loaders are:

  • RequireJS: loader for modules in AMD format
  • SystemJS: loader for modules in AMD, CommonJS, UMD or System.register format

Module bundlers

A module bundler replaces a module loader.

But, in contrast to a module loader, a module bundler runs at build time:

  • you run the module bundler to generate a bundle file at build time (e.g. bundle.js)
  • you load the bundle in the browser

If you open the network tab in your browser's developer console, you will see that only 1 file is loaded. No module loader is needed in the browser. All code is included in the bundle.

Examples of popular module bundlers are:

  • Browserify: bundler for CommonJS modules
  • Webpack: bundler for AMD, CommonJS, ES6 modules

Summary

To better understand tooling in modern JavaScript development environments, it is important to understand the differences between modules, module formats, module loaders and module bundlers.

module is a reusable piece of code that encapsulates implementation details and exposes a public API so it can be easily loaded and used by other code.

module format is the syntax we use to define a module. Different module formats such AMDCommonJSUMD and System.register have emerged in the past and a native module format is now available since ES6.

module loader interprets and loads a module written in a certain module format at runtime. Popular examples are RequireJS and SystemJS.

module bundler replaces a module loader and generates a bundle of all code at build time. Popular examples are Browserify and Webpack.

Top comments (0)