An intro to JavaScript Modules

Modules divide programs into clusters of code that, by some criterion, belong together. Most programming languages have a scope level between global (everyone can see it) and local (only this function can see it). JavaScript does not. The only way to create a new scope in JavaScript is with a function. Hence, modules are god sent.

We should design our software separated into different pieces where each piece has a specific purpose and clear boundaries for how it interacts with other pieces. In software these pieces are called modules.

Each module has three parts:

  • dependencies
    When one module needs another module, it can import that module as a dependency.
  • code
    the business logic of the module
  • exports
    the interface to the module. Whatever you export from the module will be the only bits available to whoever imports the module.

Separating code into multiple files

The first intuition to create modules was to separate code by files.

//-------- users.js --------//

var users = ["Tyler", "Sarah", "Dan"]
function getUsers() {
  return users
}

//-------- dom.js --------//

function addUserToDOM(name) {
  // ...
}

// Using the global Window object in the browser
var users = window.getUsers()


//-------- index.html --------//

<html>
  <head>...</head>
  <body>
    ...
    http://users.js
    http://dom.js
  </body>
</html>

Literally all we’ve done here is separate where the code lives. All the variables declared that aren’t in a function are just living on the global Window object. Any user of the website can access, and worse, change the behaviour of our getUsers function. There’s nothing modular about this, the code can’t be neatly dropped into another app without side-effects as it changes the global scope.

IIFE Module Pattern

Instead of having our entire app languishing in the global namespace at the mercy of the user, what if we instead expose a single object. Let’s call it App. We can then put all our methods under App and avoid polluting the global namespace. Finally, we could wrap everything else (code in user.js etc.) in a function to keep it enclosed from the rest of the app.

app.js

var App = {}    // globally accessible

users.js

function usersWrapper() {
  var users = ["Tyler", "Sarah", "Dan"]

  function getUsers() {
    return users
  }

  App.getUsers = getUsers  // attach function to global App
}

usersWrapper()

dom.js

function domWrapper() {
  function addUserToDOM(name) {
    // ...
  }

  document.getElementById("submit")
    // ...
  })

  var users = App.getUsers()
}

domWrapper()

index.html

<!DOCTYPE html>
<html>
  ...
  <body>
    ...
    http://app.js
    http://users.js
    http://dom.js
  </body>
</html>

Now the window object has references to only App and the wrapper functions usersWrapper et al. More importantly, since the important code was declared within the wrapper functions, it can’t be modified from the global scope. It can still be accessed through the App object of course.

Since we are immediately invoking our wrapper functions, and not using them elsewhere, it is safe to replace them with Immediately Invoked Function Expressions (IIFEs), like so:

(function () {
  var users = ["Tyler", "Sarah", "Dan"]

  function getUsers() {
    return users
  }

  App.getUsers = getUsers
})()

Now the window object will have a reference to App only, which we use as a namespace for all the application code.

The IIFE Module Pattern avoids dumping everything onto the global namespace. This helps with variable collisions and keeps code more private. It still has some downsides. The App variable may collide with a globally-scoped variable from a dependency. Also, the scripts must be in the exact order they are now, otherwise the app will break. We can’t simply use users.js in a file without first importing app.js.

CommonJS

The CommonJS group defined a module format to solve JavaScript scope issues by making sure each module is executed in its own namespace. This is achieved by forcing modules to explicitly export those variables it wants to expose to the “universe” and also by explicitly defining those other modules it requires to properly work.

So the CommonJS standard:

  • is file-based: each file is its own module
  • expects explicit imports or dependencies
  • expects explicit exports which will be available to any other file that imports the module

All the information regarding the module is stored in a module object.
To export an item, simply add it to the module.exports object. E.g.
users.js

var users = ["Tyler", "Sarah", "Dan"]

function getUsers() {
  return users
}

module.exports.getUsers = getUsers

// or

module.exports = {
  getUsers: getUsers
}

// or

module.exports = {
  getUsers: function() {
    return users
  }
}

require is a function that takes a string path as its first argument and returns whatever is being exported from that path. To import the users.js module above:

const users = require("./users")

users.getUsers()    // ["Tyler", "Sarah", "Dan"]

Node.js uses the CommonJS specification to implement modules. So with Node you get modules out of the box using the CommonJS require and module.exports syntax.

However, browsers don’t support CommonJS. CommonJS loads modules synchronously. This is great for servers, but in the browser this would make a terrible user experience.

There’s a way to use CommonJS modules on the web. Use a module bundler.

Module Bundler

Module bundlers examine the entire code base, look at all the import/export, then intelligently bundle all of your browser-incompatible CommonJS modules together into a single JavaScript file which the browser can understand. Webpack is a popular module bundler.

app.js ---> |         |
users.js -> | Bundler | -> bundle.js
dom.js ---> |         |

ES Modules

ES Modules are now the standardised way to create modules in JavaScript. They are native to JavaScript and modern browsers fully support them. Unfortunately, they won’t be fully supported by Node. Another explanation for that.

In order to specify exports, use the export keyword. Use import to import modules.
utils.js

// Not exported
function once(fn, context) {
  var result
  return function() {
    if(fn) {
      result = fn.apply(context || this, arguments)
      fn = null
    }
    return result
  }
}

// Exported
export function first (arr) {
  return arr[0]
}

// Exported
export function last (arr) {
  return arr[arr.length - 1]
}

app.js

// Import everything from the module and encase them
// into 'tools' namespace
import * as tools from './utils'

tools.first([1, 2, 3])   // 1

It’s not necessary to import everything a module is exporting. A feature called named imports makes this possible. Caution: it looks like destructuring but it’s not.

import { first } from './utils'

first([1, 2, 3])  // 1

EcmaScript also introduces the concept of default export. This is the value that’s imported if the import declaration doesn’t explicitly specify what to import (using a * or named imports).

// module.js

export default function hello() {
  return "World"
}

export function add(a, b) {
  return a + b
}

// app.js

import greeting from './module'

greeting()  // "World!"

// dom.js

import greeting, { add } from './modules'

greeting()   // "World!"
add(1, 2)    // 3

ES Modules are imported in HTML by specifying the script type as module:

http://dom.js

If the dom.js is dependent on other modules, those need not be specified explicitly here (unlike the IIFE module pattern), they will be imported automatically.