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.