Basics of Modular JavaScript

Intended Audience

This article is for those just being introduced to JavaScript. When I was first introduced to it, I had a lot of questions about what the difference was between ECMA and JS, transpilers and bundlers, and on webpack itself. I couldn’t quite wrap my head around all of it. Then I found a course on PluralSight that explained Modular JavaScript flawlessly. But there are two problems. One is that Pluralsight is expensive and although I love their courses, the average student or new hire can’t exactly afford a subscription quite yet. Two, not a lot of people have time to watch a 2+ hour course online. So, summarize here are my learnings and my newfound understanding of the basics in modular JavaScript. I hope it helps someone.

ECMA vs. JavaScript?

ECMAScript is a standard for scripting languages and JavaScript is an implementation of ECMAScript. The acronym ES refers to ECMAScript. ECMA stands for European Computer Manufacturer’s Association; however, you just need to remember that ES and JavaScript are virtually one in the same except that JavaScript may provide some additional features.

JavaScript began as a client-side scripting language for web applications but its earlier version such as ES5 contained many limitations that prohibited creating classes, importing modules, etc. The first standardized version of JavaScript was referred to as ES5. The newest version today is referred to as ES6/ECMAScript 2015/ES2015; this is the very first update to the language since it was standardized in 2009. ES5 originally didn’t include support for modules. To understand why this is important, we need to understand why developers need modules and what they give us.

Why do developers need modules?

JavaScript began as a language for building small scale websites; but today, websites are becoming web applications and code bases and functionality are growing to be larger. Developers needed a way to separate functionality and simultaneously specify dependencies. Languages like C++ and Java rely on external libraries such as as a math library to compute basic math equations without having to rewrite them. So libraries enable languages to not repeat code and delegate functionality and this is similar to what modules gives JavaScript. Modules help developer’s separate functionality and organize the codebase. One of the largest reasons for modules is JavaScript’s global namespace which can become easily polluted. Functions are the only thing in JavaScript that create a new scope; thus anything not declared within a function is apart of the global namespace. Thus every function we declare in JavaScript is available globally and therefore its name cannot be used again. Often this leads to a polluted namespace where names and code can be difficult to find and reuse. But sometimes we don’t want that function to be available everywhere. In order to make functions in one JavaScript file available to just a single other JavaScript file, we can use modules. Although there are many other reasons, the top reasons for modular capabilities are:

  • Simplifying dependency management
  • Code Organization: let us divide up functionality of our application, provide encapsulation
  • Offers code reusability
  • Decoupling (easing pain in refactoring)
  • Code Extensibility

In sum, our applications would be pretty messy and poorly maintained without modularity.


Modules with ES5

All modern browsers support ES5, it is the backbone of the modern web. The point of this section is to give you familiarity with modules in ES5. The caveat is, ES5 doesn’t support modules. So, developers have come up with a lot of workarounds to support modularity and below are some of them.

Vanilla ES5

Sometimes you are constrained to using plain vanilla ES5 JavaScript and cannot use any third party libraries that support modularity or even ES6. In this case, there are two common patterns in JS to implement modules but they have positives and negatives.

Singleton Pattern

The singleton pattern takes advantage of what we called immediately invoked function expressions, commonly known as an IIFE. JavaScript has function level scope so all variables and functions declared within the function expression are not available in the global scope. The only thing in the global scope will be the name we choose to give to the function expression, if we choose to give it one ( we can use an anonymous function and avoid giving it a name at all). In our case, we will assign a variable name to a function expression which will be immediately invoked, leaving the return value of that function to be assigned to the variable we choose.

Lets say we declare a file called scoreboard.js. In here, we create a variable called scoreboard and assign it to a function definition which returns an object with our public API. The object returned has a member called showMessage which is assigned to the printMessage function declaration. Simiarily, the return object’s updateMessage member is assigned to its equally named function declaration. Neither showMessage or updateMessage are available in the global namespace but the variable scoreboard is.

var scoreboard = function() {
    var message = 'Welcome to the game!';
    function printMessage() {
         console.log(message);
    }
    function updateMessage(newMessage) {
        message = newMessage;
    }
    //return an object that represents our new module
    return {
         showMessage: printMessage,
         updateMessage: updateMessage
    }
}(); //← This is called a immediately invoked function definition,   
// or IIFE

