On compatibility of JavaScript modules

Feb 17, 2020 • ☕️ 4 min read

JavaScript programs started off pretty small — most of its usage in the early days was to do isolated scripting tasks, providing a bit of interactivity to your web pages where needed, so large scripts were generally not needed.

JavaScript has had modules for a long time. However, they were implemented via libraries, not built into the language. ES6 is the first time that JavaScript has built-in modules.

Motivation

JavaScript started out as a scripting language on web browsers, the official specification only defined APIs for some objects that were useful for building browser-based applications.

Over the years, there have been multiple attempts both official and unofficial to bring JavaScript to other environments like web servers, command-line tools, desktop applications, and hybrid applications.

One feature that this post will cover is module format, which is a must in any general-purpose programming languages to create modular and reusable codes.

A module is a value that can be accessed by a single reference. If you have multiple pieces of data or functions that you want to expose in a module, they have to be properties on a single object that represents the module

CJS (CommonJS)

CommonJS (originally called ServerJS) is an ambitious project started by Kevin Dangoor back in 2009 in an attempt to bring JavaScript to the outside of web browsers. The ultimate goal is to define a JavaScript Standard Library like other general-purpose programming languages and compatible in multiple host environments.

CJS APIs also define a module format in which you can export an object in some modules and require synchronously in other modules.

CJS modules basically contain two primary parts: a free variable named exports, which contains the objects a module wishes to make available to other modules, and a require function that modules can use to import the exports of other modules

// utils.js
const log = message => {
  console.log(message)
}
module.exports = {log}
// index.js
const utils = require('./utils')
utils.log('Hello World')

The CJS modules work fine in a local environment, but they did not fully embrace some things in the browser environment that cannot change but still affect module design: network loading and inherent asynchronicity.

AMD (Asynchronous Module Definition)

The initial attempt was a CommonJS transport format, then changed over time to become a module definition API.

AMD is a module format that allows module and its dependencies can be asynchronously loaded. This is particularly well suited for the browser environment where synchronous loading of modules incurs performance, usability, debugging, and cross-domain access problems.

The specification uses define function to define named or unnamed modules based on the following signature:

define(
  module_id /*optional*/,
  [dependencies] /*optional*/,
  definition function /*function for instantiating the module or object*/
);

Example of defining a named module

//Calling define with a dependency array and a factory function
define('awesome-module', ['dep1', 'dep2'], function(dep1, dep2) {
  //Define the module value by returning a value.
  return function() {}
})

UMD (Universal Module Definition)

Since CommonJS and AMD styles have both been equally popular, it seems there’s yet no consensus. This has brought about the push for a universal pattern that supports both styles, which brings us to none other than the Universal Module Definition.

The pattern is ugly, but is both AMD and CommonJS compatible, as well as supporting the old-style global variable definition.

// Uses AMD or browser globals to create a module.
;(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define(['b'], factory)
  } else {
    // Browser globals
    root.amdWeb = factory(root.b)
  }
})(typeof self !== 'undefined' ? self : this, function(b) {
  // Use b in some fashion.

  // Just return a value to define the module export.
  // This example returns an object, but the module
  // can return a function as the exported value.
  return {}
})

ESM (ECMAScript Modules)

ES Modules is the ECMAScript standard for working with modules, this standardization process completed with ES6 and browsers started implementing this standard trying to keep everything well aligned.

The goal for ES modules was to create a format that both users of CommonJS and of AMD are happy with: Similarly to CommonJS, they have a compact syntax, a preference for single exports and support for cyclic dependencies. Similarly to AMD, they have direct support for asynchronous loading and configurable module loading.

Within a module, you can use the export keyword to export just about anything, you can export a const, a function, or any other variable binding or declaration.

// hello.js
export const apiKey = 'random-key-her'
export const doSomething = () => {
  console.log('hello there')
}
export default doSomething

You can then use the import keyword to import the module from another module.

// main.js
import doSomething, {apiKey} from './hello'

console.log(apiKey)
doSomething()

The newest part of the JavaScript modules functionality to be available in browsers is dynamic module loading. This allows you to dynamically load modules only when they are needed, rather than having to load everything up front. This has some obvious performance advantages.

import('./awesome-module.js').then(module => {
  // Do something with the module.
})

ES modules allow static analysis that helps with optimizations like tree-shaking and scope-hoisting, and provide advanced features like circular references and live bindings.

Conclusions

Even though JavaScript never had built-in modules, the community has converged on a simple style of modules, which is supported by libraries in ES5 and earlier. Then ES6 came out as a major update with native module support.

  • CJS has various implementations, including Node.js, it was not particularly designed with browsers in mind, so it doesn’t fit in the browser environment very well.
  • AMD is more suited for the browser because it supports asynchronous loading of module dependencies.
  • UMD is admitted ugly but is both AMD and CommonJS compatible.
  • ESM is the ECMAScript standard for working with modules since ES2015

Modern browsers have started to support module functionality natively, they can optimize loading of modules, making it more efficient than having to use a library and do all of that extra client-side processing and extra round trips.

Reading More

Extending standard CSS by preprocessors

CSS preprocessors empower CSS with variables, nesting, inheritance, mixins, functions, and mathematical operations

Become a Fullstack JavaScript Developer, Part 2: The Basics

Whether you are going to focus on frontend or backend, these are my curated list of knowledge or technologies you must master

On Incompleteness of Prerendering

Prerendering is a rendering strategy when you pre-render your client-side rendered applications to feed crawlers better

AssemblyScript - TypeScript to Wasm Compiler

A tool that makes WebAssembly more accessible to JavaScript developers by compiling a strict subset of Typescript to Wasm using Binaryen