Topic 16a: Using Modules To Develop Browser Code

In the last few sessions we have looked at node.js and seen that we can use modules to develop reusable code, for example a database connection, or a set of routes. You have also imported third-party modules, such as Express or Mongoose. These modules are referred to as CommonJS modules.

However it is also possible, with the help of a few tools, to use modules when developing client-side, browser-based JavaScript. For example, you might have developed a few JavaScript classes which could be reused in different web applications. You could create a module from each class by writing the class in its own .js file and exporting it. For example, we could create this file, cat.js:

class Cat {
    constructor(n, a, w) {
        this.name = n;
        this.age = a;
        this.weight = w;
    }

    eat() {
        this.weight++;
    }

    walk() {
        this.weight--;
    }

    toString() {
        return `${this.name} ${this.age} ${this.weight}`;
    }
}

module.exports = Cat;
Note how we are exporting the Cat class from the module, just like we did in our Node modules. We could then use this module in other JavaScript code, e.g, we could create this "main" JavaScript file, index.js:
const Cat = require('./cat'); // the Cat class is exported from the module
const cat = new Cat("Tiddles", 10, 10);
cat.walk();
cat.walk();
cat.eat();
console.log(cat);
Note how we require the module just as in node.js. Also note the single dot (.); this means the module is in the current folder. Also note that we can leave out the .js extension when importing a module.

How would we run our "main" JavaScript (index.js)? Note there is no init() function or equivalent. We can run arbitrary JavaScript code (without a startup function) by adding a defer option to our <script> tag. This will prevent the script running until the page has been fully loaded. For example:

<script type='text/javascript' src='index.js' defer />
This will cause the code in index.js to (in theory) run when the page has been loaded.

However, this code, as it stands, will not actually run successfully in the browser. The browser will not be able to make sense of the require() call. There is a new module mechanism, ECMAScript 6 modules, which will allow browsers to run modules directly, but at the moment, these are not fully supported in browsers or in node.js, and part of the rationale for using modules in code intended for the browser is to allow it to be reused with Node applications.

Browserify

There are a number of tools - "bundlers" - which can be used to prepare CommonJS modules for the browser. One of these tools is Browserify (see browserify.org). Browserify can transform code consisting of modules into a single "bundle" file which can be used directly in the browser. Browserify can be installed with npm, e.g. globally:

sudo npm install -g browserify
Here is the most simple usage:
browserify index.js > bundle.js
This will take the file index.js, and any modules it uses (along with any modules used by those modules) and prepare a single bundle file, bundle.js. This can then be used directly in the browser with the defer attribute to ensure it's only loaded when the page is loaded, e.g:
<script type='text/javascript' src='bundle.js' defer />
One key advantage of using a bundler like Browserify is it allows you to use various modules provided by npm in browser code. These would typically be required, so by bundling code which uses npm modules, you can make it run in the browser - as long as the npm modules you use are not Node-specific.

Babel and uglify-js

This is also a good point to mention two other common tools, Babel and uglify-js. What do these do? First, Babel (see here). Babel is known as a transpiler. It converts between different versions of JavaScript. It is commonly used to transform ECMAScript 6 code (classes, arrow functions, etc) into "classic" JavaScript so that the code can run on older browsers. Babel requires presets to run, to tell it what transformations to make. The env preset is a common one, this tells Babel to transform ECMAScript6+ code into ECMAScript 5. For example:

babel --presets @babel/preset-env cat.js > cat-ecma5.js

Babel can be installed with:

sudo npm install -g @babel/core @babel/cli
(babel-cli is the babel command-line tool)

Browserify transforms

However, what if we wanted to use Browserify and Babel in one go, to create an ECMAScript 5 bundle compatible with old browsers? We can do that by using a Browserify transform - a plugin which adds third-party functionality to Browserify. Such a transform exists for Babel: babelify (see here). This can be installed with npm in the normal way, as long as Babel is already installed:

sudo npm install -g babelify
You can then use Browserify with the -t (transform) option, e.g.:
browserify index.js -t [ babelify --presets [ @babel/preset-env ] ] > bundle-ecma5.js

uglify-js

Another useful tool is uglify-js (see here) This is an example of a minifier - a tool which removes the whitespace from a bundle to reduce its filesize and speed up loading time. Its usage is quite easy:

uglifyjs bundle.js > bundle-uglified.js
It can be installed with:
npm install -g uglify-js
However, if using with browserify, we can again perform both operations (bundle creation and minification) in one go, using another transform. The transform for uglify-js is called uglifyify(!) and is actually based not on uglify-js itself, but a fork, Terser, which has been adapted to handle ECMAScript 6 syntax (plain uglify-js itself cannot do this). This can be used with the -t option of browserify as before:
browserify index.js -t uglifyify > bundle-minified.js
and can be installed with:
sudo npm install -g uglifyify