April 23, 2018 Viswesh Subramanian 0Comment

The current JavaScript landscape and its toolchain, especially module bundlers and loaders might seem intimidating for newer and seasoned developers. In another article, I wrote about the past, present and the future of modules bundlers and loaders. Get a quick rundown of the lay of the land before reading on.

So, why understand how old school module loaders worked? Well, to truly appreciate the current tools and utilities, we need to understand how javascript veterans in the past, handled asynchronous module loads and dependency management. The first ever widely accepted standard to stand the test of time was Asynchronous Module Definition (AMD) specification. There were 3 major implementations – Dojo Toolkit, Require JS and ScriptManJS. Of the three, Require JS emerged as the fan favorite and the community ran with it.

To define a module with Require JS –

//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

Require JS handles resolving the dependent modules (dep1 & dep2) and provides them as parameters to your function implementation once resolved.

Let’s create the above dependency management magic by building our very own custom module loader. This will help us understand what module loaders are.

Create a Loader class to host and serve dependencies –

/**
* Module Loader to host and serve dependencies.
*/
class Loader {
  constructor() {
     this.modules = {}; //Modules registry
  }    
}

Now that we have a modules registry, create public methods to ‘define a module’ and to ‘get a module’.

/**
 * Gets module defition from Modules registry
 * @param {String} name 
 */
getModule(name) {
    if (this.modules[name]) return this.modules[name];
    throw new Error(`Module not available -${name}`);
}

/**
 * Defines a module
 * @param {String} name 
 * @param {Array} dependencies 
 * @param {String} module 
 */
define(name, dependencies, module) {
    let depsInstances = dependencies.map((dep) => this.getModule(dep));
    this.modules[name] = module.apply(module, depsInstances);
}

getModule – This method returns the value from modules registry if the module name is present.

define – Similar to how you would define AMD modules, this method accepts a module name, an array of dependencies and the module implementation itself. Once the defined modules are resolved, the module is invoked with the resolved dependencies fetched from the modules registry.

The Loader class should now look like

{ //ES6 IIFE
    /**
     * Module Loader to host and serve dependencies.
     */
    class Loader {
        constructor() {
            this.modules = {}; //Modules registry
        }

        /**
         * Gets module defition from Modules registry
         * @param {String} name 
         */
        getModule(name) {
            if (this.modules[name]) return this.modules[name];
            throw new Error(`Module not available -${name}`);
        }

        /**
         * Defines a module
         * @param {String} name 
         * @param {Array} dependencies 
         * @param {String} module 
         */
        define(name, dependencies, module) {
            let depsInstances = dependencies.map((dep) => this.getModule(dep));
            this.modules[name] = module.apply(module, depsInstances);
        }
    }
    window.loader = new Loader();
}

The loader is initialized on the window. This way all reference to ‘loader’ in module definitions will be available.

That’s it! Our module loader is ready. To contrive an example, let’s create an app which has articles and comments on each article.

loader.define("App", ["Article", "Comment"], (Article, Comment) => {
    let article = new Article('Module Loaders');
    console.log(article.getType());

    let comment = new Comment('Modules rock!');
    console.log(comment.getType());
})

‘App’ requires ‘Article’ & ‘Comment’. Since an article and a comment both indicate a content type, create a base class ‘Content’.

loader.define("Content", [], () => {
    return class Content {
        constructor(name, type) {
            this.name = name;
            this.type = type;
        }

        getContent() {
            return this.name;
        }

        getType() {
            return this.type;
        }
    }
});

Fantastic! Moving onto ‘Article’ & ‘Comment’

loader.define("Article", ["Content"], (Content) => {
    return class Article extends Content {
        constructor(name) {
            super(name, 'article');
        }

        getContent() {
            return `Content: ${super.getContent()}`;
        }

    }
});

loader.define("Comment", ["Content"], (Content) => {
    return class Comment extends Content {
        constructor(name) {
            super(name, 'comment');
        }

        getContent() {
            return `Content: ${super.getContent()}`;
        }

    }
});

Run the ‘App’ module and you will find ‘article’ & ‘comment’ printed as a console log. This means that the App module was invoked when its dependencies were resolved by the loader. There is a downside to this implementation though – It assumes that all dependent module definitions are already defined. If ‘Content’ module is defined after ‘Article’ and ‘Comment’, the loader will not be able to resolve them since ‘Content’ is not defined yet.

Imagine the above implementation on steroids (module timeouts, error handling, modules alias, optimization, bundling etc). That’s exactly what you get with Require JS and modern module loaders. Hopefully, this article gave you a glimpse of how module loaders work. If you have questions, leave a comment.