The evolution of JavaScript module patterns has been a huge headache for web developers. Attempting to work with plugins and libraries that all use different module patterns overly difficult, but once you understand each pattern and how to work with it you can begin to translate and make sense of each. This article should provide a good source of example JavaScript module patterns, both how to create a module and how to include it as a dependency.
Each module example below will do the exact same thing: a console.log
will send the message, “Hi my name is Kevin.”, to the browser console. I’ve ordered these in terms of their evolution, starting with the oldest patterns.
It all started here, this is the oldest form of a module in JavaScript.
An Immediately Invoked Function Expression (IIFE) is an anonymous function that is invoked when it is declared. It isn’t really a module pattern, it’s an enclosure pattern that helps encapsulate code and keep it isolated from other parts of an application.
// Module
(function(){
console.info( 'Hi my name is Kevin.' );
})()
Include the IIFE “module” in your HTML document as a <script>
. This is the older pattern out there but is still used today.
<script src="module.js"></script>
This pattern is used in browsers and makes use of a define
function to define modules, and a require
function to include a module as a dependency. The require
function is different than the Node.js pattern that most developers are familiar with, and this is the source of a lot of confusion. require
in the context of AMD dates back to an old library called require.js
that would load a separate JS file in asynchronously for browser use. This eventually led to the design of the CommonJS pattern, which is the next stage of evolution.
define( 'module', function () {
return {
log: function () {
console.info( 'Hi my name is Kevin.' );
}
};
});
require(['module'], function( module ) {
module.log();
});
This pattern evolved from the browser-based AMD pattern for use in Node.js on the server-side. It uses module.exports
to define modules, and require('...')
to include them as a dependency.
// Module
module.exports = {
log: function() {
return console.log( 'Hi my name is Kevin.' );
}
}
const module = require('./module');
module.log();
The UMD pattern emerged next as a way to support both Node.js and browsers. To do this it will conditionally check how a module was included as a dependency, and then choose from 3 approaches to include it as a module: AMD, Common.js, and browser globals. This became popular amongst open-source plugin creators and was intended to provide a way for them to support many use cases with their libraries.
(function( root, factory ) {
// AMD
if (typeof define === 'function' && define.amd) {
define( ['module'], factory );
}
// CommonJS/Node.js
else if ( typeof module === 'object' && module.exports ) {
module.exports = factory(require('module'));
}
// Browser globals
else {
// root = window
root.returnExports = factory(root.b);
}
}(this, function( module ) {
return {
log: function() {
return console.log( 'Hi my name is Kevin.' );
}
};
}));
There are 3 ways to work with a UMD module:
// AMD
require(['module'], function( module ) {
module.log();
});
// CommonJS
const module = require('./module');
module.log();
// Browser globals
const module = window.module;
module.log();
The next stage in evolution, and the most modern and best case module pattern to use today, as of 2020, is known as just System, System.register
or simply ES6 modules. This makes use of the syntax import ... from './module'
. It was designed to support the ES6 module syntax in ES5 JavaScript environments. The syntax is basically the same as the ES6 module syntax, so I’ve included these examples together here for simplicity. This pattern is typically used as the gold standard in an application built with TypeScript.
// Module
export default {
log: function() {
return console.log( 'Hi my name is Kevin.' );
}
}
import { log } from "./module";
log();