Foreword
This is the second part of a three part article and for better understanding, I recommend reading the first part before reading this part.
Part II
From the Previous Part
The Challenge
To create an engine that will implement JavaScript’s missing functionality for including JavaScript files in other JavaScript files.
The Solution
In the first part, I presented a solution that can be easily deduced and implemented by everyone who has some knowledge about dynamic HTML and the way an external JavaScript file is loaded and executed. But it has a serious drawback: all files should contain only function declarations and object declarations (but only if they're not related to objects from other files).
Therefore, in this part, we’ll see a totally different approach.
Solution 2
The Logic
Let’s rethink the solution.
Ideally, only the part with the $include
function of the file should be executed – we will name this part header, and then, when the time is right, the rest – body. But as I stated above, it cannot be done. So if we talk about header and body, then we have a logical split of the file, and where there is a logical split, can’t there be a physical one? A header file (with a jsh extension) and a source file (the normal js file)? Something like in C?
Let’s examine the implications of this approach.
We first load all the headers, and from them, we can determine the order in which the source files must be loaded. Perfect. Just, what we need. From the previous implementation, we’ll keep the code that lets us specify a path relative to the IncludingEngine.js file and not to the current HTML document.
I did the implementation and another problem appeared: the JavaScript code embedded in the HTML document gets executed before finishing the execution of all external files added dynamically to the document’s head.
I did some research – reading and testing. While there are some people who argue that external JavaScript files are loaded in separate threads, and because of that, you can never be sure of the loading order and the execution order, I cannot agree with the last part.
From my tests, it seems indeed that the browsers (especially Firefox, not 100% sure about Internet Explorer) loads the scripts in different threads; but if a script ends loading before a previously defined script, its execution is delayed until the previous script ends its loading and execution, and after that is executed; this way, all scripts are executed in the right order.
Unfortunately, this order of execution is messed up if you dynamically insert or remove script objects. Now we’re in a situation of not knowing the order of execution, but we can avoid this by loading a script only after the previous one was loaded and executed.
However, the embedded JavaScript code will be executed long before the execution of external scripts (most of the time), but the other way around is a possibility too.
So our code from the HTML document can use the code from the external script only after these are all executed.
So we must have an event like window.onload
to tell us when the external files have finished their execution. Since there is the possibility for external scripts to be executed first, an event to handle both situations would be even better. And more intuitive would be a function that will be executed when “everything is ready”. And for obvious reasons, we will call this function Main
.
The Implementation
var IncludingEngine = {};
IncludingEngine.FindPath = function()
{
var scripts = document.getElementsByTagName("script");
var foundNo = 0;
for (var i=0; i<scripts.length; i++)
{
if (scripts[i].src.match(this.FileName))
{
this.Path = scripts[i].src.replace(this.FileName, "");
foundNo ++;
}
}
if (foundNo == 0)
throw new Error("The name of this file isn't " +
this.FileName + "!\r\nPlease change it back!");
if (foundNo > 1)
throw new Error("There are " + foundNo + " files with the name " +
this.FileName + "!\r\nThere can be only one!");
}
IncludingEngine.Init = function ()
{
this.FileName = "IncludingEngine.js";
this.Path = "";
this.FilesLoaded = new Array();
this.HeaderFilesStatus = new Array();
this.SourceFileStatus = new Array();
this.SourceFiles = new Array();
this.FindPath();
}
IncludingEngine.HeaderScriptLoad = function()
{
if (this.readyState)
if (this.readyState != "complete")
return;
IncludingEngine.HeaderFilesStatus[this.src] = 2;
var done = true;
for(var el in IncludingEngine.HeaderFilesStatus)
if (IncludingEngine.HeaderFilesStatus[el] != 2)
done = false;
if (done)
{
var head = document.getElementsByTagName("head")[0];
for (var k = IncludingEngine.SourceFiles.length - 1; k >= 0; k--)
{
if (IncludingEngine.SourceFileStatus[IncludingEngine.SourceFiles[k]] == 0)
{
var script = document.createElement("script");
script.src = IncludingEngine.SourceFiles[k];
script.type = "text/javascript";
script.onload = script.onreadystatechange =
IncludingEngine.SourceScriptLoad;
IncludingEngine.SourceFileStatus[IncludingEngine.SourceFiles[k]] = 1
head.appendChild(script);
}
}
}
var head = document.getElementsByTagName("head")[0];
head.removeChild(this);
}
IncludingEngine.SourceScriptLoad = function()
{
if (this.readyState)
if (this.readyState != "complete")
return;
IncludingEngine.SourceFileStatus[this.src] = 2;
var done = true;
for(var el in IncludingEngine.SourceFileStatus)
if (IncludingEngine.SourceFileStatus[el] != 2)
done = false;
if (done)
{
if (Main)
{
if (typeof Main == "function")
Main();
}
}
}
function $include()
{
var head = document.getElementsByTagName('head')[0];
for (var i = 0; i < arguments.length; i++)
{
if (IncludingEngine.HeaderFilesStatus[arguments[i] + ".jsh"] == null)
{
var script = document.createElement("script");
script.src = IncludingEngine.Path + arguments[i] + ".jsh";
script.type = 'text/javascript';
script.onload = script.onreadystatechange = IncludingEngine.HeaderScriptLoad;
IncludingEngine.SourceFiles.push(IncludingEngine.Path + arguments[i] + ".js");
IncludingEngine.SourceFileStatus[IncludingEngine.Path + arguments[i] + ".js"] = 0;
IncludingEngine.HeaderFilesStatus[script.src] = 1;
head.appendChild(script);
}
}
}
IncludingEngine.Init();
I think that the implementation is pretty much self-explanatory. Init
is mostly like in the first solution and FindPath
is identical. HeaderScriptLoad
is for loading the headers and generating the proper order to load the source scripts, and SourceScriptLoad
is for loading the source files in the order generated earlier.
As you can observe, I didn't check for circular dependencies; this is implemented in the next part.
Notes
- I’ll edit this post to add the link to the last part as soon as I post it.
- The latest version of this implementation can be found at IncludingEngine.jsFramework.com.
Here are all the parts of the "JavaScript Including Engine" article series: Part I, Part II, Part III. Also, you may want to visit the IncludingEngine website for more information.