To access the scoreboard module in another file, we just simply access the member variables that we specified in the object returned by the scoreboard function.

scoreboard.printMessage();
scoreboard.updateMessage(“Let the game begin!”);

As you can see in the above example, it doesn’t matter what I call the object’s members in my returned object, all that matters is that they are assigned the value of their respective functions in order to have the correct implementation. We also have used an immediately invoked function definition. We want to immediately invoke the function expression assigned to the scoreboard variable so that the return value is assigned to the scoreboard variable and can be used without invoking any function later on.

Constructor Pattern

Another pattern that you may see is called the constructor pattern. The main difference between this pattern and the singleton pattern is that this function expression is no longer immediately invoked and instead must be explicitly invoked when you wish to use the variable or module in our case.

var Scoreboard = function() {
    console.log('Creating a new scoreboard...');
    var message = 'Welcome to the game!';
    function printMessage() {
        console.log(message);
    }
    return {
        printMessage: printMessage
    }
}; 
/* no longer immediately executing the function -- function
definition is assigned to a variable which must later be
invoked. */

In order to use the scoreboard variable, we invoke it using the ‘new’ keyword and assign the return value of that invoked function to a new variable.

var myScoreboard = new Scoreboard();
myScoreboard.printMessage();

Key Takeaways

  • These patterns are extremely useful when you are constricted to using plain old vanilla ES5
  • Adds one value to the global scope per module
  • No system for dependency management meaning we cannot specify that the a module called the ‘game’, will depend on the scoreboard module.

Formats and Loaders

Instead of relying on vanilla JavaScript, third party tools give us some advantages that ES5 cannot. These tools are commonly referred to as modules formats and loaders. A module format is a specified syntax that requires a loader to interpret the module syntax. A loader is a third party tool that can help interpret specific module formats. Most module formats are not native to JavaScript language and therefore cannot be used without a module loader. However, ES2015/ES6 includes built in support for modules and that syntax is considered to be a native module format which can be used without a loader. For now, I will be focusing on non-native module formats and loaders that can be used to interpret them.

The two most popular modular formats are CommonJS and AMD. There are different reasons for choosing one or the other but we won’t get into that here. Another module format you might see often is called UMD. The two most popular loaders are RequireJS, which is most commonly used to load AMD modules, and SystemJS which supports many different module formats. We will now look at two different combinations of the most popular module formats and loaders, but keep in mind, there are many other combinations to be made.

In quick summary:

AMD Format — Primarily used for JavaScript modules that need to be included in a browser as opposed to server side JavaScript

CommonJS Format — Primarily used in server side implementations as part of NodeJS applications

AMD & RequireJS

The AMD syntax provides us with a new function named define. The define function takes in two parameters, an array of dependencies as the first, and the function definition second. Define is not a function defined by JavaScript, we are actually calling a function provided by our AMD syntax that can then be interpreted by the loader.

Looking at our scoreboard example, we have no dependencies that we would specify in the first parameter but we would declare the function body in a similar way to before:

define([], function() {
    console.log('Creating a new scoreboard module');
    function addResult(newResult) { ... }
    function updateScoreboard() { ... }
    return {
        addResult: addResult,
        updateScoreboard: updateScoreboard
    }
});

If we wanted to say that our game.js module depends on the scoreboard module and player module, then we could do so like this:

define([‘./player’, ‘./scoreboard’], function(player, scoreboard) {

})

Where we pass in the file path to dependencies as an array into the first parameter (leaving off the .js extension), and the function body is still the second parameter taking in our specified dependencies as parameters.

In order for our browsers to be able to interpret this sort of code, we would use a loader like RequireJS. You can go through the following steps to install RequireJS

  1. Install RequireJS as a dev dependency with npm: npm install --save bower-requirejs
  2. Include RequireJS library in your html within a script tag like so
<script data-main="js/app" src="node_modules/requirejs/require.js"></script>

where data-main specifies the entry point for your application.

CommonJS & SystemJS

With our commonJS format, modules look quite a bit different. We declare functions and in order to make those functions available within a module, we add these functions as members of the module.exports object.

