Video Tutorial Modules
In this video I invite you to discover and understand the different module systems in JavaScript.
Why do we need a module system?
When we started writing JavaScript a single file was enough, but very quickly we needed to avoid repetitions. The simplest approach to have reusable code is to inject a variable into the global namespace (window
in the case of the browser).
var variable1 = 4
function mySuperFunction () {
// The code of the function
}
window. mySuperFunction = mySuperFunction
In order to avoid that the code of our library overflows on the code of the rest of the application, we will use an IIFE (Immediately invoked function expression). The use of a function makes it possible to limit the scope of the variables (especially at the time when only the keyword var
existed).
; (function () {
var variable1 = 4
function mySuperFunction () {
// The code of the function
}
window. mySuperFunction = mySuperFunction
}) ()
This IIFE makes sure that the variables do not overflow, but we can also use it with parameters to define the "dependencies" of our code.
; (function (myFunc) {
console.log ('Here is the result:' + myFunc ())
}) (window.mySuperFunction)
This approach was widely used with jQuery for example.
; (function ($) {
$ ('. demo'). click (function () {
$ (this) .slideToggle ()
})
}) (jQuery)
The limits of this approach
Unfortunately this approach is not extensible and starts to cause problems when you have several dependencies (the files must be included in a specific order). It was therefore necessary to create a system capable of resolving dependencies and organizing the order of execution automatically.
The solutions
CommonJS
The objective of CommonJS was to find a definition format that works with the JavaScript language (without necessarily being constrained to the limitations of browsers). A module is written in the classic way (without IIFE) and will receive an object module
which will contain a property exports
which will allow you to export what you want.
let count = 0
let step = 1
function increment () {
count + = step
return count
}
function decrement () {
count - = step
return count
}
module.exports = {
increment,
decrement
}
It is then possible to use this module using the function require ()
to which we will pass the path of our module.
const incr = require ('./ incrementer.js')
const count = document.querySelector ('# count')
document.querySelector ('# increment'). addEventListener ('click', function () {
count.innerHTML = incr.increment ()
})
The system will then take care of automatically including the file and returning what was exported in the return. If there are any sub-dependencies they will be automatically resolved.
This approach was taken by NodeJS and works very well on the server side. On the other hand, it is not possible to use it directly on the browser side and it will require a tool to convert the CommonJS code into browser compatible code. A simple example of an implementation is to create an object to represent the different modules and to encompass the different modules in a function.
modules ('./ app.js') = function (require, module) {
// The code of app.js
const incr = require ('./ incrementer.js')
const count = document.querySelector ('# count')
document.querySelector ('# increment'). addEventListener ('click', function () {
count.innerHTML = incr.increment ()
})
}
This transformation can be done through different tools such as Webpack or ParcelJS.
AMD
Not everyone was satisfied with the direction taken by CommonJS and a group of people decided to develop another module definition system, which would work directly on browsers and support asynchronous loading.
define (('jquery'), function ($) {
return function (selector) {
$ (selector) .click (function () {
$ (this)
.next ()
.slideToggle ()
})
}
})
A module can be named or represent the path of the JavaScript file.
define (('./ cart', './inventory'), function (cart, inventory) {
return {
color: 'blue',
size: 'large',
addToCart: function () {
inventory.decrement (this)
cart.add (this)
}
}
})
The advantage of this approach is that it can be used directly on browsers by setting this function define
(you can find more information on the reasons behind this mod system on the RequireJS page).
UMD
Now we find ourselves with several approaches to define a module and publish a library under these conditions had become problematic.
It is to remedy this problem that UMD (Universal Module Definition) was born. It allows you to define a module that will work with the 3 systems (CommonJS, AMD and window)
; (function (global, factory) {
typeof exports === 'object' && typeof module! == 'undefined'
? factory (exports, require ('react'))
: typeof define === 'function' && define.amd
? define (('exports', 'react'), factory)
: ((global = global || self), factory ((global.ReactDOM = {}), global.React))
}) (this, function (exports, React) {
// Some code
exports.createPortal = createPortal
exports.findDOMNode = findDOMNode
exports.render = render
exports.version = ReactVersion
})
To achieve these ends UMD will add a series of conditions to find out what type of module is used and will inject a parameter exports
to the function that will export what must be accessible.
ES205, a standard (ESM)
The solutions seen above are solutions to the absence of a native JavaScript module system. However, with the evolution of standards, JavaScript was equipped with such a system when EcmaScript 2015 arrived.
A script can be loaded into an HTML page as a module.
Inside this file it is then possible to use the keywords import
and export
.
let count = 0
let step = 1
export function increment () {
count + = step
return count
}
export function decrement () {
count - = step
return count
}
You can read more about the different export formats on the MDN documentation.
It is then possible to import one or more elements using the keyword import
.
import {increment} from './incrementer.js'
const count = document.querySelector ('# count')
document.querySelector ('# increment'). addEventListener ('click', function () {
count.innerHTML = increment ()
})
When the browser loads this script it will parse the file to discover the dependencies (here increment.js
). It will load and parse them to discover sub-dependencies if necessary (and so on). It is only after the entire dependency tree is discovered that the code can be executed.
Unfortunately, even though modern browsers support this syntax, this resolving cascade can cause performance issues (especially if there are a lot of files). Also, we will tend to use bundlers to resolve dependencies and generate a file that is easier to load for the browser. These tools also have the advantage of being able to do tree-shaking to import only the code you need. For example :
import {increment} from './incrementer.js'
// This code will be transformed like this
let count = 0
let step = 1
function increment () {
count + = step
return count
}
Function decrement
will be automatically deleted because it is not used in the final code (tree shaking is not possible with CommonJS).
ES2020, dynamic import
A new evolution of the native module system is coming to the fore and will allow asynchronous and dynamic import.
const count = document.querySelector ('# count')
document
.querySelector ('# increment')
.addEventListener ('click', async function () {
count.innerHTML = await import ('./ incrementer.js'). then (
({default: incr}) => {
return incr.increment ()
}
)
})
This system is interesting because it will allow to load certain parts of our script in a second time (very practical for imposing modules which are not present only on certain pages).
What syntax should I use?
With all these module systems we can get lost and even if it is interesting to know these different approaches, we will not tend to write modules using the ES2015 / ES2020 format.
On the server side, things are a little more complex, mainly because of NodeJS which offers still rough support for EcmaScript modules (ESM)
As of Node.js 14 there is no longer warning when using ESM in Node.js. However, the ESM implementation in Node.js remains experimental. (...) Users should be cautious when using the feature in production environments.
Suddenly a large part of the ecosystem (npm) uses, and continues to use the syntax CommonJS
. The bundlers will then play the role of gateway and allow the inclusion of the CommonJS module from an ESM syntax.
import React from 'react'
import ReactDOM from 'react-dom'
ReactDom
.render
// ...
()
In this case the module.exports
will be interpreted as a export default
.
Now we just have to wait for the ecosystem to adapt now that the modules exist in the language so as not to have to ask so many questions.