One of the weaknesses of the JavaScript language is that it does not have the concept of “importing” or “including” other source files or modules. In client-side JavaScript as used in web browsers you have to include all JavaScript files you intend to use in the main HTML file like this:
<html>
<head>
<title>...</title>
<script src="A.js"></script>
<script src="B.js"></script>
<script src="S.js"></script>
</head>
...
</body>
In this case S.js
is a page-specific JavaScript script that uses features from A.js
and B.js
.
The standard way to load other JavaScript files dynamically is to add a script
element programmatically to the head section of the document, using DOM:
function include(filename) {
var scriptTag = document.createElement("script");
scriptTag.setAttribute("lang"), "javascript"));
scriptTag.setAttribute("type"), "application/javascript"));
scriptTag.setAttribute("src", filename);
document.getElementsByTagName("head")[0].appendChild(scriptTag);
}
Unfortunately this approach has some problems as well: The include file is loaded “lazily”, i.e. the next time the JavaScript interpreter hands back control to the browser. (At least it is this way in Firefox.) If, for example, a file A.js
defines a function func_a
, the following will not work:
include("A.js");
func_a();
My first solution was to use a callback, invoked when the module has been loaded:
function include(filename, callback) {
var scriptTag = createScriptTag(filename); // as above
scriptTag.onreadystatechange = function() {
if (scriptTag.readyState == "complete") {
callback();
}
}
scriptTag.onload = function() {
callback(file);
}
}
This solution uses the non-standard onload
(Firefox) or onreadstatechange
(IE) events. It is invoked like this:
include("A.js", function() {
a_func();
});
This solution also has several problems: It is non-standard, it is ugly (though this can’t be helped, due to the asynchronous script loading), and including a module from a module still doesn’t work. To explain the last problem, I will use a script S.js
which depends on the function a_func
defined in A.js
, which in turn depends on b_func
defined in B.js
. Now the code should look like this:
// S.js
include("A.js", function() {
a_func();
});
// A.js
include("B.js", function() {
a_func = function() {
}
a_func.prototype = new b_func();
});
// B.js
// ...
The problem is that the callback in S.js
is not necessarily processed after the callback in A.js
, leading to a_func
being undefined.
The solution I have come up with is not particularily nice, but this can’t really be helped. Basically each module has to call a function when it has fully loaded, like this:
// A.js
include.module("B.js", function() {
// ...
include.moduleLoaded("A");
});
The main HTML file must load the include.js
before loading any other module and modules should only be loaded via include.module
. Also my implementation uses “real” module names, e.g. a.b.c
instead of a/b/c.js
. This allows me to expand the mechanism easily, for example to add a module search path.
Sample implementation