console.log('Creating a new scoreboard');
var results = [];
function addResult(newResult) {
results.push(newResult);
}
function updateScoreboard() {
...
}
//a little bit different way of doing it than the player module
module.exports = {
    addResult: addResult,
    updateScoreboard: updateScoreboard
};

And to include our new scoreboard module in another file, we would use the require function defined by the CommonJS syntax. This function takes in a javascript file that another file may depend on. It will import the module and assign it to the variable scoreboard.

var scoreboard = require('./scoreboard.js');

To later use the functions in scoreboard.js, we would call

scoreboard.addResult(result);

SystemJS needs to be configured with addition javascript code to tell it what JavaScript format it is loading and where to start loading.

We would use SystemJS in a similar way to RequireJS.

  1. Install SystemJS with npm as a dev dependency npm install --save systemjs
  2. Import the SystemJS library into our html with a script tag. Then, configure SystemJS in either in a separate JavaScript file or in our html. We can include it in our html in between script tags like so:
<script src = "node_modules/systemjs/dist/system.js"></script>
<script>
// configure systemJS
    System.config({
        meta: {
format: 'cjs' //cjs refers to CommonJS format
}
});

System.import('js/app.js'); //import systemJS
</script>

System.import tells the loader where to begin loading JS files. Our import statement specifies that the root module is app.js which requires the use of all our other modules but is not a dependency itself. format: 'cjs' tells the config that we are loading the commonJS format.

3. You’re all good to go!

Key Takeaways

  • Module formats and loaders give you the ability to manage your dependencies. This means you can specify what files depend on what modules before hand.
  • Provides a common format for modules

Modules in ES6

What’s new in ECMA2015/ES6

ES6 includes a lot of new features to extend capabilities and remove limitations of ES5. ES6 now provides class syntax which makes OO pattern easier to implement. Let and const are new keywords that replace var. Let can be reassigned and const can only be assigned a value once. And finally, ES6 added support for modules. Although ES6 has some cool new features, a lot of these features are not compatible with most modern day web browsers. In the meantime, developers have come up with ways to include the features they want; tools named “transpilers” are being created to translate new versions of ES6 code into older versions such as ES5 so that browser can interpret this new module syntax. Most modern web applications are using these transpilers so that their code will be compatible with ES6 when it is released but also compatible with current web browsers in the meantime. First I will go over what a module in ES6 would look like and then address what tools we might need to use this new syntax.


Modules in ES6

Modules in ES6 are very simple to use. Like other usage of modules, ES6 provides a way to export modules so that they can be specified as dependencies to other modules.

Exporting Modules

There is more than one way to export modules in ES6.

  1. Include the keyword export in your function definition.
export function setName(name) { ... }
export function getName() { ... }
export function logPlayer() { ... }

2. Include an export object with the properties you want to be included in the module.

function addResult() { ... }
function updateScoreboard() { ... }
export { addResult, updateScoreboard };

Importing Modules

There are also different syntaxes you can use to import modules in JavaScript once they have been exported. Below are a few examples of how we would import the modules exported above.

  1. Import an entire module as scoreboard
import * as scoreboard from ‘./scoreboard.js’;

This would be equivalent to saying, “import all the properties we exported from scoreboard.js and call the module scoreboard”. Then we can simply call the properties of scoreboard like so:

scoreboard.updateScoreboard();

2. Import specific elements of a module

import { getName as getPlayerName, logPlayer } from ‘./player.js’;

We can also rename the properties we import from modules by using the as keyword. And we can specify which properties to import instead of importing everything contained within the player module. So the above import statement, would give us a function called getPlayerName which references the function getName in player.js and a function called logPlayer which references the logPlayer function in player.js. Since we have not specified a name for our module, we can simply call the functions like this from the game module:

function printGame() {
    logPlayer();
    console.log(getPlayerName() + “ is now playing”);
}

Transpilers

