JavaScript Modules

source

The modern world is filled with complicated engineered devices, these devices are all constructed from multiple parts that all go together to make the whole, without this separation of concerns, it would be impossible to construct most of them due to any one of a multitude of factors.

Imagine all of the parts that go into making something as complex as a mechanical watch, the attention to detail in every part and how those parts are designed to be modular for their construction. The benefits of so doing are many, one particularly useful one is reusability. Software can be designed and built in the same way and by so doing, many of the benefits found when constructing physical devices are also found in code, that is to say, when code is constructed from multiple elements that are then brought together to constitute the final program, the complexity is manageable and parts can be reused in other programs.

In software these 'pieces' of the whole are called modules; A module in JavaScript has three parts.

Example module


// imports
import React from "react";
import { createMemoryHistory } from "history";
import Router from "./Router";

// code
class MemoryRouter extends React.Component {
  history = createMemoryHistory(this.props);
  render() {
    return (
    <Router
      history = {this.history}
      children = {this.props.children}
    />;
    )
  }
}

// exports
export default MemoryRouter
				

Modules bring us:

If a module would be of use in another program, it can be make into a package, a package can contain one or more modules and can be uploaded to a package management service such as npm.

The power of having the many thousands of modules available to you in the package libraries affords great leverage in solving problems with pre existing code.

Having code in modules brings it isolation, making reasoning about the code, solving problems or just working on it, far easier, it also helps enormously in isolating the name space.

Modules make the ensemble of the program much more organised, assisting again in reasoning about the program, sharing code and also removing it.

Old Style Module Code

source

Writing a small application using old style javascript, our first instict is that we might well be to do so at the file level. In this example we have three files, our html file and two javascript files: one for users, the other for the DOM.

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Users</title>
  </head>
  <body>
    <h1>Users</h1>
    <ul id="users"></ul>
    <input
      id='input'
      type='text'
      placeholder='new user'>
    </input>
    <button id='submit'>submit</button>
    <script src='users.js'></script>
    <script src='dom.js'></script>
  </body>
</html>
				

users.js

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

function getUsers() {
  return users;
}
				

dom.js

function addUserToDOM(name) {
  const node = document.createElement('li');
  const text = document.createTextNode(name);
  node.appendChild(text);

  document.getElementById('users')
    .appendChild(node);
}

document.getElementById('submit')
  .addEventListener('click', function() {
    var input = document.getElementById('input');
    addUserToDOM(input.value);
    input.value = '';
  });

var users = window.getUsers();
for (var i = 0; i < users.length; i++) {
  addUserToDOM(users[i])
}
				

Using separate files like this separates the code but these are not modules, the scope is global. With the script tags inside the html file, it is as if all of the code is sitting where the tags are found after the html markup. All of the function that we have declared can be accessed by the global window object.

Wrapper Functions

We can confine our scope somewhat by defining an APP variable in the global window space, and then wrapping our users function and variable in another function and calling that from the window scope, from within this function, we then add our getUsers function to the global APP object, and call this function from our APP object rather than the window

The same for dom.js, a function called domWrapper to get the dom functions out of the global scope.

app.js

var APP = {}
				

users.js

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

  function getUsers() {
    return users;
  }

  APP.getUsers = getUsers
}

usersWrapper();
				

dom.js


function addUserToDOM(name) {
  const node = document.createElement('li');
  const text = document.createTextNode(name);
  node.appendChild(text);

  document.getElementById('users')
    .appendChild(node);
}

document.getElementById('submit')
  .addEventListener('click', function() {
    var input = document.getElementById('input');
    addUserToDOM(input.value);
    input.value = '';
  });

var users = APP.getUsers();
for (var i = 0; i < users.length; i++) {
  addUserToDOM(users[i]);
}
				

Now when we look at our application in the browser we no longer have all the user data and functions in the global scope, we have the APP object and our two wrapper functions instead.

We are closer to modular code, but this is still at best a hack. The next step will be to try to remove the wrapper functions from the global name space and add those also to the APP object.

IIFE Module Pattern

In the previous code we created wrapper functions and invoked them immediately, in JavaScript there is more concise method by which we can achieve the same. A pattern known as an "immediately invoked function expression" or IIFE, It constitutes an anonymous function, wrapped in parenthesis, and invoked upon its declaration.

app.js


(function() {
  console.log("This is an IIFE function")
})()
				

The function is defined and wrapped in brackets which are immediately invoked with function parenthesis; By using this instead of defining functions with names in the global space, the functions no longer appear on the global window object.

users.js


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

  function getUsers() {
    return users
  }

  APP.getUsers = getUsers
})()
				

dom.js


(function () {
  function addUserToDOM(name) {
    const node = document.createElement('li');
    const text = document.createTextNode(name);
    node.appendChild(text);

    document.getElementById('users')
      .appendChild(node);
  }

  document.getElementById('submit')
    .addEventListener('click', function() {
      var input = document.getElementById('input');
      addUserToDOM(input.value);
      input.value = '';
    });

  var users = APP.getUsers();
  for (var i = 0; i < users.length; i++) {
    addUserToDOM(users[i])
  }
})()
				

There are a couple of downsides to IIFE such as the annoyance of having to wrap every file within an enveloping function

We also still have an APP object on the global name space, if any other code within this space decides to use the same name, then we are in trouble, bad things can happen.

