Sooner or later, every Javascript developer encounters the Module Pattern. It isn't always used for modules in the conventional sense, but even when it is used for modules, the files in which those modules reside are still often loaded by carefully hand-listing script-elements, in dependency order, in a HTML page.
/*!
* Modules library
*
* @example
* To trigger project loading in our HTML client page, all we need to do is to
* write an inline module, listing the dependencies we would like to use, and
* the code we would like to call, and our modules library will do the rest:
*
* <html>
* <head>
* <script src="modules.js"></script>
* </head>
* <body>
* <script>
* module('client',['main.js','debug.js'], function(main, debug) {
* var result = main.doSomething();
* debug.log(result);
* });
* </script>
* </body>
* </html>
*
* @link https://libraryinstitute.wordpress.com/2010/12/01/loading-javascript-modules/
*/
(function () {
//'use strict';
var modules = {}; // private record of module data
// modules are functions with additional information
function module(name, imports, mod) {
// record module information
window.console.log('found module ' + name);
modules[name] = {
name: name,
imports: imports,
mod: mod
};
// trigger loading of import dependencies
for (var imp in imports) {
if (imports.hasOwnProperty(imp)) {
loadModule(imports[imp]);
}
}
// check whether this was the last module to be loaded
// in a given dependency group
loadedModule(name);
}
// trigger module loading by adding script element
function loadModule(mod) {
if (modules[mod]) {
return; // don't load the same module twice
} else {
modules[mod] = {}; // mark module as currently loading
}
// add a script element to document head, with module as src
var element = document.createElement('script');
element.setAttribute('type', 'text/javascript');
element.setAttribute('src', mod);
document.getElementsByTagName('head')[0].appendChild(element);
}
// check whether this was the last module to be loaded
// in a given dependency group;
// if yes, start linking and running modules
function loadedModule(mod) {
window.console.log('finished loading: ' + mod);
// collect modules marked as currently loading
var pending = [];
for (var m in modules) {
if (modules.hasOwnProperty(m)) {
if (!modules[m].name) {
pending.push(m);
}
}
}
// if no more modules need to be loaded, we can start
// linking the modules together
if (pending.length === 0) {
window.console.log('all done loading');
linkModules();
} else {
window.console.log('loads pending: ' + pending.join(', '));
}
}
// Sort modules by dependencies (dependents last),
// returning sorted list of module names.
function dependencySort(modules) {
var pending = [], // modules remaining to be sorted
sorted = [], // modules already sorted
beenHere = {}; // remember length of pending list for each module
// (if we revisit a module without pending
// getting any shorter, we are stuck in a loop)
// preparation: linked modules do not need to be sorted,
// all others go into pending
for (var name in modules)
if (modules[name].linked) {
sorted.push(name); // allready linked by a previous run
} else {
pending.push(name); // sort for linking (after its dependencies)
}
// has mod been sorted already?
function issorted(mod) {
var result = false;
for (var s in sorted) {
if (sorted.hasOwnProperty(s)) {
result = result || (sorted[s] === mod);
}
}
return result;
}
// have all dependencies deps been sorted already?
function aresorted(deps) {
var result = true;
for (var d in deps) {
if (deps.hasOwnProperty(d)) {
result = result && (issorted(deps[d]));
}
}
return result;
}
// repeat while there are modules pending
while (pending.length > 0) {
// consider the next pending module
var m = pending.shift();
// if we've been here and have not made any progress, we are looping
// (no support for cyclic module dependencies)
if (beenHere[m] && beenHere[m] <= pending.length) {
throw ('can\'t sort dependencies: ' + sorted + ' < ' + m + ' < ' + pending);
} else {
beenHere[m] = pending.length;
}
// consider the current module's import dependencies
var deps = modules[m].imports;
if (aresorted(deps))
sorted.push(m); // dependencies done; module done
else
pending.push(m); // some dependencies still pending;
// revisit module later
}
return sorted;
}
// link and run loaded modules, keep record of results
function linkModules() {
window.console.log('linking modules');
// sort modules in dependency order
var sortedNames = dependencySort(modules);
// link modules in dependency order
for (var nextName in sortedNames) {
if (sortedNames.hasOwnProperty(nextName)) {
var name = sortedNames[nextName];
var module = modules[name];
var imports = module.imports;
if (module.linked) {
window.console.log('already linked ' + name);
continue;
}
window.console.log('linking module ' + name);
// collect import dependencies
var deps = [];
for (var i in imports) {
if (imports.hasOwnProperty(i)) {
deps.push(modules[imports[i]].linked);
}
}
// execute module code, pass imports, record exports
modules[name].linked = module.mod.apply(null, deps);
}
}
}
// export module wrapper
window.module = module;
// trigger module loading for our project
// NOTE: we assume that loadModule calls are executed before the
// script elements they create; otherwise, we have a possible
// race condition (linkModules could be called early, because
// not all dependencies are marked as currently loading yet)
// loadModule('utils.js');
// loadModule('debug.js');
// loadModule('module1.js');
// loadModule('module2.js');
// loadModule('main.js');
// just calling linkModules here would not work, as we have only
// added the script elements, the scripts could still be loading;
// calling linkModules in document onload would not work
// in browsers which do not stop parsing while script-inserted
// external scripts are loading;
// therefore, we call linkModules when all modules in a dependency
// group have been loaded, as checked by loadedModule;
})();