With the advent of modules in ES2015, we now don’t need to use any third party libraries to create modular JavaScript code. Unfortunately, modern day browsers don’t support ES2015 code quite yet, which means our user’s browsers cannot render the code we write in the newest version of JS. The solution to this has been transpilers. This word may sound similar to a compiler. Whereas a compiler transforms source code into a form of runnable code such as binary code, a transpiler takes source code in one language and transforms it to its equivalent source code in another language. What a transpiler does, is take ES6 code and translates it into ES5 code that is supported by modern browsers. This way we can write code in the newest version of JavaScript that will be supported in the future and can still be read by today’s applications. The most popular well known transpiler today is babel. On babel’s site, you can even test out how it would transpile your ES6 code into ES5 with the try it out feature. A sequence of steps to using babel might look something like this:

  1. Install with npm `npm install --save-dev babel-cli
  2. Install additional presets to apply certain transformations such as babel-preset-es2015 like so `npm install --save-dev babel-preset-es2015
  3. To run babel, navigate to the directory in node_modules/.bin/babel directory where babel was installed and run the command js — — preset s es2015 — — out-dir build The out-dir will specify where to put our transpiled JavaScript. Babel should tell you the destination of where the transpiled code went.

Bundlers

A bundle by definition is several objects bound together, thereby making a bundler a type of machine that does that binding. In JavaScript, a bundler does just that. It takes a group of JavaScript files together and binds them into a single JavaScript file. When we bind multiple JS files together, then there are less dependencies to load at startup time. Instead of loading 12 different javascript files for one HTML page, we can instead bundle them together and load just a single one when we load the page. But beware of the pitfall of bundlers:

When we bundle too many files together at once, loading that one bundled file can take so long it will interrupt the user’s experience instead of making it smoother.

A common module bundler today is Browserify and to install and use Browersify, you would go through the following steps:

  1. Write JS code in any modular format
  2. Install Browserify with npm install --save-dev browserify
  3. Make a directory which you want the bundled files to be held in such as build or bundledJS
  4. Browserify was installed at node_modules/.bin/browserify directory so we can run it from there by passing in the root module js/app.js and use the --outfile followed by your destination directory to specify where to put your bundled files and what to call it. Put it all together and you would run something like this js/app.js --outfile build/bundle.js

Overall, module bundlers:

  • Combine modules into fewer files
  • Decreases the number of dependencies to load which may decrease startup time to load the application

Webpack

Sometimes using these old Javascript formats and having a large amount of JavaScript code can get overwhelming. Every time you bundle your JS, it generates another file you need to include in your HTML and on top of transpiling them with babel, things can get confusing. So in order to solve this problem, Webpack has evolved to do everything at once. Webpack is a tool that does bundling, transpiling and a lot more by running a single command.

Figure 1. IMAGE REF: http://webpack.github.io/

What Figure 1 shows, is how webpack will take modules with dependencies, even css files and images, and bundle them into static assets we can include in our HTML code. So how exactly does webpack do this?

The Config File

The webpack config files specifies everything you need to run webpack such as the following:

  • An entry point for the application
  • An output file and location
  • Uses a regex expression to choose what files to run through the loader
  • Excludes files to not run through the loader
  • What type of loader to use
  • What presets to pass to the loader

Here is an example of one:

module.exports = {
    entry: './js/app.js', //an entry point for the application
    output: {
        path: './build', //location for output file
        filename: 'bundle.js', //name of output file
    },
    module: {
        loaders: [{
            test: /\.js$/, //run through files matching this regex
            exclude: /node_modules/, //exclude these files
            Loader:'babel-loader', //use a babel loader
            query: {
                presets: ['es2015'] //use this babel preset
            }
        }]
    }
};

Ta da! It is that simple. With this config file, you can simply run webpack from its location (for me that would be in the node_modules directory):

./node_modules/.bin/webpack

Then an output file is generated in the build directory, called bundle.js and I can include this file with one line in my HTML like so:

<script src = “./build/bundle.js”></script>

Of course, running the webpack command can be automated and doesn’t have to be done manually.

Key Takeaways

  • Modules are possible in plain vanilla javascript if you are constrained
  • Modules in earlier version of JavaScript are implemented using formats and loaders
  • More commonly though, modules are being implemented in ES2015
  • Browsers don’t yet support modules in ES2015
  • Transpilers can take different module formats in code not supported by browsers and convert it into a version of JavaScript supported by modern day browsers
  • Webpack can transpile and bundle JavaScript files for your, supporting dependency management and module clusters

References

The above code examples and most of the information were taken from a PluralSight course called ‘JavaScript Modules’. I do not claim to have come up this the code example on my own. This course gave me a lot of the information that I needed to learn all about modules in JavaScript. It is a highly recommended course, a long with many others PluralSight has to offer.