Finally the order in which the scripts are called matters, if the script that use the APP object are not called after the object has been defined, this will cause errors. Which is not such an issue when you are working by yourself, but can quickly become problematic if you are in a team and many people are contributing to the code base.

CommonJS Modules

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

Pros
none
Cons
Browsers
Synchronous

Module Bundler

webpacks

The module bundler is a work around solution for the lack of built in modules in JavaScript, what it does is to parse all of you script files, it then intelligently bundles all of your modules together into a single file that the browser can understand, using all of your require statements and exports insuring that they are in the correct order and are not in the global scope; Now, rather than including all of our scripts in out html file, we need only include the bundler compose file, thus illuminating the problems caused by the complexity of sequential declaration and avoiding the global scope.


app.js  -> |         |
user.js -> | bundler | -> bundler.js
dom.js  -> |         |
				

To set up for using webpack we navigate into our project folder, for this I have simply created a folder within assets and will access the finished bundler.js file in there, I am also putting my javascript files in there too, and then initialise it with npm.


npm init -y
user@work:~$ /home/user/temp/package.json:

{
  "name": "temp",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
				

And then install webpack and webpack-cli:


npm install webpack webpack-cli
				

Create a new file: webpack.config.js this sets up the entry point for our code, the output file name and also the base file path for that file.


var path = require('path');

module.exports = {
  entry: './dom.js',
  output: {
    path: path.resolve(__dirname),
    filename: 'bundle.js'
  },
  mode: 'development'
}
				

We can now remove all the IIFE from the js script files, as each file is now its own module. Then we add exports to each so that the appropriate functions are accessible to the outside world.


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

function getUsers() {
  return users
}

module.exports = {
  getUsers: getUsers
}
				

In dom.js now we no longer call to APP.getUsers, instead we place a require stament that will retrieve it from the users.js file.


var getUsers = require('./users').getUsers;

function addUserToDOM(name) {
  const node = document.createElement('li');
  const text = document.createTextNode(name);
  node.appendChild(text);

  document.getElementById('users')
    .appendChild(node);
}

document.getElementById('submit')
  .addEventListener('click', function() {
    var input = document.getElementById('input');
    addUserToDOM(input.value);
    input.value = '';
  });

var users = APP.getUsers();
for (var i = 0; i < users.length; i++) {
  addUserToDOM(users[i])
}
				

The app.js file we can delete now, as it is no longer required. Which means that we can now go inside of index.html and delete the calls to our old js files and add only the link to our soon to be generated bundle.js file.


<!DOCTYPE html>
<html>
  <head>
    <title>Users</title>
  </head>
  <body>
    <h1>Users</h1>
    <input
      id='input'
      type='text'
      placeholder='new user'>
    </input>
    <button id='submit'>submit</button>
    <script src='assets/webpack/bundle.js'></script>
  </body>
</html>
				

Now to get webpack to compile our bundle.js file, we will add a script to the webpack generate package.json file instructing it how to do so.


{
  "name": "webpack",
  "version": "1.0.0",
  "description": "",
  "main": "webpack-app.js",
  "scripts": {
    "start": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "webpack": "^5.75.0",
    "webpack-cli": "^5.0.1"
  }
}
				

Inside the terminal within our webpack folder we type:


npm run start
				

... and our code is working again and we no longer have the window.APP in the global scope.

By now we should be asking why it is that JavaScript does not have a built in module system. Fortunately for us ES6 has added exactly that!

ES Modules

The modules functionality was added to the ECMAscript specification and thus to JavaScript in ES6, there are really three things that are really important to understand, that are the differences between CommonJS and ES modules.

first off is that out of the box ES modules support async, where previously we have had to use webpack to make sure that every thing loaded in the correct order, now with async functionality this is no longer required as async support comes with the language.

Next we have the import keyword, so instead of requiring something we will import it.

Finally we have the export keyword, so rather than calling exports we just export it.

Lets take a look at this in use; Supposing that we have an utils.js file, we can export whatever we want from it with the export keyword.

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];
}
				

... and then to import from utils.js


// To import everything
import * as utils from './utils'

utils.first([1,2,3]); // 1
utils.last([1,2,3]); // 3

// Named import
import { first } from './utils'

first([1,2,3]);
				

default exports

Using default imports changes a little how we import it.

leftpad.js


export default function leftpad (str, len, ch) {
  var pad = '';
  while (true) {
    if (len ∧ 1) pad += ch;
    len >>= 1;
    else break;
  }
  return pad + str;
}
				

... and to import:


import leftpad from './leftpad'
				

mixed import export

If we have mixed exported functions along with a default function we can import them like so:


import leftpad, { first, last } from './utils'
				

Users Module

Applying this to our code, we just add the export and import to our existing files.

users.js


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

export default function getUsers() {
  return users
}
				

dom.js


import getUsers from "./users.js"

function addUserToDOM(name) {
  const node = document.createElement('li');
  const text = document.createTextNode(name);
  node.appendChild(text);

  document.getElementById('users')
    .appendChild(node);
}

document.getElementById('submit')
  .addEventListener('click', function() {
    var input = document.getElementById('input');
    addUserToDOM(input.value);
    input.value = '';
  });

var users = getUsers();
for (var i = 0; i < users.length; i++) {
  addUserToDOM(users[i]);
}