Good day, friends!I present to you the translation of the article “Understanding (all) JavaScript module formats and tools” by Dixin.When creating an application, there is often a desire to divide the code into parts, logical or functional blocks (modules). However, JavaScript initially had no module support. This has led to the emergence of various modular technologies. This article discusses all the basic concepts, templates, libraries, syntax and tools for working with modules in JavaScript.IIFE module: JS module template
By defining a variable in JS, we define it as a global variable. This means that such a variable will be available in all JS files loaded on the current page:
let count = 0
const increase = () => ++count
const reset = () => {
count = 0
console.log(' .')
}
increase()
reset()
In order to avoid pollution of the global namespace, you can use an anonymous function: (() => {
let count = 0
})
Voila, there are no more global variables. However, the code inside the function is not executed.IIFE: immediate function expression
In order to execute code inside a function f
, it must be called using ()
how f()
. To execute code inside an anonymous function (() => {})
should also be used ()
. It looks like this (() => {})()
: (() => {
let count = 0
})()
This is called IIFE (immediately called function expression). A module can be defined as follows:
const iifeCounterModule = (() => {
let count = 0
return {
increase: () => ++count,
reset: () => {
count = 0
console.log(' .')
}
}
})()
iifeCounterModule.increase()
iifeCounterModule.reset()
We wrap the module code in IIFE. An anonymous function returns an object. This replaces the export interface. There is only one global variable - the name of the module (or its namespace). Subsequently, the name of the module can be used to call it (export). This is called the JS module template.Impurities of import
When defining a module, some dependencies may be required. When using a modular template, each dependent module is a global variable. Dependent modules can be defined inside an anonymous function or passed to it as arguments:
const iifeCounterModule = ((dependencyModule1, dependencyModule2) => {
let count = 0
return {
increase: () => ++count,
reset: () => {
count = 0
console.log(' .')
}
}
})(dependencyModule1, dependencyModule2)
Earlier versions of popular libraries such as jQuery used this template (the latest version of jQuery uses the UMD module).Open module: open JS module template
The open module template was coined by Christian Heilmann. This template is also IIFE, but the emphasis is on defining all interfaces as local variables inside an anonymous function:
const revealingCounterModule = (() => {
let count = 0
const increase = () => ++count
const reset = () => {
count = 0
console.log(' .')
}
return {
increase,
reset
}
})()
revealingCounterModule.increase()
revealingCounterModule.reset()
This syntax makes it easier to understand what each interface is responsible for (or what it does).CJS module: CommonJS module or Node.js module
CommonJS, originally named ServerJS, is a template for defining and using modules. It is built into Node.js. By default, each JS file is a CJS. Variables module
also exports
provide export of the module (file). The function require
provides loading and using the module. The following code demonstrates the definition of a counter module in CommonJS syntax:
const dependencyModule1 = require('./dependencyModule1')
const dependencyModule2 = require('./dependencyModule2')
let count = 0
const increase = () => ++count
const reset = () => {
count = 0
console.log(' .')
}
exports.increase = increase
exports.reset = reset
module.exports = {
increase,
reset
}
Here's how this module is used:
const {
increase,
reset
} = require('./commonJSCounterModule')
increase()
reset()
const commonJSCounterModule = require('./commonJSCounterModule')
commonJSCounterModule.increase()
commonJSCounterModule.reset()
In the Node.js runtime (engine), this template is used by wrapping the code inside the file into a function, to which variables exports, module
and a function are passed as parameters require
:
(function(exports, require, module, __filename, __dirname) {
const dependencyModule1 = require('./dependencyModule1')
const dependencyModule2 = require('./dependencyModule2')
let count = 0
const increase = () => ++count
const reset = () => {
count = 0
console.log(' .')
}
module.exports = {
increase,
reset
}
return module.exports
}).call(thisValue, exports, require, module, filename, dirname)
(function(exports, require, module, __filename, __dirname) {
const commonJSCounterModule = require('./commonJSCounterModule')
commonJSCounterModule.increase()
commonJSCounterModule.reset()
}).call(thisValue, exports, require, module, filename, dirname)
AMD module or RequireJS module
AMD ( asynchronous module definition ) is a template for defining and using modules. It is used in the RequireJS library . AMD contains a define
module definition function that accepts the module name, dependency names, and factory function:
define('amdCounterModule', ['dependencyModule1', 'dependencyModule2'], (dependencyModule1, dependencyModule2) => {
let count = 0
const increase = () => ++count
const reset = () => {
count = 0
console.log(' .')
}
return {
increase,
reset
}
})
It also contains a function require
for using the module:
require(['amdCounterModule'], amdCounterModule => {
amdCounterModule.increase()
amdCounterModule.reset()
})
require
AMD differs from require
CommonJS in that it takes the names of the modules and the modules themselves as arguments to the function.Dynamic loading
The function define
also has a different purpose. It takes a callback function and passes a CommonJS-like require
function to this function. Inside the callback function, require is called to dynamically load the module:
define(require => {
const dynamicDependencyModule1 = require('dependencyModule1')
const dynamicDependencyModule2 = require('dependencyModule2')
let count = 0
const increase = () => ++count
const reset = () => {
count = 0
console.log(' .')
}
return {
increase,
reset
}
})
AMD module from CommonJS module
The above function define
, in addition require
, can take the variables exports
and as arguments module
. Therefore, define
code from CommonJS can be executed inside :
define((require, exports, module) => {
const dependencyModule1 = require('dependencyModule1')
const dependencyModule2 = require('dependencyModule2')
let count = 0
const increase = () => ++count
const reset = () => {
count = 0
console.log(' .')
}
exports.increase = increase
exports.reset = reset
})
define(require => {
const counterModule = require('amdCounterModule')
counterModule.increase()
counterModule.reset()
})
UMD module: universal module definition or UmdJS module
UMD ( universal module definition ) - a set of templates for ensuring the operation of the module in different runtimesUMD for AMD (RequireJS) and browser
The following code provides the module in both AMD (RequireJS) and the browser:
((root, factory) => {
if (typeof define === 'function' && define.amd) {
define('umdCounterModule', ['dependencyModule1', 'dependencyModule2'], factory)
} else {
root.umdCounterModule = factory(root.dependencyModule1, root.dependencyModule2)
}
})(typeof self !== undefined ? self : this, (dependencyModule1, dependencyModule2) => {
let count = 0
const increase = () => ++count
const reset = () => {
count = 0
console.log(' ')
}
return {
increase,
reset
}
})
It looks complicated, but it's just IIFE. An anonymous function determines if there is a function define
from AMD / RequireJS.- If
define
detected, the factory function is called through it. - If
define
not found, the factory function is called directly. At this point, the argument root
is the browser Window object. It receives dependent modules from global variables (properties of the Window object). When a factory
module returns, it also becomes a global variable (a property of the Window object).
UMD for AMD (RequireJS) and CommonJS (Node.js)
The following code provides the module in both AMD (RequireJS) and CommonJS (Node.js): (define => define((require, exports, module) => {
const dependencyModule1 = require("dependencyModule1")
const dependencyModule2 = require("dependencyModule2")
let count = 0
const increase = () => ++count
const reset = () => {
count = 0
console.log("Count is reset.")
}
module.export = {
increase,
reset
}
}))(
typeof module === 'object' && module.exports && typeof define !== 'function'
?
factory => module.exports = factory(require, exports, module)
:
define)
Don’t be scared, it’s just IIFE again. When an anonymous function is called, its argument is “evaluated”. Assessing the argument to determine the execution environment (defined by the presence of variables module
and exports
of CommonJS / Node.js, and the functions define
of the AMD / RequireJS).- If the runtime is CommonJS / Node.js, the anonymous function argument manually creates the function
define
. - If the runtime is AMD / RequireJS, the argument to the anonymous function is a function
define
from that environment. Performing an anonymous function ensures that the function works define
. Inside an anonymous function, a function is called to create the module define
.
ES module: ECMAScript2015 or ES6 module
In 2015, version 6 of the JS specification introduced a new modular syntax. This is called ECMAScript 2015 (ES2015) or ECMAScript 6 (ES6). The basis of the new syntax is the keywords import
and export
. The following code demonstrates the use of the ES module for named and default (default) import / export:
import dependencyModule1 from './dependencyModule1.mjs'
import dependencyModule2 from './dependencyModule2.mjs'
let count = 0
export const increase = () => ++count
export const reset = () => {
count = 0
console.log(' .')
}
export default {
increase,
reset
}
To use the module file in the browser, add the tag <script>
and identify it as a module: <script type="module" src="esCounterModule.js"></script>
. To use this module in Node.js, change its extension to .mjs
:
import {
increase,
reset
} from './esCounterModule.mjs'
increase()
reset()
import esCounterModule from './esCounterModule.mjs'
esCounterModule.increase()
esCounterModule.reset()
For backward compatibility in the browser, you can add a tag <script>
with the attribute nomodule
: <script nomodule>
alert ('Not supported.')
</script>
ES dynamic module: ECMAScript2020 or ES11 dynamic module
The latest 11 version of the JS 2020 specification introduces a built-in function import
for the dynamic use of ES modules. This function returns a promise, so you can use the module with then
:
import('./esCounterModule.js').then(({
increase,
reset
}) => {
increase()
reset()
})
import('./esCounterModule.js').then(dynamicESCounterModule => {
dynamicESCounterModule.increase()
dynamicESCounterModule.reset()
})
Due to the fact that the function import
returns a promise, it can use the keyword await
:
(async () => {
const {
increase,
reset
} = await import('./esCounterModule.js')
increase()
reset()
const dynamicESCounterModule = await import('./esCounterModule.js')
dynamicESCounterModule.increase()
dynamicESCounterModule.reset()
})
System Module: SystemJS Module
SystemJS is a library for supporting ES modules in older browsers. For example, the following module is written using ES6 syntax:
import dependencyModule1 from "./dependencyModule1.js"
import dependencyModule2 from "./dependencyModule2.js"
dependencyModule1.api1()
dependencyModule2.api2()
let count = 0
export const increase = function() {
return ++count
}
export const reset = function() {
count = 0
console.log("Count is reset.")
}
export default {
increase,
reset
}
This code will not work in browsers that do not support ES6 syntax. One solution to this problem is to translate the code using the System.register
SystemJS library interface:
System.register(['./dependencyModule1', './dependencyModule2'], function(exports_1, context_1) {
'use strict'
var dependencyModule1_js_1, dependencyModule2_js_1, count, increase, reset
var __moduleName = context_1 && context_1.id
return {
setters: [
function(dependencyModule1_js_1_1) {
dependencyModule1_js_1 = dependencyModule1_js_1_1
},
function(dependencyModule2_js_1_1) {
dependencyModule2_js_1 = dependencyModule2_js_1_1
}
],
execute: function() {
dependencyModule1_js_1.default.api1()
dependencyModule2_js_1.default.api2()
count = 0
exports_1('increase', increase = function() {
return ++count
})
exports_1('reset', reset = function() {
count = 0
console.log(' .')
})
exports_1('default', {
increase,
reset
})
}
}
})
The new modular ES6 syntax is gone. But the code will work fine in older browsers. This transpilation can be done automatically using Webpack, TypeScript, etc.Dynamic module loading
SystemJS also contains a function import
for dynamic import:
System.import('./esCounterModule.js').then(dynamicESCounterModule => {
dynamicESCounterModule.increase()
dynamicESCounterModule.reset()
})
Webpack module: compilation and assembly of CJS, AMD and ES modules
Webpack is a module builder. Its transpiler combines CommonJS, AMD and ES modules into a single balanced modular template and collects all the code into a single file. For example, in the following 3 files, 3 modules are defined using different syntax:
define('amdDependencyModule1', () => {
const api1 = () => {}
return {
api1
}
})
const dependencyModule2 = require('./commonJSDependencyModule2')
const api2 = () => dependencyModule1.api1()
exports.api2 = api2
import dependencyModule1 from './amdDependencyModule1'
import dependencyModule2 from './commonJSDependencyModule2'
let count = 0
const increase = () => ++count
const reset = () => {
count = 0
console.log(' .')
}
export default {
increase,
reset
}
The following code demonstrates the use of this module:
import counterModule from './esCounterModule'
counterModule.increase()
counterModule.reset()
Webpack is able to combine these files, despite the fact that they are different modular systems, into one file main.js
: root
dist
main.js (assembly of files located in the src folder)
src
amdDependencyModule1.js
commonJSDependencyModule2.js
esCounterModule.js
index.js
webpack.config.js
Since Webpack is based on Node.js, it uses the modular syntax of CommonJS. In webpack.config.js
: const path = require('path')
module.exports = {
entry: './src/index.js',
mode: 'none',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
}
To compile and build, you must run the following commands: npm install webpack webpack-cli --save-dev
npx webpack --config webpack.config.js
As a result, Webpack will create the file main.js
. The following code is main.js
formatted to improve readability: (function(modules) {
var installedModules = {}
function require(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports
}
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
}
modules[moduleId].call(module.exports, module, module.exports, require)
module.l = true
return module.exports
}
require.m = modules
require.c = installedModules
require.d = function(exports, name, getter) {
if (!require.o(exports, name)) {
Object.defineProperty(exports, name, {
enumerable: true,
get: getter
})
}
}
require.r = function(exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {
value: 'Module'
})
}
Object.defineProperty(exports, '__esModule', {
value: true
})
}
require.t = function(value, mode) {
if (mode & 1) value = require(value)
if (mode & 8) return value
if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value
var ns = Object.create(null)
require.r(ns)
Object.defineProperty(ns, 'default', {
enumerable: true,
value: value
})
if (mode & 2 && typeof value !== 'string')
for (var key in value) require.d(ns, key, function(key) {
return value[key]
}.bind(null, key))
return ns
}
require.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() {
return module['default']
} :
function getModuleExports() {
return module
}
require.d(getter, 'a', getter)
return getter
}
require.o = function(object, property) {
return Object.prototype.hasOwnProperty.call(object, property)
}
require.p = ''
return require(require.s = 0)
})([
function(module, exports, require) {
'use strict'
require.r(exports)
var esCounterModule = require(1)
esCounterModule['default'].increase()
esCounterModule['default'].reset()
},
function(module, exports, require) {
'use strict'
require.r(exports)
var amdDependencyModule1 = require.n(require(2))
var commonJSDependencyModule2 = require.n(require(3))
amdDependencyModule1.a.api1()
commonJSDependencyModule2.a.api2()
let count = 0
const increase = () => ++count
const reset = () => {
count = 0
console.log(' .')
}
exports['default'] = {
increase,
reset
}
},
function(module, exports, require) {
var result!(result = (() => {
const api1 = () => {}
return {
api1
}
}).call(exports, require, exports, module),
result !== undefined && (module.exports = result))
},
function(module, exports, require) {
const dependencyModule1 = require(2)
const api2 = () => dependencyModule1.api1()
exports.api2 = api2
}
])
And again, this is just IIFE. The code from 4 files is converted into an array of 4 functions. And this array is passed to the anonymous function as a parameter.Babel module: transpilation of ES module
Babel is another transporter for supporting ES6 + code in older browsers. The above ES6 + module can be converted to a Babel module as follows:
Object.defineProperty(exports, '__esModule', {
value: true
})
exports['default'] = void 0
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {
'default': obj
}
}
var dependencyModule1 = _interopRequireDefault(require('./amdDependencyModule1'))
var dependencyModule2 = _interopRequireDefault(require('./commonJSDependencyModule2'))
dependencyModule1['default'].api1()
dependencyModule2['default'].api2()
var count = 0
var increase = function() {
return ++count
}
var reset = function() {
count = 0
console.log(' .')
}
exports['default'] = {
increase: increase,
reset: reset
}
And here is the code in index.js
, demonstrating the use of this module:
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {
'default': obj
}
}
var esCounterModule = _interopRequireDefault(require('./esCounterModule.js'))
esCounterModule['default'].increase()
esCounterModule['default'].reset()
This is the default transpilation. Babel also knows how to work with other tools.Babel and SystemJS
SystemJS can be used as a plugin for Babel: npm install --save-dev @ babel / plugin-transform-modules-systemjs
This plugin should be added to babel.config.json
: {
'plugins': ['@ babel / plugin-transform-modules-systemjs'],
'presets': [
[
'@ babel / env',
{
'targets': {
'ie': '11'
}
}
]
]
}
Now Babel can work with SystemJS to transpile CommonJS / Node.js, AMD / RequireJS and ES modules: npx babel src --out-dir lib
Result: root
lib
amdDependencyModule1.js (transpiled using SystemJS)
commonJSDependencyModule2.js (transpiled using SystemJS)
esCounterModule.js (transpiled using SystemJS)
index.js (transpiled using SystemJS)
src
amdDependencyModule1.js
commonJSDependencyModule2.js
esCounterModule.js
index.js
babel.config.json
The entire syntax of AMD, CommonJS and ES modules is transposed into SystemJS syntax:
System.register([], function(_export, _context) {
'use strict'
return {
setters: [],
execute: function() {
define('amdDependencyModule1', () => {
const api1 = () => {}
return {
api1
}
})
}
}
})
System.register([], function(_export, _context) {
'use strict'
var dependencyModule1, api2
return {
setters: [],
execute: function() {
dependencyModule1 = require('./amdDependencyModule1')
api2 = () => dependencyModule1.api1()
exports.api2 = api2
}
}
})
System.register(['./amdDependencyModule1', './commonJSDependencyModule2'], function(_export, _context) {
var dependencyModule1, dependencyModule2, count, increase, reset
return {
setters: [function(_amdDependencyModule) {
dependencyModule1 = _amdDependencyModule.default
}, function(_commonJSDependencyModule) {
dependencyModule2 = _commonJSDependencyModule.default
}],
execute: function() {
dependencyModule1.api1()
dependencyModule1.api2()
count = 0
increase = () => ++count
reset = () => {
count = 0
console.log(' .')
}
_export('default', {
increase,
reset
})
}
}
})
System.register(['./esCounterModule'], function(_export, _context) {
var esCounterModule
return {
setters: [function(_esCounterModule) {
esCounterModule = _esCounterModule.default
}],
execute: function() {
esCounterModule.increase()
esCounterModule.reset()
}
}
})
TypeScript module: transpilation of CJS, AMD, ES and SystemJS modules
TypeScript supports all flavors of JS syntax, including ES6. During transpilation, the syntax of the ES6 module can be saved or converted to another format, including CommonJS / Node.js, AMD / RequireJS, UMD / UmdJS or SystemJS according to the settings of the transpilation in tsconfig.json
: {
'compilerOptions': {
'module': 'ES2020' // None, CommonJS, AMD, System,
UMD, ES6, ES2015, ESNext
}
}
For instance:
import dependencyModule from './dependencyModule'
dependencyModule.api()
let count = 0
export const increase = function() {
return ++count
}
var __importDefault = (this && this.__importDefault) || function(mod) {
return (mod && mod.__esModule) ? mod : {
'default': mod
}
}
exports.__esModule = true
var dependencyModule_1 = __importDefault(require('./dependencyModule'))
dependencyModule_1['default'].api()
var count = 0
exports.increase = function() {
return ++count
}
var __importDefault = (this && this.__importDefault) || function(mod) {
return (mod && mod.__esModule) ? mod : {
'default': mod
}
}
define(['require', 'exports', './dependencyModule'], function(require, exports, dependencyModule_1) {
'use strict'
exports.__esModule = true
dependencyModule_1 = __importDefault(dependencyModule_1)
dependencyModule_1['default'].api()
var count = 0
exports.increase = function() {
return ++count
}
})
var __importDefault = (this & this.__importDefault) || function(mod) {
return (mod && mod.__esModule) ? mod : {
'default': mod
}
}
(function(factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
var v = factory(require, exports)
if (v !== undefined) module.exports = v
} else if (typeof define === 'function' && define.amd) {
define(['require', 'exports', './dependencyModule'], factory)
}
})(function(require, exports) {
'use strict'
exports.__esModule = true
var dependencyModule_1 = __importDefault(require('./dependencyModule'))
dependencyModule_1['default'].api()
var count = 0
exports.increase = function() {
return ++count
}
})
System.register(['./dependencyModule'], function(exports_1, context_1) {
'use strict'
var dependencyModule_1, count, increase
car __moduleName = context_1 && context_1.id
return {
setters: [
function(dependencyModule_1_1) {
dependencyModule_1 = dependencyModule_1_1
}
],
execute: function() {
dependencyModule_1['default'].api()
count = 0
exports_1('increase', increase = function() {
return ++count
})
}
}
})
The modular ES syntax supported by TypeScript is called external modules.Internal Modules and Namespace
TypeScript also has keywords module
and namespace
. They are called internal modules: module Counter {
let count = 0
export const increase = () => ++count
export const reset = () => {
count = 0
console.log(' .')
}
}
namespace Counter {
let count = 0
export const increase = () => ++count
export const reset = () => {
count = 0
console.log(' .')
}
}
Both are transposed into JS objects: var Counter;
(function(Counter) {
var count = 0
Counter.increase = function() {
return ++count
}
Counter.reset = function() => {
count = 0
console.log(' .')
}
})(Counter || (Counter = {}))
TypeScript module and namespace can have several levels of nesting through the separator .
: module Counter.Sub {
let count = 0
export const increase = () => ++count
}
namespace Counter.Sub {
let count = 0
export const increase = () => ++count
}
Sub module and sub namespace are transposed into object properties: var Counter;
(function(Counter) {
var Sub;
(function(Sub) {
var count = 0
Sub.increase = function() {
return ++count
}
})(Sub = Counter.Sub || (Counter.Sub = {}))
})(Counter || (Counter = {}))
TypeScript module and namespace can also be used in a statement export
: module Counter {
let count = 0
export module Sub {
export const increase = () => ++count
}
}
module Counter {
let count = 0
export namespace Sub {
export const increase = () => ++count
}
}
The above code also translates to sub module and sub namespace: var Counter;
(function(Counter) {
var count = 0
var Sub;
(function(Sub) {
Sub.increase = function() {
return ++count
}
})(Sub = Counter.Sub || (Counter.Sub = {}))
})(Counter || (Counter = {}))
Conclusion
Welcome to JS, which has 10+ systems / modulation formats / namespaces:- IIFE module: JS module template
- Open module: open JS module template
- CJS module: CommonJS module or Node.js module
- AMD module: asynchronous module definition or RequireJS module
- UMD module: universal module definition or UmdJS module
- ES module: ECMAScript2015 or ES6 module
- ES dynamic module: ECMAScript2020 or ES11 dynamic module
- System Module: SystemJS Module
- Webpack module: compilation and assembly of CJS, AMD and ES modules
- Babel module: transpilation of ES module
- TypeScript module and namespace
Fortunately, JS currently has standard built-in tools for working with modules supported by Node.js and all modern browsers. For older browsers, you can use the new modular ES syntax, translating it into compatible syntax using Webpack / Babel / SystemJS / TypeScript.Thank you for your time. I hope it was well spent.