Building Multiple Modules

Developing Modular Applications with Mantri

Mantri enables you to break up your code in multiple modules that you can lazy-load on-demand on your visitor's browser. To get more into the spirit and terminology of Modular Applications, check out this blog post by the Mantri Author. Beware, building multiple modules is an advanced and very engaged process. That said, Mantri does it's best to smooth it out for you.

Terminology

A few terms that are used throughout the document need to be explained.

  • Built-Module The compiled (minified) output of a Module. Your core / base file is also considered a Module and has a Built-Module.
  • Static Linking With Static Linking every Built-Module contains the core stack and helping libraries.
  • Dynamic Linking With Dynamic Linking, every Built-Module does not contain the core stack or any helping libraries.

Getting Started

To create multiple Built-Modules you will need Multiple bootstrap files. Each bootstrap file would represent a single module you want to build & publish.

Depending on your usecase, if you are authoring a library or an end-user application, you would either need to apply the Static or Dynamic linking pattern.

Typically if you are authoring a library you'd need Static Linking, which allows you to include the Core and dependencies in the Built-Modules. And if you are creating an end-user application you'd need Dynamic Linking which dictates that every built-module only includes it's own source and nothing else.

The Scenario

In our scenario we will explore the Dynamic Linking pattern. Suppose we have a very large code-base and we want to split it in half so we can achieve optimized page loads. These two parts are the Core and ModuleA. The ModuleA's Built-Module should not have any overlapping code with the Core, so we have to apply the Dynamic Linking pattern.

Let's examine how to construct the two bootstrap files that represent the two modules (Core and ModuleA).

Multiple Bootstrap Files

The idea behind multiple bootstrap files is pretty simple, create a file that will sufficiently guide Mantri to discover all the needed dependencies and bundle them into Built-Modules. In our scenario, the Core Module obviously loads first and ModuleA will load at a later time. That means that Core's source will have already been fetched and evaluated by the browser so it does not need to be included in ModuleA. This is a very important concept that needs to be understood and applied throughout the codebase.

As counter-intuitive as it sounds, you should not have a goog.require() statement within ModuleA that requires any part of code that will be included in the Core Built-Module. If such a require statement exists, it will instruct Mantri to include that file in the ModuleA's Build-Module output and result in code overlap between the Core and ModuleA.

This is what the two bootstrap files should look like:

/js/core.js
// bootstrap file for just the App's Core.
goog.provide('app');

// generic helper functions
goog.require('app.util');

// our applications core
goog.require('app.router');
goog.require('app.xhr');
goog.require('app.ui');
/js/moduleA.js
// bootstrap file for ModuleA.
goog.provide('app.moduleA');

// As a stand-alone dynamically linked module,
// we do not require 'app.util' or 'app.core' on purpose.
// They have already been fetched in core and are available
// in the global namespace.
app.util.log('app.moduleA Kicking in!');

// require the moduleA's modules...
goog.require('app.dothis');
goog.require('app.dothat');

Configuring Mantri to Build Multiple Modules

Multiple Modules can be built in Mantri by using the key buildModules in your mantriConf.json file.

{
  "buildModules": {
    "app.moduleA": {
      "dest": "dist/app-module-A.min.js"
    }
  }
}

You can define as many Modules as you like. The key app.moduleA is directly referencing the namespace of ModuleA that we use in our scenario. You should replace that with the exact value that you define in the goog.provide() statement of your module's bootstrap file.

Each module Object in mantriConf.json can have the following keys:

  • dest Type: string Default: Required :: Where to save the compiled output.
  • sourceMapFile Type: string Default: None :: Optionally set where to save the sourcemap file.
  • sourceMapURL Type: string Default: None :: Optionally set where your browser should look for the sourceMap file.
  • outputWrapper Type: string Default: None :: Do not use this unless you know what you are doing, needs to properly export symbols. Read more about this next on "Exporting Symbols from Built-Modules".

Exporting Symbols from Built-Modules

Mantri uses the Google Closure Compiler (GCC) to produce the Built-Modules. As much as this helps in optimizing the codebase it also poses a few obstacles.

Suppose that a Module uses the same top-level namespace as your Core module. Like it happens in our scenario, app.moduleA uses the same top-level namespace app as the Core module does app.core. This will result in the produced Built-Module to overwrite the core. This happens because of the way GCC, rightly so, bootstraps your Built-Module.

This is the compiled output of GCC, the Built-Module, in pretty format:

var goog = goog || {};
var app = {
  moduleA:{
    dothis: {},
    dothat: {}
  }
};
app.moduleA.dothis.run = function(){};

/* ... */

As you can see, the key app is defined in the global namespace, this results in overwriting any previously defined app keys. This will effectively wipe out of memory the Core Module. Or every other module that was fetched and evaluated before this one.

To overcome this problem Mantri will wrap your Built-Module in an Immediately-Invoked Function Expression (IIFE) and properly export the root of the Module's namespace. In our example that is app.moduleA so the used outputWrapper would look like this, in pretty print with comments:

;(function(){

  // this is where the Built-Module will get appended
  // %output%;

  // "this" in this context refers to the global "window" object.
  this.app = this.app || {};

  // "app.moduleA" refers to the local symbol in the IFFE closure,
  // which had been defined above in the %output% placeholder.
  this.app.moduleA = app.moduleA;
}).call(this);

There can be more optimal ways of exporting symbols from Built-Modules but this is the bare-bones implementation that Mantri offers.

Mantri allows you to override this wrapper by defining your own using the outputWrapper key. Alternatively you can turn of wrapping altogether by setting a value of null to the outputWrapper key.

{
  "buildModules": {
    "app.moduleA": {
      "dest": "dist/app-module-A.min.js",
      "outputWrapper": "(function(global){%output%;global.something=app.moduleA;})(window)"
    },
    "app.moduleB": {
      "dest": "dist/app-module-B.min.js",
      "outputWrapper": null
    }
  }
}

Building Multiple Modules

Nothing new here, build like you always build, either from the CLI mantri build or as a grunt plugin grunt mantriBuild.

Loading Modules

Built-Module Loaders (aka Module Loaders) is beyond the scope of Mantri. This is one of the fine points where Mantri differs from RequireJS and other Module Loaders:

Built-Module Loading is a different concept than Dependency Management

There are several lightweight Module Loaders you can use, or you can create your own; using XHR with less than 10 lines of code.

Multiple Module Shortcomings

The current implementation of multiple modules requires that all the separate modules are pre-build before they can be fetched by the core when on development.

To implement such a workflow requires a build-step for each modification that happens in any of the modules. While this flow can easily be implemented with nowdays automation tools, it is far from optimal.

To solve this issue and enable single file fetching for all multiple modules the goog.provide and goog.require functions would need to be enhanced. If you need this enhancement then please let me know.

Example

You can checkout a full example using the classic ToDo MVC app in the multi-module branch of the todoAppMantri repository.